From 8c16517f101e7fac65fa290913409ac3a9cceaab Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Mon, 19 Jan 2015 07:38:10 +1100 Subject: [PATCH 01/35] building out new RDS2 Mock. completed: * create_db_instance() * create_option_group() --- moto/__init__.py | 1 + moto/rds2/__init__.py | 12 + moto/rds2/exceptions.py | 38 +++ moto/rds2/models.py | 476 +++++++++++++++++++++++++++++++++ moto/rds2/responses.py | 289 ++++++++++++++++++++ moto/rds2/urls.py | 10 + tests/test_rds2/test_rds2.py | 259 ++++++++++++++++++ tests/test_rds2/test_server.py | 20 ++ 8 files changed, 1105 insertions(+) create mode 100644 moto/rds2/__init__.py create mode 100644 moto/rds2/exceptions.py create mode 100644 moto/rds2/models.py create mode 100644 moto/rds2/responses.py create mode 100644 moto/rds2/urls.py create mode 100644 tests/test_rds2/test_rds2.py create mode 100644 tests/test_rds2/test_server.py diff --git a/moto/__init__.py b/moto/__init__.py index 965eaf4ee09c..0b0137d451bc 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -13,6 +13,7 @@ from .iam import mock_iam # flake8: noqa from .kinesis import mock_kinesis # flake8: noqa from .rds import mock_rds # flake8: noqa +from .rds2 import mock_rds2 # 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 diff --git a/moto/rds2/__init__.py b/moto/rds2/__init__.py new file mode 100644 index 000000000000..602c21ede630 --- /dev/null +++ b/moto/rds2/__init__.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals +from .models import rds2_backends +from ..core.models import MockAWS + +rds2_backend = rds2_backends['us-west-1'] + + +def mock_rds2(func=None): + if func: + return MockAWS(rds2_backends)(func) + else: + return MockAWS(rds2_backends) diff --git a/moto/rds2/exceptions.py b/moto/rds2/exceptions.py new file mode 100644 index 000000000000..936b979d23cb --- /dev/null +++ b/moto/rds2/exceptions.py @@ -0,0 +1,38 @@ +from __future__ import unicode_literals + +import json +from werkzeug.exceptions import BadRequest + + +class RDSClientError(BadRequest): + def __init__(self, code, message): + super(RDSClientError, self).__init__() + self.description = json.dumps({ + "Error": { + "Code": code, + "Message": message, + 'Type': 'Sender', + }, + 'RequestId': '6876f774-7273-11e4-85dc-39e55ca848d1', + }) + + +class DBInstanceNotFoundError(RDSClientError): + def __init__(self, database_identifier): + super(DBInstanceNotFoundError, self).__init__( + 'DBInstanceNotFound', + "Database {0} not found.".format(database_identifier)) + + +class DBSecurityGroupNotFoundError(RDSClientError): + def __init__(self, security_group_name): + super(DBSecurityGroupNotFoundError, self).__init__( + 'DBSecurityGroupNotFound', + "Security Group {0} not found.".format(security_group_name)) + + +class DBSubnetGroupNotFoundError(RDSClientError): + def __init__(self, subnet_group_name): + super(DBSubnetGroupNotFoundError, self).__init__( + 'DBSubnetGroupNotFound', + "Subnet Group {0} not found.".format(subnet_group_name)) diff --git a/moto/rds2/models.py b/moto/rds2/models.py new file mode 100644 index 000000000000..8e1511177143 --- /dev/null +++ b/moto/rds2/models.py @@ -0,0 +1,476 @@ +from __future__ import unicode_literals + +import copy + +import boto.rds2 +from jinja2 import Template + +from moto.cloudformation.exceptions import UnformattedGetAttTemplateException +from moto.core import BaseBackend +from moto.core.utils import get_random_hex +from moto.ec2.models import ec2_backends +from .exceptions import DBInstanceNotFoundError, DBSecurityGroupNotFoundError, DBSubnetGroupNotFoundError + + +class Database(object): + def __init__(self, **kwargs): + self.status = "available" + self.is_replica = False + self.replicas = [] + self.region = kwargs.get('region') + self.engine = kwargs.get("engine") + self.engine_version = kwargs.get("engine_version", None) + self.default_engine_versions = {"MySQL": "5.6.21", + "mysql": "5.6.21", + "oracle-se1": "11.2.0.4.v3", + "oracle-se": "11.2.0.4.v3", + "oracle-ee": "11.2.0.4.v3", + "sqlserver-ee": "11.00.2100.60.v1", + "sqlserver-se": "11.00.2100.60.v1", + "sqlserver-ex": "11.00.2100.60.v1", + "sqlserver-web": "11.00.2100.60.v1", + "postgres": "9.3.3" + } + if not self.engine_version and self.engine in self.default_engine_versions: + self.engine_version = self.default_engine_versions[self.engine] + self.iops = kwargs.get("iops") + self.storage_type = kwargs.get("storage_type") + self.master_username = kwargs.get('master_username') + self.master_user_password = kwargs.get('master_user_password') + self.auto_minor_version_upgrade = kwargs.get('auto_minor_version_upgrade') + if self.auto_minor_version_upgrade is None: + self.auto_minor_version_upgrade = True + self.allocated_storage = kwargs.get('allocated_storage') + self.db_instance_identifier = kwargs.get('db_instance_identifier') + self.source_db_identifier = kwargs.get("source_db_identifier") + self.db_instance_class = kwargs.get('db_instance_class') + self.port = kwargs.get('port') + self.db_instance_identifier = kwargs.get('db_instance_identifier') + self.db_name = kwargs.get("db_name") + self.publicly_accessible = kwargs.get("publicly_accessible") + if self.publicly_accessible is None: + self.publicly_accessible = True + self.backup_retention_period = kwargs.get("backup_retention_period") + if self.backup_retention_period is None: + self.backup_retention_period = 1 + self.availability_zone = kwargs.get("availability_zone") + self.multi_az = kwargs.get("multi_az") + self.db_subnet_group_name = kwargs.get("db_subnet_group_name") + if self.db_subnet_group_name: + self.db_subnet_group = rds2_backends[self.region].describe_subnet_groups(self.db_subnet_group_name)[0] + else: + self.db_subnet_group = [] + self.db_security_groups = kwargs.get('security_groups', ['a']) + self.vpc_security_group_ids = kwargs.get('vpc_security_group_ids', []) + self.preferred_maintenance_window = kwargs.get('preferred_maintenance_window', 'wed:06:38-wed:07:08') + self.db_parameter_group_name = kwargs.get('db_parameter_group_name', None) + self.default_parameter_groups = {"MySQL": "default.mysql5.6", + "mysql": "default.mysql5.6", + "postgres": "default.postgres9.3" + } + if not self.db_parameter_group_name and self.engine in self.default_parameter_groups: + self.db_parameter_group_name = self.default_parameter_groups[self.engine] + + self.preferred_backup_window = kwargs.get('preferred_backup_window', '13:14-13:44') + self.license_model = kwargs.get('license_model', 'general-public-license') + self.option_group_name = kwargs.get('option_group_name', None) + self.default_option_groups = {"MySQL": "default.mysql5.6", + "mysql": "default.mysql5.6", + "postgres": "default.postgres9.3" + } + if not self.option_group_name and self.engine in self.default_option_groups: + self.option_group_name = self.default_option_groups[self.engine] + self.character_set_name = kwargs.get('character_set_name', None) + self.tags = kwargs.get('tags', None) + + @property + def address(self): + return "{0}.aaaaaaaaaa.{1}.rds.amazonaws.com".format(self.db_instance_identifier, self.region) + + def add_replica(self, replica): + self.replicas.append(replica.db_instance_identifier) + + def remove_replica(self, replica): + self.replicas.remove(replica.db_instance_identifier) + + def set_as_replica(self): + self.is_replica = True + self.replicas = [] + + def update(self, db_kwargs): + for key, value in db_kwargs.items(): + if value is not None: + setattr(self, key, value) + + def get_cfn_attribute(self, attribute_name): + if attribute_name == 'Endpoint.Address': + return self.address + elif attribute_name == 'Endpoint.Port': + return self.port + raise UnformattedGetAttTemplateException() + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + + db_instance_identifier = properties.get('DBInstanceIdentifier') + if not db_instance_identifier: + db_instance_identifier = resource_name.lower() + get_random_hex(12) + db_security_groups = properties.get('DBSecurityGroups') + if not db_security_groups: + db_security_groups = [] + security_groups = [group.group_name for group in db_security_groups] + db_subnet_group = properties.get("DBSubnetGroupName") + db_subnet_group_name = db_subnet_group.subnet_name if db_subnet_group else None + db_kwargs = { + "auto_minor_version_upgrade": properties.get('AutoMinorVersionUpgrade'), + "allocated_storage": properties.get('AllocatedStorage'), + "availability_zone": properties.get("AvailabilityZone"), + "backup_retention_period": properties.get("BackupRetentionPeriod"), + "db_instance_class": properties.get('DBInstanceClass'), + "db_instance_identifier": db_instance_identifier, + "db_name": properties.get("DBName"), + "db_subnet_group_name": db_subnet_group_name, + "engine": properties.get("Engine"), + "engine_version": properties.get("EngineVersion"), + "iops": properties.get("Iops"), + "master_password": properties.get('MasterUserPassword'), + "master_username": properties.get('MasterUsername'), + "multi_az": properties.get("MultiAZ"), + "port": properties.get('Port', 3306), + "publicly_accessible": properties.get("PubliclyAccessible"), + "region": region_name, + "security_groups": security_groups, + "storage_type": properties.get("StorageType"), + } + + rds2_backend = rds2_backends[region_name] + source_db_identifier = properties.get("SourceDBInstanceIdentifier") + if source_db_identifier: + # Replica + db_kwargs["source_db_identifier"] = source_db_identifier.db_instance_identifier + database = rds2_backend.create_database_replica(db_kwargs) + else: + database = rds2_backend.create_database(db_kwargs) + return database + + def to_json(self): + template = Template(""""DBInstance": { + "AllocatedStorage": 10, + "AutoMinorVersionUpgrade": "{{ database.auto_minor_version_upgrade }}", + "AvailabilityZone": "{{ database.availability_zone }}", + "BackupRetentionPeriod": "{{ database.backup_retention_period }}", + "CharacterSetName": {%- if database.character_set_name -%}{{ database.character_set_name }}{%- else %} null{%- endif -%}, + "DBInstanceClass": "{{ database.db_instance_class }}", + "DBInstanceIdentifier": "{{ database.db_instance_identifier }}", + "DBInstanceStatus": "{{ database.status }}", + "DBName": {%- if database.db_name -%}{{ database.db_name }}{%- else %} null{%- endif -%}, + {% if database.db_parameter_group_name -%}"DBParameterGroups": { + "DBParameterGroup": { + "ParameterApplyStatus": "in-sync", + "DBParameterGroupName": "{{ database.db_parameter_group_name }}" + } + },{%- endif %} + "DBSecurityGroups": [{ + {% for security_group in database.db_security_groups -%}{%- if loop.index != 1 -%},{%- endif -%} + "DBSecurityGroup": { + "Status": "active", + "DBSecurityGroupName": "{{ security_group }}" + }{% endfor %} + }],{%- if database.db_subnet_group -%} + "DBSubnetGroup": { + "DBSubnetGroupDescription": "nabil-db-subnet-group", + "DBSubnetGroupName": "nabil-db-subnet-group", + "SubnetGroupStatus": "Complete", + "Subnets": [ + { + "SubnetAvailabilityZone": { + "Name": "us-west-2c", + "ProvisionedIopsCapable": false + }, + "SubnetIdentifier": "subnet-c0ea0099", + "SubnetStatus": "Active" + }, + { + "SubnetAvailabilityZone": { + "Name": "us-west-2a", + "ProvisionedIopsCapable": false + }, + "SubnetIdentifier": "subnet-ff885d88", + "SubnetStatus": "Active" + } + ], + "VpcId": "vpc-8e6ab6eb" + },{%- endif %} + "Engine": "{{ database.engine }}", + "EngineVersion": "{{ database.engine_version }}", + "LatestRestorableTime": null, + "LicenseModel": "{{ database.license_model }}", + "MasterUsername": "{{ database.master_username }}", + "MultiAZ": "{{ database.multi_az }}",{% if database.option_group_name %} + "OptionGroupMemberships": [{ + "OptionGroupMembership": { + "OptionGroupName": "{{ database.option_group_name }}", + "Status": "in-sync" + } + }],{%- endif %} + "PendingModifiedValues": { "MasterUserPassword": "****" }, + "PreferredBackupWindow": "{{ database.preferred_backup_window }}", + "PreferredMaintenanceWindow": "{{ database.preferred_maintenance_window }}", + "PubliclyAccessible": "{{ database.publicly_accessible }}", + "AllocatedStorage": "{{ database.allocated_storage }}", + "Endpoint": null, + "InstanceCreateTime": null, + "Iops": null, + "ReadReplicaDBInstanceIdentifiers": [], + "ReadReplicaSourceDBInstanceIdentifier": null, + "SecondaryAvailabilityZone": null, + "StatusInfos": null, + "VpcSecurityGroups": [ + { + "Status": "active", + "VpcSecurityGroupId": "sg-123456" + } + ] + }""") + return template.render(database=self) + + +class SecurityGroup(object): + def __init__(self, group_name, description): + self.group_name = group_name + self.description = description + self.status = "authorized" + self.ip_ranges = [] + self.ec2_security_groups = [] + + def to_xml(self): + template = Template(""" + + {% for security_group in security_group.ec2_security_groups %} + + {{ security_group.id }} + {{ security_group.name }} + {{ security_group.owner_id }} + authorized + + {% endfor %} + + + {{ security_group.description }} + + {% for ip_range in security_group.ip_ranges %} + + {{ ip_range }} + authorized + + {% endfor %} + + {{ security_group.ownder_id }} + {{ security_group.group_name }} + """) + return template.render(security_group=self) + + def authorize_cidr(self, cidr_ip): + self.ip_ranges.append(cidr_ip) + + def authorize_security_group(self, security_group): + self.ec2_security_groups.append(security_group) + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + group_name = resource_name.lower() + get_random_hex(12) + description = properties['GroupDescription'] + security_group_ingress = properties['DBSecurityGroupIngress'] + + ec2_backend = ec2_backends[region_name] + rds2_backend = rds2_backends[region_name] + security_group = rds2_backend.create_security_group( + group_name, + description, + ) + for ingress_type, ingress_value in security_group_ingress.items(): + if ingress_type == "CIDRIP": + security_group.authorize_cidr(ingress_value) + elif ingress_type == "EC2SecurityGroupName": + subnet = ec2_backend.get_security_group_from_name(ingress_value) + security_group.authorize_security_group(subnet) + elif ingress_type == "EC2SecurityGroupId": + subnet = ec2_backend.get_security_group_from_id(ingress_value) + security_group.authorize_security_group(subnet) + return security_group + + +class SubnetGroup(object): + def __init__(self, subnet_name, description, subnets): + self.subnet_name = subnet_name + self.description = description + self.subnets = subnets + self.status = "Complete" + + self.vpc_id = self.subnets[0].vpc_id + + def to_xml(self): + template = Template(""" + {{ subnet_group.vpc_id }} + {{ subnet_group.status }} + {{ subnet_group.description }} + {{ subnet_group.subnet_name }} + + {% for subnet in subnet_group.subnets %} + + Active + {{ subnet.id }} + + {{ subnet.availability_zone }} + false + + + {% endfor %} + + """) + return template.render(subnet_group=self) + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + + subnet_name = resource_name.lower() + get_random_hex(12) + description = properties['DBSubnetGroupDescription'] + subnet_ids = properties['SubnetIds'] + + ec2_backend = ec2_backends[region_name] + subnets = [ec2_backend.get_subnet(subnet_id) for subnet_id in subnet_ids] + rds2_backend = rds2_backends[region_name] + subnet_group = rds2_backend.create_subnet_group( + subnet_name, + description, + subnets, + ) + return subnet_group + + +class RDS2Backend(BaseBackend): + + def __init__(self): + self.databases = {} + self.security_groups = {} + self.subnet_groups = {} + self.option_groups = {} + + def create_database(self, db_kwargs): + database_id = db_kwargs['db_instance_identifier'] + database = Database(**db_kwargs) + self.databases[database_id] = database + return database + + def create_database_replica(self, db_kwargs): + database_id = db_kwargs['db_instance_identifier'] + source_database_id = db_kwargs['source_db_identifier'] + primary = self.describe_databases(source_database_id)[0] + replica = copy.deepcopy(primary) + replica.update(db_kwargs) + replica.set_as_replica() + self.databases[database_id] = replica + primary.add_replica(replica) + return replica + + def describe_databases(self, db_instance_identifier=None): + if db_instance_identifier: + if db_instance_identifier in self.databases: + return [self.databases[db_instance_identifier]] + else: + raise DBInstanceNotFoundError(db_instance_identifier) + return self.databases.values() + + def modify_database(self, db_instance_identifier, db_kwargs): + database = self.describe_databases(db_instance_identifier)[0] + database.update(db_kwargs) + return database + + def delete_database(self, db_instance_identifier): + if db_instance_identifier in self.databases: + database = self.databases.pop(db_instance_identifier) + if database.is_replica: + primary = self.describe_databases(database.source_db_identifier)[0] + primary.remove_replica(database) + return database + else: + raise DBInstanceNotFoundError(db_instance_identifier) + + def create_security_group(self, group_name, description): + security_group = SecurityGroup(group_name, description) + self.security_groups[group_name] = security_group + return security_group + + def describe_security_groups(self, security_group_name): + if security_group_name: + if security_group_name in self.security_groups: + return [self.security_groups[security_group_name]] + else: + raise DBSecurityGroupNotFoundError(security_group_name) + return self.security_groups.values() + + def delete_security_group(self, security_group_name): + if security_group_name in self.security_groups: + return self.security_groups.pop(security_group_name) + else: + raise DBSecurityGroupNotFoundError(security_group_name) + + def authorize_security_group(self, security_group_name, cidr_ip): + security_group = self.describe_security_groups(security_group_name)[0] + security_group.authorize_cidr(cidr_ip) + return security_group + + def create_subnet_group(self, subnet_name, description, subnets): + subnet_group = SubnetGroup(subnet_name, description, subnets) + self.subnet_groups[subnet_name] = subnet_group + return subnet_group + + def describe_subnet_groups(self, subnet_group_name): + if subnet_group_name: + if subnet_group_name in self.subnet_groups: + return [self.subnet_groups[subnet_group_name]] + else: + raise DBSubnetGroupNotFoundError(subnet_group_name) + return self.subnet_groups.values() + + def delete_subnet_group(self, subnet_name): + if subnet_name in self.subnet_groups: + return self.subnet_groups.pop(subnet_name) + else: + raise DBSubnetGroupNotFoundError(subnet_name) + + def create_option_group(self, option_group_kwargs): + option_group_id = option_group_kwargs['name'] + option_group = OptionGroup(**option_group_kwargs) + self.option_groups[option_group_id] = option_group + return option_group + + +class OptionGroup(object): + def __init__(self, name, engine_name, major_engine_version, description): + self.engine_name = engine_name + self.major_engine_version = major_engine_version + self.description = description + self.name = name + self.vpc_and_non_vpc_instance_memberships = False + self.options = {} + self.vpcId = 'null' + + def to_json(self): + template = Template("""{ + "VpcId": null, + "MajorEngineVersion": "{{ option_group.engine_name }}", + "OptionGroupDescription": "{{ option_group.description }}", + "AllowsVpcAndNonVpcInstanceMemberships": "{{ option_group.vpc_and_non_vpc_instance_memberships }}", + "EngineName": "{{ option_group.engine_name }}", + "Options": [], + "OptionGroupName": "{{ option_group.name }}" +}""") + return template.render(option_group=self) + +rds2_backends = {} +for region in boto.rds2.regions(): + rds2_backends[region.name] = RDS2Backend() diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py new file mode 100644 index 000000000000..7b2ece202ca8 --- /dev/null +++ b/moto/rds2/responses.py @@ -0,0 +1,289 @@ +from __future__ import unicode_literals + +from moto.core.responses import BaseResponse +from moto.ec2.models import ec2_backends +from .models import rds2_backends + + +class RDS2Response(BaseResponse): + + @property + def backend(self): + return rds2_backends[self.region] + + def _get_db_kwargs(self): + return { + "auto_minor_version_upgrade": self._get_param('AutoMinorVersionUpgrade'), + "allocated_storage": self._get_int_param('AllocatedStorage'), + "availability_zone": self._get_param("AvailabilityZone"), + "backup_retention_period": self._get_param("BackupRetentionPeriod"), + "db_instance_class": self._get_param('DBInstanceClass'), + "db_instance_identifier": self._get_param('DBInstanceIdentifier'), + "db_name": self._get_param("DBName"), + # DBParameterGroupName + "db_subnet_group_name": self._get_param("DBSubnetGroupName"), + "engine": self._get_param("Engine"), + "engine_version": self._get_param("EngineVersion"), + "iops": self._get_int_param("Iops"), + "master_password": self._get_param('MasterUserPassword'), + "master_username": self._get_param('MasterUsername'), + "multi_az": self._get_bool_param("MultiAZ"), + # OptionGroupName + "port": self._get_param('Port'), + # PreferredBackupWindow + # PreferredMaintenanceWindow + "publicly_accessible": self._get_param("PubliclyAccessible"), + "region": self.region, + "security_groups": self._get_multi_param('DBSecurityGroups.member'), + "storage_type": self._get_param("StorageType"), + # VpcSecurityGroupIds.member.N + } + + def _get_db_replica_kwargs(self): + return { + "auto_minor_version_upgrade": self._get_param('AutoMinorVersionUpgrade'), + "availability_zone": self._get_param("AvailabilityZone"), + "db_instance_class": self._get_param('DBInstanceClass'), + "db_instance_identifier": self._get_param('DBInstanceIdentifier'), + "db_subnet_group_name": self._get_param("DBSubnetGroupName"), + "iops": self._get_int_param("Iops"), + # OptionGroupName + "port": self._get_param('Port'), + "publicly_accessible": self._get_param("PubliclyAccessible"), + "source_db_identifier": self._get_param('SourceDBInstanceIdentifier'), + "storage_type": self._get_param("StorageType"), + } + + def _get_option_group_kwargs(self): + return { + 'major_engine_version': self._get_param('MajorEngineVersion'), + 'description': self._get_param('OptionGroupDescription'), + 'engine_name': self._get_param('EngineName'), + 'name': self._get_param('OptionGroupName') + } + + def create_dbinstance(self): + return self.create_db_instance() + + def create_db_instance(self): + db_kwargs = self._get_db_kwargs() + database = self.backend.create_database(db_kwargs) + template = self.response_template(CREATE_DATABASE_TEMPLATE) + result = template.render(database=database) + return result + + # TODO: Update function to new method + def create_dbinstance_read_replica(self): + db_kwargs = self._get_db_replica_kwargs() + + database = self.backend.create_database_replica(db_kwargs) + template = self.response_template(CREATE_DATABASE_REPLICA_TEMPLATE) + return template.render(database=database) + + def describe_dbinstances(self): + return self.describe_db_instances() + + def describe_dbinstances(self): + db_instance_identifier = self._get_param('DBInstanceIdentifier') + databases = self.backend.describe_databases(db_instance_identifier) + template = self.response_template(DESCRIBE_DATABASES_TEMPLATE) + return template.render(databases=databases) + + # TODO: Update function to new method + def modify_dbinstance(self): + db_instance_identifier = self._get_param('DBInstanceIdentifier') + db_kwargs = self._get_db_kwargs() + database = self.backend.modify_database(db_instance_identifier, db_kwargs) + template = self.response_template(MODIFY_DATABASE_TEMPLATE) + return template.render(database=database) + + # TODO: Update function to new method + def delete_dbinstance(self): + db_instance_identifier = self._get_param('DBInstanceIdentifier') + database = self.backend.delete_database(db_instance_identifier) + template = self.response_template(DELETE_DATABASE_TEMPLATE) + return template.render(database=database) + + # TODO: Update function to new method + def create_dbsecurity_group(self): + group_name = self._get_param('DBSecurityGroupName') + description = self._get_param('DBSecurityGroupDescription') + security_group = self.backend.create_security_group(group_name, description) + template = self.response_template(CREATE_SECURITY_GROUP_TEMPLATE) + return template.render(security_group=security_group) + + # TODO: Update function to new method + def describe_dbsecurity_groups(self): + security_group_name = self._get_param('DBSecurityGroupName') + security_groups = self.backend.describe_security_groups(security_group_name) + template = self.response_template(DESCRIBE_SECURITY_GROUPS_TEMPLATE) + return template.render(security_groups=security_groups) + + # TODO: Update function to new method + def delete_dbsecurity_group(self): + security_group_name = self._get_param('DBSecurityGroupName') + security_group = self.backend.delete_security_group(security_group_name) + template = self.response_template(DELETE_SECURITY_GROUP_TEMPLATE) + return template.render(security_group=security_group) + + # TODO: Update function to new method + def authorize_dbsecurity_group_ingress(self): + security_group_name = self._get_param('DBSecurityGroupName') + cidr_ip = self._get_param('CIDRIP') + security_group = self.backend.authorize_security_group(security_group_name, cidr_ip) + template = self.response_template(AUTHORIZE_SECURITY_GROUP_TEMPLATE) + return template.render(security_group=security_group) + + # TODO: Update function to new method + def create_dbsubnet_group(self): + subnet_name = self._get_param('DBSubnetGroupName') + description = self._get_param('DBSubnetGroupDescription') + subnet_ids = self._get_multi_param('SubnetIds.member') + subnets = [ec2_backends[self.region].get_subnet(subnet_id) for subnet_id in subnet_ids] + subnet_group = self.backend.create_subnet_group(subnet_name, description, subnets) + template = self.response_template(CREATE_SUBNET_GROUP_TEMPLATE) + return template.render(subnet_group=subnet_group) + + # TODO: Update function to new method + def describe_dbsubnet_groups(self): + subnet_name = self._get_param('DBSubnetGroupName') + subnet_groups = self.backend.describe_subnet_groups(subnet_name) + template = self.response_template(DESCRIBE_SUBNET_GROUPS_TEMPLATE) + return template.render(subnet_groups=subnet_groups) + + # TODO: Update function to new method + def delete_dbsubnet_group(self): + subnet_name = self._get_param('DBSubnetGroupName') + subnet_group = self.backend.delete_subnet_group(subnet_name) + template = self.response_template(DELETE_SUBNET_GROUP_TEMPLATE) + return template.render(subnet_group=subnet_group) + + def create_option_group(self): + kwargs = self._get_option_group_kwargs() + option_group = self.backend.create_option_group(kwargs) + template = self.response_template(CREATE_OPTION_GROUP_TEMPLATE) + return template.render(option_group=option_group) + +CREATE_DATABASE_TEMPLATE = """{ + "CreateDBInstanceResponse": { + "CreateDBInstanceResult": { + {{ database.to_json() }} + }, + "ResponseMetadata": { "RequestId": "523e3218-afc7-11c3-90f5-f90431260ab4" } + } +}""" + +CREATE_DATABASE_REPLICA_TEMPLATE = """ + + {{ database.to_xml() }} + + + ba8dedf0-bb9a-11d3-855b-576787000e19 + +""" + +DESCRIBE_DATABASES_TEMPLATE = """{ + "DescribeDBInstanceResponse": { + "DescribeDBInstanceResult": [ + {%- for database in databases -%} + {%- if loop.index != 1 -%},{%- endif -%} + { {{ database.to_json() }} } + {%- endfor -%} + ], + "ResponseMetadata": { "RequestId": "523e3218-afc7-11c3-90f5-f90431260ab4" } + } +}""" + +MODIFY_DATABASE_TEMPLATE = """ + + {{ database.to_xml() }} + + + f643f1ac-bbfe-11d3-f4c6-37db295f7674 + +""" + +DELETE_DATABASE_TEMPLATE = """ + + {{ database.to_xml() }} + + + 7369556f-b70d-11c3-faca-6ba18376ea1b + +""" + +CREATE_SECURITY_GROUP_TEMPLATE = """ + + {{ security_group.to_xml() }} + + + e68ef6fa-afc1-11c3-845a-476777009d19 + +""" + +DESCRIBE_SECURITY_GROUPS_TEMPLATE = """ + + + {% for security_group in security_groups %} + {{ security_group.to_xml() }} + {% endfor %} + + + + b76e692c-b98c-11d3-a907-5a2c468b9cb0 + +""" + +DELETE_SECURITY_GROUP_TEMPLATE = """ + + 7aec7454-ba25-11d3-855b-576787000e19 + +""" + +AUTHORIZE_SECURITY_GROUP_TEMPLATE = """ + + {{ security_group.to_xml() }} + + + 6176b5f8-bfed-11d3-f92b-31fa5e8dbc99 + +""" + +CREATE_SUBNET_GROUP_TEMPLATE = """ + + {{ subnet_group.to_xml() }} + + + 3a401b3f-bb9e-11d3-f4c6-37db295f7674 + +""" + +DESCRIBE_SUBNET_GROUPS_TEMPLATE = """ + + + {% for subnet_group in subnet_groups %} + {{ subnet_group.to_xml() }} + {% endfor %} + + + + b783db3b-b98c-11d3-fbc7-5c0aad74da7c + +""" + +DELETE_SUBNET_GROUP_TEMPLATE = """ + + 6295e5ab-bbf3-11d3-f4c6-37db295f7674 + +""" + +CREATE_OPTION_GROUP_TEMPLATE = """{ + "CreateOptionGroupResponse": { + "CreateOptionGroupResult": { + "OptionGroup": {{ option_group.to_json() }} + }, + "ResponseMetadata": { + "RequestId": "1e38dad4-9f50-11e4-87ea-a31c60ed2e36" + } + } +}""" diff --git a/moto/rds2/urls.py b/moto/rds2/urls.py new file mode 100644 index 000000000000..3c8a129a88dd --- /dev/null +++ b/moto/rds2/urls.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from .responses import RDS2Response + +url_bases = [ + "https?://rds.(.+).amazonaws.com", +] + +url_paths = { + '{0}/$': RDS2Response().dispatch, +} diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py new file mode 100644 index 000000000000..cd4934eb4f3a --- /dev/null +++ b/tests/test_rds2/test_rds2.py @@ -0,0 +1,259 @@ +from __future__ import unicode_literals + +import boto.rds2 +import boto.vpc +from boto.exception import BotoServerError +import sure # noqa + +from moto import mock_ec2, mock_rds2 +from tests.helpers import disable_on_py3 + + +@disable_on_py3() +@mock_rds2 +def test_create_database(): + conn = boto.rds2.connect_to_region("us-west-2") + database = conn.create_db_instance(db_instance_identifier='db-master-1', + allocated_storage=10, + engine='postgres', + db_instance_class='db.m1.small', + master_username='root', + master_user_password='hunter2', + db_security_groups=["my_sg"]) + database['CreateDBInstanceResponse']['CreateDBInstanceResult']['DBInstance']['DBInstanceStatus'].should.equal('available') + database['CreateDBInstanceResponse']['CreateDBInstanceResult']['DBInstance']['DBInstanceIdentifier'].should.equal("db-master-1") + database['CreateDBInstanceResponse']['CreateDBInstanceResult']['DBInstance']['AllocatedStorage'].should.equal('10') + database['CreateDBInstanceResponse']['CreateDBInstanceResult']['DBInstance']['DBInstanceClass'].should.equal("db.m1.small") + database['CreateDBInstanceResponse']['CreateDBInstanceResult']['DBInstance']['MasterUsername'].should.equal("root") + database['CreateDBInstanceResponse']['CreateDBInstanceResult']['DBInstance']['DBSecurityGroups'][0]['DBSecurityGroup']['DBSecurityGroupName'].should.equal('my_sg') + + +@disable_on_py3() +@mock_rds2 +def test_get_databases(): + conn = boto.rds2.connect_to_region("us-west-2") + + instances = conn.describe_db_instances() + list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(0) + + conn.create_db_instance(db_instance_identifier='db-master-1', + allocated_storage=10, + engine='postgres', + db_instance_class='db.m1.small', + master_username='root', + master_user_password='hunter2', + db_security_groups=["my_sg"]) + conn.create_db_instance(db_instance_identifier='db-master-2', + allocated_storage=10, + engine='postgres', + db_instance_class='db.m1.small', + master_username='root', + master_user_password='hunter2', + db_security_groups=["my_sg"]) + instances = conn.describe_db_instances() + list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(2) + + instances = conn.describe_db_instances("db-master-1") + list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(1) + instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult'][0]['DBInstance']['DBInstanceIdentifier'].should.equal("db-master-1") + + +@mock_rds2 +def test_describe_non_existant_database(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.describe_db_instances.when.called_with("not-a-db").should.throw(BotoServerError) + + +@mock_rds2 +def test_create_option_group(): + conn = boto.rds2.connect_to_region("us-west-2") + print conn.create_option_group('test', 'postgres', '9.3', 'test') + + +#@disable_on_py3() +#@mock_rds2 +#def test_delete_database(): +# conn = boto.rds2.connect_to_region("us-west-2") +# list(conn.get_all_dbinstances()).should.have.length_of(0) +# +# conn.create_dbinstance("db-master-1", 10, 'db.m1.small', 'root', 'hunter2') +# list(conn.get_all_dbinstances()).should.have.length_of(1) +# +# conn.delete_dbinstance("db-master-1") +# list(conn.get_all_dbinstances()).should.have.length_of(0) +# +# +#@mock_rds2 +#def test_delete_non_existant_database(): +# conn = boto.rds2.connect_to_region("us-west-2") +# conn.delete_dbinstance.when.called_with("not-a-db").should.throw(BotoServerError) + + +#@mock_rds2 +#def test_create_database_security_group(): +# conn = boto.rds2.connect_to_region("us-west-2") +# +# security_group = conn.create_dbsecurity_group('db_sg', 'DB Security Group') +# security_group.name.should.equal('db_sg') +# security_group.description.should.equal("DB Security Group") +# list(security_group.ip_ranges).should.equal([]) +# +# +#@mock_rds2 +#def test_get_security_groups(): +# conn = boto.rds2.connect_to_region("us-west-2") +# +# list(conn.get_all_dbsecurity_groups()).should.have.length_of(0) +# +# conn.create_dbsecurity_group('db_sg1', 'DB Security Group') +# conn.create_dbsecurity_group('db_sg2', 'DB Security Group') +# +# list(conn.get_all_dbsecurity_groups()).should.have.length_of(2) +# +# databases = conn.get_all_dbsecurity_groups("db_sg1") +# list(databases).should.have.length_of(1) +# +# databases[0].name.should.equal("db_sg1") +# +# +#@mock_rds2 +#def test_get_non_existant_security_group(): +# conn = boto.rds2.connect_to_region("us-west-2") +# conn.get_all_dbsecurity_groups.when.called_with("not-a-sg").should.throw(BotoServerError) +# +# +#@mock_rds2 +#def test_delete_database_security_group(): +# conn = boto.rds2.connect_to_region("us-west-2") +# conn.create_dbsecurity_group('db_sg', 'DB Security Group') +# +# list(conn.get_all_dbsecurity_groups()).should.have.length_of(1) +# +# conn.delete_dbsecurity_group("db_sg") +# list(conn.get_all_dbsecurity_groups()).should.have.length_of(0) +# +# +#@mock_rds2 +#def test_delete_non_existant_security_group(): +# conn = boto.rds2.connect_to_region("us-west-2") +# conn.delete_dbsecurity_group.when.called_with("not-a-db").should.throw(BotoServerError) +# +# +#@disable_on_py3() +#@mock_rds2 +#def test_security_group_authorize(): +# conn = boto.rds2.connect_to_region("us-west-2") +# security_group = conn.create_dbsecurity_group('db_sg', 'DB Security Group') +# list(security_group.ip_ranges).should.equal([]) +# +# security_group.authorize(cidr_ip='10.3.2.45/32') +# security_group = conn.get_all_dbsecurity_groups()[0] +# list(security_group.ip_ranges).should.have.length_of(1) +# security_group.ip_ranges[0].cidr_ip.should.equal('10.3.2.45/32') +# +# +#@disable_on_py3() +#@mock_rds2 +#def test_add_security_group_to_database(): +# conn = boto.rds2.connect_to_region("us-west-2") +# +# database = conn.create_dbinstance("db-master-1", 10, 'db.m1.small', 'root', 'hunter2') +# security_group = conn.create_dbsecurity_group('db_sg', 'DB Security Group') +# database.modify(security_groups=[security_group]) +# +# database = conn.get_all_dbinstances()[0] +# list(database.security_groups).should.have.length_of(1) +# +# database.security_groups[0].name.should.equal("db_sg") +# +# +#@mock_ec2 +#@mock_rds2 +#def test_add_database_subnet_group(): +# vpc_conn = boto.vpc.connect_to_region("us-west-2") +# vpc = vpc_conn.create_vpc("10.0.0.0/16") +# subnet1 = vpc_conn.create_subnet(vpc.id, "10.1.0.0/24") +# subnet2 = vpc_conn.create_subnet(vpc.id, "10.2.0.0/24") +# +# subnet_ids = [subnet1.id, subnet2.id] +# conn = boto.rds2.connect_to_region("us-west-2") +# subnet_group = conn.create_db_subnet_group("db_subnet", "my db subnet", subnet_ids) +# subnet_group.name.should.equal('db_subnet') +# subnet_group.description.should.equal("my db subnet") +# list(subnet_group.subnet_ids).should.equal(subnet_ids) +# +# +#@mock_ec2 +#@mock_rds2 +#def test_describe_database_subnet_group(): +# vpc_conn = boto.vpc.connect_to_region("us-west-2") +# vpc = vpc_conn.create_vpc("10.0.0.0/16") +# subnet = vpc_conn.create_subnet(vpc.id, "10.1.0.0/24") +# +# conn = boto.rds2.connect_to_region("us-west-2") +# conn.create_db_subnet_group("db_subnet1", "my db subnet", [subnet.id]) +# conn.create_db_subnet_group("db_subnet2", "my db subnet", [subnet.id]) +# +# list(conn.get_all_db_subnet_groups()).should.have.length_of(2) +# list(conn.get_all_db_subnet_groups("db_subnet1")).should.have.length_of(1) +# +# conn.get_all_db_subnet_groups.when.called_with("not-a-subnet").should.throw(BotoServerError) +# +# +#@mock_ec2 +#@mock_rds2 +#def test_delete_database_subnet_group(): +# vpc_conn = boto.vpc.connect_to_region("us-west-2") +# vpc = vpc_conn.create_vpc("10.0.0.0/16") +# subnet = vpc_conn.create_subnet(vpc.id, "10.1.0.0/24") +# +# conn = boto.rds2.connect_to_region("us-west-2") +# conn.create_db_subnet_group("db_subnet1", "my db subnet", [subnet.id]) +# list(conn.get_all_db_subnet_groups()).should.have.length_of(1) +# +# conn.delete_db_subnet_group("db_subnet1") +# list(conn.get_all_db_subnet_groups()).should.have.length_of(0) +# +# conn.delete_db_subnet_group.when.called_with("db_subnet1").should.throw(BotoServerError) +# +# +#@disable_on_py3() +#@mock_ec2 +#@mock_rds2 +#def test_create_database_in_subnet_group(): +# vpc_conn = boto.vpc.connect_to_region("us-west-2") +# vpc = vpc_conn.create_vpc("10.0.0.0/16") +# subnet = vpc_conn.create_subnet(vpc.id, "10.1.0.0/24") +# +# conn = boto.rds2.connect_to_region("us-west-2") +# conn.create_db_subnet_group("db_subnet1", "my db subnet", [subnet.id]) +# +# database = conn.create_dbinstance("db-master-1", 10, 'db.m1.small', +# 'root', 'hunter2', db_subnet_group_name="db_subnet1") +# +# database = conn.get_all_dbinstances("db-master-1")[0] +# database.subnet_group.name.should.equal("db_subnet1") +# +# +#@disable_on_py3() +#@mock_rds2 +#def test_create_database_replica(): +# conn = boto.rds2.connect_to_region("us-west-2") +# +# primary = conn.create_dbinstance("db-master-1", 10, 'db.m1.small', 'root', 'hunter2') +# +# replica = conn.create_dbinstance_read_replica("replica", "db-master-1", "db.m1.small") +# replica.id.should.equal("replica") +# replica.instance_class.should.equal("db.m1.small") +# status_info = replica.status_infos[0] +# status_info.normal.should.equal(True) +# status_info.status_type.should.equal('read replication') +# status_info.status.should.equal('replicating') +# +# primary = conn.get_all_dbinstances("db-master-1")[0] +# primary.read_replica_dbinstance_identifiers[0].should.equal("replica") +# +# conn.delete_dbinstance("replica") +# +# primary = conn.get_all_dbinstances("db-master-1")[0] +# list(primary.read_replica_dbinstance_identifiers).should.have.length_of(0) diff --git a/tests/test_rds2/test_server.py b/tests/test_rds2/test_server.py new file mode 100644 index 000000000000..19c2b6e9fa71 --- /dev/null +++ b/tests/test_rds2/test_server.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +import sure # noqa + +import moto.server as server +from moto import mock_rds2 + +''' +Test the different server responses +''' + + +#@mock_rds2 +#def test_list_databases(): +# backend = server.create_backend_app("rds2") +# test_client = backend.test_client() +# +# res = test_client.get('/?Action=DescribeDBInstances') +# +# res.data.decode("utf-8").should.contain("") From c6437930de0ca5d8959e8a39c8b6f03c8038668c Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Mon, 19 Jan 2015 08:06:37 +1100 Subject: [PATCH 02/35] fixed up tests for create_option_group, fixed return json to have major_version correct --- moto/rds2/models.py | 2 +- tests/test_rds2/test_rds2.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 8e1511177143..67a865c7f23b 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -462,7 +462,7 @@ def __init__(self, name, engine_name, major_engine_version, description): def to_json(self): template = Template("""{ "VpcId": null, - "MajorEngineVersion": "{{ option_group.engine_name }}", + "MajorEngineVersion": "{{ option_group.major_engine_version }}", "OptionGroupDescription": "{{ option_group.description }}", "AllowsVpcAndNonVpcInstanceMemberships": "{{ option_group.vpc_and_non_vpc_instance_memberships }}", "EngineName": "{{ option_group.engine_name }}", diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index cd4934eb4f3a..dd44719e477e 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -67,8 +67,11 @@ def test_describe_non_existant_database(): @mock_rds2 def test_create_option_group(): conn = boto.rds2.connect_to_region("us-west-2") - print conn.create_option_group('test', 'postgres', '9.3', 'test') - + option_group = conn.create_option_group('test', 'postgres', '9.3', 'test option group') + option_group['CreateOptionGroupResponse']['CreateOptionGroupResult']['OptionGroup']['OptionGroupName'].should.equal('test') + option_group['CreateOptionGroupResponse']['CreateOptionGroupResult']['OptionGroup']['EngineName'].should.equal('postgres') + option_group['CreateOptionGroupResponse']['CreateOptionGroupResult']['OptionGroup']['OptionGroupDescription'].should.equal('test option group') + option_group['CreateOptionGroupResponse']['CreateOptionGroupResult']['OptionGroup']['MajorEngineVersion'].should.equal('9.3') #@disable_on_py3() #@mock_rds2 From 40db44f2cdd3695e8358a62e88fe7b7732aaee2f Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Mon, 19 Jan 2015 17:03:14 +1100 Subject: [PATCH 03/35] Added Exceptions on create_option_group --- moto/rds2/exceptions.py | 1 + moto/rds2/models.py | 18 ++++++++++++++++-- tests/test_rds2/test_rds2.py | 18 ++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/moto/rds2/exceptions.py b/moto/rds2/exceptions.py index 936b979d23cb..a5c9356593f8 100644 --- a/moto/rds2/exceptions.py +++ b/moto/rds2/exceptions.py @@ -36,3 +36,4 @@ def __init__(self, subnet_group_name): super(DBSubnetGroupNotFoundError, self).__init__( 'DBSubnetGroupNotFound', "Subnet Group {0} not found.".format(subnet_group_name)) + diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 67a865c7f23b..f6edb240b12d 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -9,7 +9,7 @@ from moto.core import BaseBackend from moto.core.utils import get_random_hex from moto.ec2.models import ec2_backends -from .exceptions import DBInstanceNotFoundError, DBSecurityGroupNotFoundError, DBSubnetGroupNotFoundError +from .exceptions import RDSClientError, DBInstanceNotFoundError, DBSecurityGroupNotFoundError, DBSubnetGroupNotFoundError class Database(object): @@ -444,13 +444,27 @@ def delete_subnet_group(self, subnet_name): def create_option_group(self, option_group_kwargs): option_group_id = option_group_kwargs['name'] + valid_option_group_engines = {'postgres': ['9.3'], 'mysql': []} + + if 'description' not in option_group_kwargs or not option_group_kwargs['description']: + raise RDSClientError('InvalidParameterValue', + 'The parameter OptionGroupDescription must be provided and must not be blank.') + if option_group_kwargs['engine_name'] not in valid_option_group_engines.keys(): + raise RDSClientError('InvalidParameterValue', 'Invalid DB engine: non-existant') + if option_group_kwargs['major_engine_version'] not in\ + valid_option_group_engines[option_group_kwargs['engine_name']]: + raise RDSClientError('InvalidParameterCombination', + 'Cannot find major version {0} for {1}'.format( + option_group_kwargs['major_engine_version'], + option_group_kwargs['engine_name'] + )) option_group = OptionGroup(**option_group_kwargs) self.option_groups[option_group_id] = option_group return option_group class OptionGroup(object): - def __init__(self, name, engine_name, major_engine_version, description): + def __init__(self, name, engine_name, major_engine_version, description=None): self.engine_name = engine_name self.major_engine_version = major_engine_version self.description = description diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index dd44719e477e..d75f294f3c84 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -73,6 +73,24 @@ def test_create_option_group(): option_group['CreateOptionGroupResponse']['CreateOptionGroupResult']['OptionGroup']['OptionGroupDescription'].should.equal('test option group') option_group['CreateOptionGroupResponse']['CreateOptionGroupResult']['OptionGroup']['MajorEngineVersion'].should.equal('9.3') +@mock_rds2 +def test_create_option_group_bad_engine_name(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.create_option_group.when.called_with('test', 'invalid_engine', '9.3', 'test invalid engine').should.throw(BotoServerError) + + +@mock_rds2 +def test_create_option_group_bad_engine_major_version(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.create_option_group.when.called_with('test', 'postgres', '9.3.a', 'test invalid engine version').should.throw(BotoServerError) + + +@mock_rds2 +def test_create_option_group_empty_description(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.create_option_group.when.called_with('test', 'postgres', '9.3', '').should.throw(BotoServerError) + + #@disable_on_py3() #@mock_rds2 #def test_delete_database(): From 503d46d36a0edef77f71628f952373aebc9370b5 Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Mon, 19 Jan 2015 20:29:32 +1100 Subject: [PATCH 04/35] Added decribe_option_groups and delete_option_group support --- moto/rds2/models.py | 35 +++++++++++++++++++++++++++++++++++ moto/rds2/responses.py | 32 ++++++++++++++++++++++++++++++++ tests/test_rds2/test_rds2.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index f6edb240b12d..ef2aa4c025e5 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -3,6 +3,7 @@ import copy import boto.rds2 +import json from jinja2 import Template from moto.cloudformation.exceptions import UnformattedGetAttTemplateException @@ -462,6 +463,40 @@ def create_option_group(self, option_group_kwargs): self.option_groups[option_group_id] = option_group return option_group + def delete_option_group(self, option_group_name): + if option_group_name in self.option_groups: + return self.option_groups.pop(option_group_name) + else: + raise RDSClientError('OptionGroupNotFoundFault', 'Specified OptionGroupName: {} not found.'.format(option_group_name)) + + def describe_option_groups(self, option_group_kwargs): + option_group_list = [] + + if option_group_kwargs['marker']: + marker = option_group_kwargs['marker'] + else: + marker = 0 + if option_group_kwargs['max_records']: + max_records = option_group_kwargs['max_records'] + else: + max_records = 100 + + for option_group_name, option_group in self.option_groups.items(): + if option_group_kwargs['name'] and option_group.name != option_group_kwargs['name']: + continue + elif option_group_kwargs['engine_name'] and \ + option_group.engine_name != option_group_kwargs['engine_name']: + continue + elif option_group_kwargs['major_engine_version'] and \ + option_group.major_engine_version != option_group_kwargs['major_engine_version']: + continue + else: + option_group_list.append(option_group) + if not len(option_group_list): + raise RDSClientError('OptionGroupNotFoundFault', + 'Specified OptionGroupName: {} not found.'.format(option_group_kwargs['name'])) + return option_group_list[marker:max_records+marker] + class OptionGroup(object): def __init__(self, name, engine_name, major_engine_version, description=None): diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index 7b2ece202ca8..eaf3cb87abe4 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -164,6 +164,21 @@ def create_option_group(self): template = self.response_template(CREATE_OPTION_GROUP_TEMPLATE) return template.render(option_group=option_group) + def delete_option_group(self): + kwargs = self._get_option_group_kwargs() + option_group = self.backend.delete_option_group(kwargs['name']) + template = self.response_template(DELETE_OPTION_GROUP_TEMPLATE) + return template.render(option_group=option_group) + + def describe_option_groups(self): + kwargs = self._get_option_group_kwargs() + kwargs['max_records'] = self._get_param('MaxRecords') + kwargs['marker'] = self._get_param('Marker') + option_groups = self.backend.describe_option_groups(kwargs) + template = self.response_template(DESCRIBE_OPTION_GROUP_TEMPLATE) + return template.render(option_groups=option_groups) + + CREATE_DATABASE_TEMPLATE = """{ "CreateDBInstanceResponse": { "CreateDBInstanceResult": { @@ -287,3 +302,20 @@ def create_option_group(self): } } }""" + +DELETE_OPTION_GROUP_TEMPLATE = \ + """{"DeleteOptionGroupResponse": {"ResponseMetadata": {"RequestId": "e2590367-9fa2-11e4-99cf-55e92d41c60e"}}}""" + +DESCRIBE_OPTION_GROUP_TEMPLATE = \ + """{"DescribeOptionGroupsResponse": { + "DescribeOptionGroupsResult": { + "Marker": null, + "OptionGroupsList": [ + {%- for option_group in option_groups -%} + {%- if loop.index != 1 -%},{%- endif -%} + {{ option_group.to_json() }} + {%- endfor -%} + ]}, + "ResponseMetadata": {"RequestId": "4caf445d-9fbc-11e4-87ea-a31c60ed2e36"} + }}""" + diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index d75f294f3c84..384c112dd23c 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -91,6 +91,36 @@ def test_create_option_group_empty_description(): conn.create_option_group.when.called_with('test', 'postgres', '9.3', '').should.throw(BotoServerError) +@mock_rds2 +def test_describe_option_group(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.create_option_group('test', 'postgres', '9.3', 'test option group') + option_groups = conn.describe_option_groups('test') + option_groups['DescribeOptionGroupsResponse']['DescribeOptionGroupsResult']['OptionGroupsList'][0]['OptionGroupName'].should.equal('test') + +@mock_rds2 +def test_describe_non_existant_option_group(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.describe_option_groups.when.called_with("not-a-option-group").should.throw(BotoServerError) + + +@mock_rds2 +def test_delete_option_group(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.create_option_group('test', 'postgres', '9.3', 'test option group') + option_groups = conn.describe_option_groups('test') + option_groups['DescribeOptionGroupsResponse']['DescribeOptionGroupsResult']['OptionGroupsList'][0]['OptionGroupName'].should.equal('test') + conn.delete_option_group('test') + conn.describe_option_groups.when.called_with('test').should.throw(BotoServerError) + + +@mock_rds2 +def test_delete_non_existant_option_group(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.delete_option_group.when.called_with('non-existant').should.throw(BotoServerError) + + + #@disable_on_py3() #@mock_rds2 #def test_delete_database(): From a43b002c3a50ae00d65e0b0dfbe798ca5dd52fd7 Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Tue, 20 Jan 2015 07:18:52 +1100 Subject: [PATCH 05/35] Added describe_option_group_options. --- moto/rds2/models.py | 62 +++++++++++++++++++++++++++++++++++- moto/rds2/responses.py | 18 +++++++++++ tests/test_rds2/test_rds2.py | 28 +++++++++++----- 3 files changed, 99 insertions(+), 9 deletions(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index ef2aa4c025e5..fd3307cfa97f 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -445,7 +445,13 @@ def delete_subnet_group(self, subnet_name): def create_option_group(self, option_group_kwargs): option_group_id = option_group_kwargs['name'] - valid_option_group_engines = {'postgres': ['9.3'], 'mysql': []} + valid_option_group_engines = {'mysql': ['5.6'], + 'oracle-se1': ['11.2'], + 'oracle-se': ['11.2'], + 'oracle-ee': ['11.2'], + 'sqlserver-se': ['10.50', '11.00'], + 'sqlserver-ee': ['10.50', '11.00'] + } if 'description' not in option_group_kwargs or not option_group_kwargs['description']: raise RDSClientError('InvalidParameterValue', @@ -477,6 +483,9 @@ def describe_option_groups(self, option_group_kwargs): else: marker = 0 if option_group_kwargs['max_records']: + if option_group_kwargs['max_records'] < 20 or option_group_kwargs['max_records'] > 100: + raise RDSClientError('InvalidParameterValue', + 'Invalid value for max records. Must be between 20 and 100') max_records = option_group_kwargs['max_records'] else: max_records = 100 @@ -497,6 +506,35 @@ def describe_option_groups(self, option_group_kwargs): 'Specified OptionGroupName: {} not found.'.format(option_group_kwargs['name'])) return option_group_list[marker:max_records+marker] + @staticmethod + def describe_option_group_options(engine_name, major_engine_version=None): + default_option_group_options = { + 'mysql': {'all': '{"DescribeOptionGroupOptionsResponse": {"DescribeOptionGroupOptionsResult": {"Marker": null, "OptionGroupOptions": [{"MinimumRequiredMinorEngineVersion": "12", "OptionsDependedOn": [], "MajorEngineVersion": "5.6", "Persistent": false, "DefaultPort": 11211, "Permanent": false, "OptionGroupOptionSettings": [{"SettingDescription": "Specifies how many memcached read operations (get) to perform before doing a COMMIT to start a new transaction", "DefaultValue": "1", "AllowedValues": "1-4294967295", "IsModifiable": true, "SettingName": "DAEMON_MEMCACHED_R_BATCH_SIZE", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies how many memcached write operations, such as add, set, or incr, to perform before doing a COMMIT to start a new transaction", "DefaultValue": "1", "AllowedValues": "1-4294967295", "IsModifiable": true, "SettingName": "DAEMON_MEMCACHED_W_BATCH_SIZE", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies how often to auto-commit idle connections that use the InnoDB memcached interface.", "DefaultValue": "5", "AllowedValues": "1-1073741824", "IsModifiable": true, "SettingName": "INNODB_API_BK_COMMIT_INTERVAL", "ApplyType": "DYNAMIC"}, {"SettingDescription": "Disables the use of row locks when using the InnoDB memcached interface.", "DefaultValue": "0", "AllowedValues": "0,1", "IsModifiable": true, "SettingName": "INNODB_API_DISABLE_ROWLOCK", "ApplyType": "STATIC"}, {"SettingDescription": "Locks the table used by the InnoDB memcached plugin, so that it cannot be dropped or altered by DDL through the SQL interface.", "DefaultValue": "0", "AllowedValues": "0,1", "IsModifiable": true, "SettingName": "INNODB_API_ENABLE_MDL", "ApplyType": "STATIC"}, {"SettingDescription": "Lets you control the transaction isolation level on queries processed by the memcached interface.", "DefaultValue": "0", "AllowedValues": "0-3", "IsModifiable": true, "SettingName": "INNODB_API_TRX_LEVEL", "ApplyType": "STATIC"}, {"SettingDescription": "The binding protocol to use which can be either auto, ascii, or binary. The default is auto which means the server automatically negotiates the protocol with the client.", "DefaultValue": "auto", "AllowedValues": "auto,ascii,binary", "IsModifiable": true, "SettingName": "BINDING_PROTOCOL", "ApplyType": "STATIC"}, {"SettingDescription": "The backlog queue configures how many network connections can be waiting to be processed by memcached", "DefaultValue": "1024", "AllowedValues": "1-2048", "IsModifiable": true, "SettingName": "BACKLOG_QUEUE_LIMIT", "ApplyType": "STATIC"}, {"SettingDescription": "Disable the use of compare and swap (CAS) which reduces the per-item size by 8 bytes.", "DefaultValue": "0", "AllowedValues": "0,1", "IsModifiable": true, "SettingName": "CAS_DISABLED", "ApplyType": "STATIC"}, {"SettingDescription": "Minimum chunk size in bytes to allocate for the smallest item\'s key, value, and flags. The default is 48 and you can get a significant memory efficiency gain with a lower value.", "DefaultValue": "48", "AllowedValues": "1-48", "IsModifiable": true, "SettingName": "CHUNK_SIZE", "ApplyType": "STATIC"}, {"SettingDescription": "Chunk size growth factor that controls the size of each successive chunk with each chunk growing times this amount larger than the previous chunk.", "DefaultValue": "1.25", "AllowedValues": "1-2", "IsModifiable": true, "SettingName": "CHUNK_SIZE_GROWTH_FACTOR", "ApplyType": "STATIC"}, {"SettingDescription": "If enabled when there is no more memory to store items, memcached will return an error rather than evicting items.", "DefaultValue": "0", "AllowedValues": "0,1", "IsModifiable": true, "SettingName": "ERROR_ON_MEMORY_EXHAUSTED", "ApplyType": "STATIC"}, {"SettingDescription": "Maximum number of concurrent connections. Setting this value to anything less than 10 prevents MySQL from starting.", "DefaultValue": "1024", "AllowedValues": "10-1024", "IsModifiable": true, "SettingName": "MAX_SIMULTANEOUS_CONNECTIONS", "ApplyType": "STATIC"}, {"SettingDescription": "Verbose level for memcached.", "DefaultValue": "v", "AllowedValues": "v,vv,vvv", "IsModifiable": true, "SettingName": "VERBOSITY", "ApplyType": "STATIC"}], "EngineName": "mysql", "Name": "MEMCACHED", "PortRequired": true, "Description": "Innodb Memcached for MySQL"}]}, "ResponseMetadata": {"RequestId": "c9847a08-9fca-11e4-9084-5754f80d5144"}}}', + '5.6': '{"DescribeOptionGroupOptionsResponse": {"DescribeOptionGroupOptionsResult": {"Marker": null, "OptionGroupOptions": [{"MinimumRequiredMinorEngineVersion": "12", "OptionsDependedOn": [], "MajorEngineVersion": "5.6", "Persistent": false, "DefaultPort": 11211, "Permanent": false, "OptionGroupOptionSettings": [{"SettingDescription": "Specifies how many memcached read operations (get) to perform before doing a COMMIT to start a new transaction", "DefaultValue": "1", "AllowedValues": "1-4294967295", "IsModifiable": true, "SettingName": "DAEMON_MEMCACHED_R_BATCH_SIZE", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies how many memcached write operations, such as add, set, or incr, to perform before doing a COMMIT to start a new transaction", "DefaultValue": "1", "AllowedValues": "1-4294967295", "IsModifiable": true, "SettingName": "DAEMON_MEMCACHED_W_BATCH_SIZE", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies how often to auto-commit idle connections that use the InnoDB memcached interface.", "DefaultValue": "5", "AllowedValues": "1-1073741824", "IsModifiable": true, "SettingName": "INNODB_API_BK_COMMIT_INTERVAL", "ApplyType": "DYNAMIC"}, {"SettingDescription": "Disables the use of row locks when using the InnoDB memcached interface.", "DefaultValue": "0", "AllowedValues": "0,1", "IsModifiable": true, "SettingName": "INNODB_API_DISABLE_ROWLOCK", "ApplyType": "STATIC"}, {"SettingDescription": "Locks the table used by the InnoDB memcached plugin, so that it cannot be dropped or altered by DDL through the SQL interface.", "DefaultValue": "0", "AllowedValues": "0,1", "IsModifiable": true, "SettingName": "INNODB_API_ENABLE_MDL", "ApplyType": "STATIC"}, {"SettingDescription": "Lets you control the transaction isolation level on queries processed by the memcached interface.", "DefaultValue": "0", "AllowedValues": "0-3", "IsModifiable": true, "SettingName": "INNODB_API_TRX_LEVEL", "ApplyType": "STATIC"}, {"SettingDescription": "The binding protocol to use which can be either auto, ascii, or binary. The default is auto which means the server automatically negotiates the protocol with the client.", "DefaultValue": "auto", "AllowedValues": "auto,ascii,binary", "IsModifiable": true, "SettingName": "BINDING_PROTOCOL", "ApplyType": "STATIC"}, {"SettingDescription": "The backlog queue configures how many network connections can be waiting to be processed by memcached", "DefaultValue": "1024", "AllowedValues": "1-2048", "IsModifiable": true, "SettingName": "BACKLOG_QUEUE_LIMIT", "ApplyType": "STATIC"}, {"SettingDescription": "Disable the use of compare and swap (CAS) which reduces the per-item size by 8 bytes.", "DefaultValue": "0", "AllowedValues": "0,1", "IsModifiable": true, "SettingName": "CAS_DISABLED", "ApplyType": "STATIC"}, {"SettingDescription": "Minimum chunk size in bytes to allocate for the smallest item\'s key, value, and flags. The default is 48 and you can get a significant memory efficiency gain with a lower value.", "DefaultValue": "48", "AllowedValues": "1-48", "IsModifiable": true, "SettingName": "CHUNK_SIZE", "ApplyType": "STATIC"}, {"SettingDescription": "Chunk size growth factor that controls the size of each successive chunk with each chunk growing times this amount larger than the previous chunk.", "DefaultValue": "1.25", "AllowedValues": "1-2", "IsModifiable": true, "SettingName": "CHUNK_SIZE_GROWTH_FACTOR", "ApplyType": "STATIC"}, {"SettingDescription": "If enabled when there is no more memory to store items, memcached will return an error rather than evicting items.", "DefaultValue": "0", "AllowedValues": "0,1", "IsModifiable": true, "SettingName": "ERROR_ON_MEMORY_EXHAUSTED", "ApplyType": "STATIC"}, {"SettingDescription": "Maximum number of concurrent connections. Setting this value to anything less than 10 prevents MySQL from starting.", "DefaultValue": "1024", "AllowedValues": "10-1024", "IsModifiable": true, "SettingName": "MAX_SIMULTANEOUS_CONNECTIONS", "ApplyType": "STATIC"}, {"SettingDescription": "Verbose level for memcached.", "DefaultValue": "v", "AllowedValues": "v,vv,vvv", "IsModifiable": true, "SettingName": "VERBOSITY", "ApplyType": "STATIC"}], "EngineName": "mysql", "Name": "MEMCACHED", "PortRequired": true, "Description": "Innodb Memcached for MySQL"}]}, "ResponseMetadata": {"RequestId": "c9847a08-9fca-11e4-9084-5754f80d5144"}}}', + }, + 'sqlserver-ee': {'all': '{"DescribeOptionGroupOptionsResponse": {"DescribeOptionGroupOptionsResult": {"Marker": null, "OptionGroupOptions": [{"MinimumRequiredMinorEngineVersion": "2789.0.v1", "OptionsDependedOn": [], "MajorEngineVersion": "10.50", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "sqlserver-ee", "Name": "Mirroring", "PortRequired": false, "Description": "SQLServer Database Mirroring"}, {"MinimumRequiredMinorEngineVersion": "2789.0.v1", "OptionsDependedOn": [], "MajorEngineVersion": "10.50", "Persistent": true, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "sqlserver-ee", "Name": "TDE", "PortRequired": false, "Description": "SQL Server - Transparent Data Encryption"}, {"MinimumRequiredMinorEngineVersion": "2100.60.v1", "OptionsDependedOn": [], "MajorEngineVersion": "11.00", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "sqlserver-ee", "Name": "Mirroring", "PortRequired": false, "Description": "SQLServer Database Mirroring"}, {"MinimumRequiredMinorEngineVersion": "2100.60.v1", "OptionsDependedOn": [], "MajorEngineVersion": "11.00", "Persistent": true, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "sqlserver-ee", "Name": "TDE", "PortRequired": false, "Description": "SQL Server - Transparent Data Encryption"}]}, "ResponseMetadata": {"RequestId": "c9f2fd9b-9fcb-11e4-8add-31b6fe33145f"}}}', + '10.50': '{"DescribeOptionGroupOptionsResponse": {"DescribeOptionGroupOptionsResult": {"Marker": null, "OptionGroupOptions": [{"MinimumRequiredMinorEngineVersion": "2789.0.v1", "OptionsDependedOn": [], "MajorEngineVersion": "10.50", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "sqlserver-ee", "Name": "Mirroring", "PortRequired": false, "Description": "SQLServer Database Mirroring"}, {"MinimumRequiredMinorEngineVersion": "2789.0.v1", "OptionsDependedOn": [], "MajorEngineVersion": "10.50", "Persistent": true, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "sqlserver-ee", "Name": "TDE", "PortRequired": false, "Description": "SQL Server - Transparent Data Encryption"}]}, "ResponseMetadata": {"RequestId": "e6326fd0-9fcb-11e4-99cf-55e92d4bbada"}}}', + '11.00': '{"DescribeOptionGroupOptionsResponse": {"DescribeOptionGroupOptionsResult": {"Marker": null, "OptionGroupOptions": [{"MinimumRequiredMinorEngineVersion": "2100.60.v1", "OptionsDependedOn": [], "MajorEngineVersion": "11.00", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "sqlserver-ee", "Name": "Mirroring", "PortRequired": false, "Description": "SQLServer Database Mirroring"}, {"MinimumRequiredMinorEngineVersion": "2100.60.v1", "OptionsDependedOn": [], "MajorEngineVersion": "11.00", "Persistent": true, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "sqlserver-ee", "Name": "TDE", "PortRequired": false, "Description": "SQL Server - Transparent Data Encryption"}]}, "ResponseMetadata": {"RequestId": "222cbeeb-9fcc-11e4-bb07-576f5bf522b5"}}}' + }, + 'oracle-ee': {'all': '{"DescribeOptionGroupOptionsResponse": {"DescribeOptionGroupOptionsResult": {"Marker": null, "OptionGroupOptions": [{"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": ["XMLDB"], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "APEX", "PortRequired": false, "Description": "Oracle Application Express Runtime Environment"}, {"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": ["APEX"], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "APEX-DEV", "PortRequired": false, "Description": "Oracle Application Express Development Environment"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [{"SettingDescription": "Specifies the desired encryption behavior", "DefaultValue": "REQUESTED", "AllowedValues": "ACCEPTED,REJECTED,REQUESTED,REQUIRED", "IsModifiable": true, "SettingName": "SQLNET.ENCRYPTION_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies the desired data integrity behavior", "DefaultValue": "REQUESTED", "AllowedValues": "ACCEPTED,REJECTED,REQUESTED,REQUIRED", "IsModifiable": true, "SettingName": "SQLNET.CRYPTO_CHECKSUM_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies list of encryption algorithms in order of intended use", "DefaultValue": "RC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40", "AllowedValues": "RC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40", "IsModifiable": true, "SettingName": "SQLNET.ENCRYPTION_TYPES_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies list of checksumming algorithms in order of intended use", "DefaultValue": "SHA1,MD5", "AllowedValues": "SHA1,MD5", "IsModifiable": true, "SettingName": "SQLNET.CRYPTO_CHECKSUM_TYPES_SERVER", "ApplyType": "STATIC"}], "EngineName": "oracle-ee", "Name": "NATIVE_NETWORK_ENCRYPTION", "PortRequired": false, "Description": "Oracle Advanced Security - Native Network Encryption"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": 1158, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "OEM", "PortRequired": true, "Description": "Oracle Enterprise Manager (Database Control only)"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "STATSPACK", "PortRequired": false, "Description": "Oracle Statspack"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "TDE", "PortRequired": false, "Description": "Oracle Advanced Security - Transparent Data Encryption"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "TDE_HSM", "PortRequired": false, "Description": "Oracle Advanced Security - TDE with HSM"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [{"SettingDescription": "Specifies the timezone the user wants to change the system time to", "DefaultValue": "UTC", "AllowedValues": "Africa/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTC", "IsModifiable": true, "SettingName": "TIME_ZONE", "ApplyType": "DYNAMIC"}], "EngineName": "oracle-ee", "Name": "Timezone", "PortRequired": false, "Description": "Change time zone"}, {"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "XMLDB", "PortRequired": false, "Description": "Oracle XMLDB Repository"}]}, "ResponseMetadata": {"RequestId": "36a0a612-9fcc-11e4-a07c-e12b0fcebb71"}}}', + '11.2': '{"DescribeOptionGroupOptionsResponse": {"DescribeOptionGroupOptionsResult": {"Marker": null, "OptionGroupOptions": [{"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": ["XMLDB"], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "APEX", "PortRequired": false, "Description": "Oracle Application Express Runtime Environment"}, {"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": ["APEX"], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "APEX-DEV", "PortRequired": false, "Description": "Oracle Application Express Development Environment"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [{"SettingDescription": "Specifies the desired encryption behavior", "DefaultValue": "REQUESTED", "AllowedValues": "ACCEPTED,REJECTED,REQUESTED,REQUIRED", "IsModifiable": true, "SettingName": "SQLNET.ENCRYPTION_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies the desired data integrity behavior", "DefaultValue": "REQUESTED", "AllowedValues": "ACCEPTED,REJECTED,REQUESTED,REQUIRED", "IsModifiable": true, "SettingName": "SQLNET.CRYPTO_CHECKSUM_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies list of encryption algorithms in order of intended use", "DefaultValue": "RC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40", "AllowedValues": "RC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40", "IsModifiable": true, "SettingName": "SQLNET.ENCRYPTION_TYPES_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies list of checksumming algorithms in order of intended use", "DefaultValue": "SHA1,MD5", "AllowedValues": "SHA1,MD5", "IsModifiable": true, "SettingName": "SQLNET.CRYPTO_CHECKSUM_TYPES_SERVER", "ApplyType": "STATIC"}], "EngineName": "oracle-ee", "Name": "NATIVE_NETWORK_ENCRYPTION", "PortRequired": false, "Description": "Oracle Advanced Security - Native Network Encryption"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": 1158, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "OEM", "PortRequired": true, "Description": "Oracle Enterprise Manager (Database Control only)"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "STATSPACK", "PortRequired": false, "Description": "Oracle Statspack"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "TDE", "PortRequired": false, "Description": "Oracle Advanced Security - Transparent Data Encryption"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "TDE_HSM", "PortRequired": false, "Description": "Oracle Advanced Security - TDE with HSM"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [{"SettingDescription": "Specifies the timezone the user wants to change the system time to", "DefaultValue": "UTC", "AllowedValues": "Africa/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTC", "IsModifiable": true, "SettingName": "TIME_ZONE", "ApplyType": "DYNAMIC"}], "EngineName": "oracle-ee", "Name": "Timezone", "PortRequired": false, "Description": "Change time zone"}, {"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "XMLDB", "PortRequired": false, "Description": "Oracle XMLDB Repository"}]}, "ResponseMetadata": {"RequestId": "36a0a612-9fcc-11e4-a07c-e12b0fcebb71"}}}' + }, + 'oracle-sa': {'all': '{"DescribeOptionGroupOptionsResponse": {"DescribeOptionGroupOptionsResult": {"Marker": null, "OptionGroupOptions": [{"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": ["XMLDB"], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "APEX", "PortRequired": false, "Description": "Oracle Application Express Runtime Environment"}, {"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": ["APEX"], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "APEX-DEV", "PortRequired": false, "Description": "Oracle Application Express Development Environment"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [{"SettingDescription": "Specifies the desired encryption behavior", "DefaultValue": "REQUESTED", "AllowedValues": "ACCEPTED,REJECTED,REQUESTED,REQUIRED", "IsModifiable": true, "SettingName": "SQLNET.ENCRYPTION_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies the desired data integrity behavior", "DefaultValue": "REQUESTED", "AllowedValues": "ACCEPTED,REJECTED,REQUESTED,REQUIRED", "IsModifiable": true, "SettingName": "SQLNET.CRYPTO_CHECKSUM_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies list of encryption algorithms in order of intended use", "DefaultValue": "RC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40", "AllowedValues": "RC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40", "IsModifiable": true, "SettingName": "SQLNET.ENCRYPTION_TYPES_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies list of checksumming algorithms in order of intended use", "DefaultValue": "SHA1,MD5", "AllowedValues": "SHA1,MD5", "IsModifiable": true, "SettingName": "SQLNET.CRYPTO_CHECKSUM_TYPES_SERVER", "ApplyType": "STATIC"}], "EngineName": "oracle-ee", "Name": "NATIVE_NETWORK_ENCRYPTION", "PortRequired": false, "Description": "Oracle Advanced Security - Native Network Encryption"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": 1158, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "OEM", "PortRequired": true, "Description": "Oracle Enterprise Manager (Database Control only)"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "STATSPACK", "PortRequired": false, "Description": "Oracle Statspack"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "TDE", "PortRequired": false, "Description": "Oracle Advanced Security - Transparent Data Encryption"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "TDE_HSM", "PortRequired": false, "Description": "Oracle Advanced Security - TDE with HSM"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [{"SettingDescription": "Specifies the timezone the user wants to change the system time to", "DefaultValue": "UTC", "AllowedValues": "Africa/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTC", "IsModifiable": true, "SettingName": "TIME_ZONE", "ApplyType": "DYNAMIC"}], "EngineName": "oracle-ee", "Name": "Timezone", "PortRequired": false, "Description": "Change time zone"}, {"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "XMLDB", "PortRequired": false, "Description": "Oracle XMLDB Repository"}]}, "ResponseMetadata": {"RequestId": "36a0a612-9fcc-11e4-a07c-e12b0fcebb71"}}}', + '11.2': '{"DescribeOptionGroupOptionsResponse": {"DescribeOptionGroupOptionsResult": {"Marker": null, "OptionGroupOptions": [{"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": ["XMLDB"], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "APEX", "PortRequired": false, "Description": "Oracle Application Express Runtime Environment"}, {"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": ["APEX"], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "APEX-DEV", "PortRequired": false, "Description": "Oracle Application Express Development Environment"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [{"SettingDescription": "Specifies the desired encryption behavior", "DefaultValue": "REQUESTED", "AllowedValues": "ACCEPTED,REJECTED,REQUESTED,REQUIRED", "IsModifiable": true, "SettingName": "SQLNET.ENCRYPTION_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies the desired data integrity behavior", "DefaultValue": "REQUESTED", "AllowedValues": "ACCEPTED,REJECTED,REQUESTED,REQUIRED", "IsModifiable": true, "SettingName": "SQLNET.CRYPTO_CHECKSUM_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies list of encryption algorithms in order of intended use", "DefaultValue": "RC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40", "AllowedValues": "RC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40", "IsModifiable": true, "SettingName": "SQLNET.ENCRYPTION_TYPES_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies list of checksumming algorithms in order of intended use", "DefaultValue": "SHA1,MD5", "AllowedValues": "SHA1,MD5", "IsModifiable": true, "SettingName": "SQLNET.CRYPTO_CHECKSUM_TYPES_SERVER", "ApplyType": "STATIC"}], "EngineName": "oracle-ee", "Name": "NATIVE_NETWORK_ENCRYPTION", "PortRequired": false, "Description": "Oracle Advanced Security - Native Network Encryption"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": 1158, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "OEM", "PortRequired": true, "Description": "Oracle Enterprise Manager (Database Control only)"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "STATSPACK", "PortRequired": false, "Description": "Oracle Statspack"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "TDE", "PortRequired": false, "Description": "Oracle Advanced Security - Transparent Data Encryption"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "TDE_HSM", "PortRequired": false, "Description": "Oracle Advanced Security - TDE with HSM"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [{"SettingDescription": "Specifies the timezone the user wants to change the system time to", "DefaultValue": "UTC", "AllowedValues": "Africa/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTC", "IsModifiable": true, "SettingName": "TIME_ZONE", "ApplyType": "DYNAMIC"}], "EngineName": "oracle-ee", "Name": "Timezone", "PortRequired": false, "Description": "Change time zone"}, {"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "XMLDB", "PortRequired": false, "Description": "Oracle XMLDB Repository"}]}, "ResponseMetadata": {"RequestId": "36a0a612-9fcc-11e4-a07c-e12b0fcebb71"}}}' + }, + 'oracle-sa1': {'all': '{"DescribeOptionGroupOptionsResponse": {"DescribeOptionGroupOptionsResult": {"Marker": null, "OptionGroupOptions": [{"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": ["XMLDB"], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "APEX", "PortRequired": false, "Description": "Oracle Application Express Runtime Environment"}, {"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": ["APEX"], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "APEX-DEV", "PortRequired": false, "Description": "Oracle Application Express Development Environment"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [{"SettingDescription": "Specifies the desired encryption behavior", "DefaultValue": "REQUESTED", "AllowedValues": "ACCEPTED,REJECTED,REQUESTED,REQUIRED", "IsModifiable": true, "SettingName": "SQLNET.ENCRYPTION_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies the desired data integrity behavior", "DefaultValue": "REQUESTED", "AllowedValues": "ACCEPTED,REJECTED,REQUESTED,REQUIRED", "IsModifiable": true, "SettingName": "SQLNET.CRYPTO_CHECKSUM_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies list of encryption algorithms in order of intended use", "DefaultValue": "RC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40", "AllowedValues": "RC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40", "IsModifiable": true, "SettingName": "SQLNET.ENCRYPTION_TYPES_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies list of checksumming algorithms in order of intended use", "DefaultValue": "SHA1,MD5", "AllowedValues": "SHA1,MD5", "IsModifiable": true, "SettingName": "SQLNET.CRYPTO_CHECKSUM_TYPES_SERVER", "ApplyType": "STATIC"}], "EngineName": "oracle-ee", "Name": "NATIVE_NETWORK_ENCRYPTION", "PortRequired": false, "Description": "Oracle Advanced Security - Native Network Encryption"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": 1158, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "OEM", "PortRequired": true, "Description": "Oracle Enterprise Manager (Database Control only)"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "STATSPACK", "PortRequired": false, "Description": "Oracle Statspack"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "TDE", "PortRequired": false, "Description": "Oracle Advanced Security - Transparent Data Encryption"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "TDE_HSM", "PortRequired": false, "Description": "Oracle Advanced Security - TDE with HSM"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [{"SettingDescription": "Specifies the timezone the user wants to change the system time to", "DefaultValue": "UTC", "AllowedValues": "Africa/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTC", "IsModifiable": true, "SettingName": "TIME_ZONE", "ApplyType": "DYNAMIC"}], "EngineName": "oracle-ee", "Name": "Timezone", "PortRequired": false, "Description": "Change time zone"}, {"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "XMLDB", "PortRequired": false, "Description": "Oracle XMLDB Repository"}]}, "ResponseMetadata": {"RequestId": "36a0a612-9fcc-11e4-a07c-e12b0fcebb71"}}}', + '11.2': '{"DescribeOptionGroupOptionsResponse": {"DescribeOptionGroupOptionsResult": {"Marker": null, "OptionGroupOptions": [{"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": ["XMLDB"], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "APEX", "PortRequired": false, "Description": "Oracle Application Express Runtime Environment"}, {"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": ["APEX"], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "APEX-DEV", "PortRequired": false, "Description": "Oracle Application Express Development Environment"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [{"SettingDescription": "Specifies the desired encryption behavior", "DefaultValue": "REQUESTED", "AllowedValues": "ACCEPTED,REJECTED,REQUESTED,REQUIRED", "IsModifiable": true, "SettingName": "SQLNET.ENCRYPTION_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies the desired data integrity behavior", "DefaultValue": "REQUESTED", "AllowedValues": "ACCEPTED,REJECTED,REQUESTED,REQUIRED", "IsModifiable": true, "SettingName": "SQLNET.CRYPTO_CHECKSUM_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies list of encryption algorithms in order of intended use", "DefaultValue": "RC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40", "AllowedValues": "RC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40", "IsModifiable": true, "SettingName": "SQLNET.ENCRYPTION_TYPES_SERVER", "ApplyType": "STATIC"}, {"SettingDescription": "Specifies list of checksumming algorithms in order of intended use", "DefaultValue": "SHA1,MD5", "AllowedValues": "SHA1,MD5", "IsModifiable": true, "SettingName": "SQLNET.CRYPTO_CHECKSUM_TYPES_SERVER", "ApplyType": "STATIC"}], "EngineName": "oracle-ee", "Name": "NATIVE_NETWORK_ENCRYPTION", "PortRequired": false, "Description": "Oracle Advanced Security - Native Network Encryption"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": 1158, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "OEM", "PortRequired": true, "Description": "Oracle Enterprise Manager (Database Control only)"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "STATSPACK", "PortRequired": false, "Description": "Oracle Statspack"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "TDE", "PortRequired": false, "Description": "Oracle Advanced Security - Transparent Data Encryption"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "TDE_HSM", "PortRequired": false, "Description": "Oracle Advanced Security - TDE with HSM"}, {"MinimumRequiredMinorEngineVersion": "0.2.v3", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": true, "DefaultPort": null, "Permanent": true, "OptionGroupOptionSettings": [{"SettingDescription": "Specifies the timezone the user wants to change the system time to", "DefaultValue": "UTC", "AllowedValues": "Africa/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTC", "IsModifiable": true, "SettingName": "TIME_ZONE", "ApplyType": "DYNAMIC"}], "EngineName": "oracle-ee", "Name": "Timezone", "PortRequired": false, "Description": "Change time zone"}, {"MinimumRequiredMinorEngineVersion": "0.2.v4", "OptionsDependedOn": [], "MajorEngineVersion": "11.2", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "oracle-ee", "Name": "XMLDB", "PortRequired": false, "Description": "Oracle XMLDB Repository"}]}, "ResponseMetadata": {"RequestId": "36a0a612-9fcc-11e4-a07c-e12b0fcebb71"}}}' + } + } + if engine_name not in default_option_group_options: + raise RDSClientError('InvalidParameterValue', 'Invalid DB engine: {}'.format(engine_name)) + if major_engine_version and major_engine_version not in default_option_group_options[engine_name]: + raise RDSClientError('InvalidParameterCombination', + 'Cannot find major version {} for {}'.format(major_engine_version, engine_name)) + if major_engine_version: + return default_option_group_options[engine_name][major_engine_version] + return default_option_group_options[engine_name]['all'] + class OptionGroup(object): def __init__(self, name, engine_name, major_engine_version, description=None): @@ -520,6 +558,28 @@ def to_json(self): }""") return template.render(option_group=self) + +class OptionGroupOption(object): + def __init__(self, engine_name, major_engine_version): + self.engine_name = engine_name + self.major_engine_version = major_engine_version + + def to_json(self): + template = Template("""{ "MinimumRequiredMinorEngineVersion": + "2789.0.v1", + "OptionsDependedOn": [], + "MajorEngineVersion": "10.50", + "Persistent": false, + "DefaultPort": null, + "Permanent": false, + "OptionGroupOptionSettings": [], + "EngineName": "sqlserver-se", + "Name": "Mirroring", + "PortRequired": false, + "Description": "SQLServer Database Mirroring" + }""") + return template.render(option_group=self) + rds2_backends = {} for region in boto.rds2.regions(): rds2_backends[region.name] = RDS2Backend() diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index eaf3cb87abe4..471946772ca3 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -3,6 +3,7 @@ from moto.core.responses import BaseResponse from moto.ec2.models import ec2_backends from .models import rds2_backends +import json class RDS2Response(BaseResponse): @@ -178,6 +179,11 @@ def describe_option_groups(self): template = self.response_template(DESCRIBE_OPTION_GROUP_TEMPLATE) return template.render(option_groups=option_groups) + def describe_option_group_options(self): + engine_name = self._get_param('EngineName') + major_engine_version = self._get_param('MajorEngineVersion') + option_group_options = self.backend.describe_option_group_options(engine_name, major_engine_version) + return option_group_options CREATE_DATABASE_TEMPLATE = """{ "CreateDBInstanceResponse": { @@ -319,3 +325,15 @@ def describe_option_groups(self): "ResponseMetadata": {"RequestId": "4caf445d-9fbc-11e4-87ea-a31c60ed2e36"} }}""" +DESCRIBE_OPTION_GROUP_OPTIONS_TEMPLATE = \ + """{"DescribeOptionGroupOptionsResponse": { + "DescribeOptionGroupOptionsResult": { + "Marker": null, + "OptionGroupOptions": [ + {%- for option_group_option in option_group_options -%} + {%- if loop.index != 1 -%},{%- endif -%} + {{ option_group_option.to_json() }} + {%- endfor -%} + ]}, + "ResponseMetadata": {"RequestId": "457f7bb8-9fbf-11e4-9084-5754f80d5144"} + }}""" \ No newline at end of file diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 384c112dd23c..4c565717838a 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -67,37 +67,38 @@ def test_describe_non_existant_database(): @mock_rds2 def test_create_option_group(): conn = boto.rds2.connect_to_region("us-west-2") - option_group = conn.create_option_group('test', 'postgres', '9.3', 'test option group') + option_group = conn.create_option_group('test', 'mysql', '5.6', 'test option group') option_group['CreateOptionGroupResponse']['CreateOptionGroupResult']['OptionGroup']['OptionGroupName'].should.equal('test') - option_group['CreateOptionGroupResponse']['CreateOptionGroupResult']['OptionGroup']['EngineName'].should.equal('postgres') + option_group['CreateOptionGroupResponse']['CreateOptionGroupResult']['OptionGroup']['EngineName'].should.equal('mysql') option_group['CreateOptionGroupResponse']['CreateOptionGroupResult']['OptionGroup']['OptionGroupDescription'].should.equal('test option group') - option_group['CreateOptionGroupResponse']['CreateOptionGroupResult']['OptionGroup']['MajorEngineVersion'].should.equal('9.3') + option_group['CreateOptionGroupResponse']['CreateOptionGroupResult']['OptionGroup']['MajorEngineVersion'].should.equal('5.6') @mock_rds2 def test_create_option_group_bad_engine_name(): conn = boto.rds2.connect_to_region("us-west-2") - conn.create_option_group.when.called_with('test', 'invalid_engine', '9.3', 'test invalid engine').should.throw(BotoServerError) + conn.create_option_group.when.called_with('test', 'invalid_engine', '5.6', 'test invalid engine').should.throw(BotoServerError) @mock_rds2 def test_create_option_group_bad_engine_major_version(): conn = boto.rds2.connect_to_region("us-west-2") - conn.create_option_group.when.called_with('test', 'postgres', '9.3.a', 'test invalid engine version').should.throw(BotoServerError) + conn.create_option_group.when.called_with('test', 'mysql', '6.6.6', 'test invalid engine version').should.throw(BotoServerError) @mock_rds2 def test_create_option_group_empty_description(): conn = boto.rds2.connect_to_region("us-west-2") - conn.create_option_group.when.called_with('test', 'postgres', '9.3', '').should.throw(BotoServerError) + conn.create_option_group.when.called_with('test', 'mysql', '5.6', '').should.throw(BotoServerError) @mock_rds2 def test_describe_option_group(): conn = boto.rds2.connect_to_region("us-west-2") - conn.create_option_group('test', 'postgres', '9.3', 'test option group') + conn.create_option_group('test', 'mysql', '5.6', 'test option group') option_groups = conn.describe_option_groups('test') option_groups['DescribeOptionGroupsResponse']['DescribeOptionGroupsResult']['OptionGroupsList'][0]['OptionGroupName'].should.equal('test') + @mock_rds2 def test_describe_non_existant_option_group(): conn = boto.rds2.connect_to_region("us-west-2") @@ -107,7 +108,7 @@ def test_describe_non_existant_option_group(): @mock_rds2 def test_delete_option_group(): conn = boto.rds2.connect_to_region("us-west-2") - conn.create_option_group('test', 'postgres', '9.3', 'test option group') + conn.create_option_group('test', 'mysql', '5.6', 'test option group') option_groups = conn.describe_option_groups('test') option_groups['DescribeOptionGroupsResponse']['DescribeOptionGroupsResult']['OptionGroupsList'][0]['OptionGroupName'].should.equal('test') conn.delete_option_group('test') @@ -120,6 +121,17 @@ def test_delete_non_existant_option_group(): conn.delete_option_group.when.called_with('non-existant').should.throw(BotoServerError) +@mock_rds2 +def test_describe_option_group_options(): + conn = boto.rds2.connect_to_region("us-west-2") + option_group_options = conn.describe_option_group_options('sqlserver-ee') + len(option_group_options['DescribeOptionGroupOptionsResponse']['DescribeOptionGroupOptionsResult']['OptionGroupOptions']).should.equal(4) + option_group_options = conn.describe_option_group_options('sqlserver-ee', '11.00') + len(option_group_options['DescribeOptionGroupOptionsResponse']['DescribeOptionGroupOptionsResult']['OptionGroupOptions']).should.equal(2) + option_group_options = conn.describe_option_group_options('mysql', '5.6') + len(option_group_options['DescribeOptionGroupOptionsResponse']['DescribeOptionGroupOptionsResult']['OptionGroupOptions']).should.equal(1) + conn.describe_option_group_options.when.called_with('non-existent').should.throw(BotoServerError) + conn.describe_option_group_options.when.called_with('mysql', 'non-existent').should.throw(BotoServerError) #@disable_on_py3() #@mock_rds2 From dfb33aaad31f632bcdfd4e17ad2b0b1cf34fee37 Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Tue, 20 Jan 2015 21:01:34 +1100 Subject: [PATCH 06/35] Started implementing modify_option_group. --- moto/rds2/models.py | 23 ++++++++++++++++++++++- moto/rds2/responses.py | 26 ++++++++++++++++++++++++++ tests/test_rds2/test_rds2.py | 27 +++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index fd3307cfa97f..63d31324115f 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -452,7 +452,9 @@ def create_option_group(self, option_group_kwargs): 'sqlserver-se': ['10.50', '11.00'], 'sqlserver-ee': ['10.50', '11.00'] } - + if option_group_kwargs['name'] in self.option_groups: + raise RDSClientError('OptionGroupAlreadyExistsFault', + 'An option group named {} already exists.'.format(option_group_kwargs['name'])) if 'description' not in option_group_kwargs or not option_group_kwargs['description']: raise RDSClientError('InvalidParameterValue', 'The parameter OptionGroupDescription must be provided and must not be blank.') @@ -535,6 +537,19 @@ def describe_option_group_options(engine_name, major_engine_version=None): return default_option_group_options[engine_name][major_engine_version] return default_option_group_options[engine_name]['all'] + def modify_option_group(self, option_group_name, options_to_include=None, options_to_remove=None, apply_immediately=None): + if option_group_name not in self.option_groups: + raise RDSClientError('OptionGroupNotFoundFault', + 'Specified OptionGroupName: {} not found.'.format(option_group_name)) + if not options_to_include and not options_to_remove: + raise RDSClientError('InvalidParameterValue', + 'At least one option must be added, modified, or removed.') + if options_to_remove: + self.option_groups[option_group_name].remove_options(options_to_remove) + if options_to_include: + self.option_groups[option_group_name].add_options(options_to_include) + return ['a'] + class OptionGroup(object): def __init__(self, name, engine_name, major_engine_version, description=None): @@ -558,6 +573,12 @@ def to_json(self): }""") return template.render(option_group=self) + def remove_options(self, options_to_remove): + return + + def add_options(self, options_to_add): + return + class OptionGroupOption(object): def __init__(self, engine_name, major_engine_version): diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index 471946772ca3..35fccd714b48 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -185,6 +185,32 @@ def describe_option_group_options(self): option_group_options = self.backend.describe_option_group_options(engine_name, major_engine_version) return option_group_options + def modify_option_group(self): + option_group_name = self._get_param('OptionGroupName') + count = 1 + options_to_include = [] + while self._get_param('OptionsToInclude.member.{}.OptionName'.format(count)): + options_to_include.append({ + 'Port': self._get_param('OptionsToInclude.member.{}.Port'.format(count)), + 'OptionName': self._get_param('OptionsToInclude.member.{}.OptionName'.format(count)), + 'DBSecurityGroupMemberships': self._get_param('OptionsToInclude.member.{}.DBSecurityGroupMemberships'.format(count)), + 'OptionSettings': self._get_param('OptionsToInclude.member.{}.OptionSettings'.format(count)), + 'VpcSecurityGroupMemberships': self._get_param('OptionsToInclude.member.{}.VpcSecurityGroupMemberships'.format(count)) + }) + count += 1 + count = 1 + options_to_remove = [] + while self._get_param('OptionsToRemove.member.{}'.format(count)): + options_to_remove.append(self._get_param('OptionsToRemove.member.{}'.format(count))) + count += 1 + apply_immediately = self._get_param('ApplyImmediately') + result = self.backend.modify_option_group(option_group_name, + options_to_include, + options_to_remove, + apply_immediately) + return json.dumps('{"DescribeOptionGroupOptionsResponse": {"DescribeOptionGroupOptionsResult": {"Marker": null, "OptionGroupOptions": [{"MinimumRequiredMinorEngineVersion": "2789.0.v1", "OptionsDependedOn": [], "MajorEngineVersion": "10.50", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "sqlserver-ee", "Name": "Mirroring", "PortRequired": false, "Description": "SQLServer Database Mirroring"}, {"MinimumRequiredMinorEngineVersion": "2789.0.v1", "OptionsDependedOn": [], "MajorEngineVersion": "10.50", "Persistent": true, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "sqlserver-ee", "Name": "TDE", "PortRequired": false, "Description": "SQL Server - Transparent Data Encryption"}, {"MinimumRequiredMinorEngineVersion": "2100.60.v1", "OptionsDependedOn": [], "MajorEngineVersion": "11.00", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "sqlserver-ee", "Name": "Mirroring", "PortRequired": false, "Description": "SQLServer Database Mirroring"}, {"MinimumRequiredMinorEngineVersion": "2100.60.v1", "OptionsDependedOn": [], "MajorEngineVersion": "11.00", "Persistent": true, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "sqlserver-ee", "Name": "TDE", "PortRequired": false, "Description": "SQL Server - Transparent Data Encryption"}]}, "ResponseMetadata": {"RequestId": "c9f2fd9b-9fcb-11e4-8add-31b6fe33145f"}}}') + + CREATE_DATABASE_TEMPLATE = """{ "CreateDBInstanceResponse": { "CreateDBInstanceResult": { diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 4c565717838a..44c684721b0a 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -91,6 +91,13 @@ def test_create_option_group_empty_description(): conn.create_option_group.when.called_with('test', 'mysql', '5.6', '').should.throw(BotoServerError) +@mock_rds2 +def test_create_option_group_duplicate(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.create_option_group('test', 'mysql', '5.6', 'test option group') + conn.create_option_group.when.called_with('test', 'mysql', '5.6', 'foo').should.throw(BotoServerError) + + @mock_rds2 def test_describe_option_group(): conn = boto.rds2.connect_to_region("us-west-2") @@ -133,6 +140,26 @@ def test_describe_option_group_options(): conn.describe_option_group_options.when.called_with('non-existent').should.throw(BotoServerError) conn.describe_option_group_options.when.called_with('mysql', 'non-existent').should.throw(BotoServerError) + +@mock_rds2 +def test_modify_option_group(): + conn = boto.rds2.connect_to_region("us-west-2") + # if Someone can tell me how the hell to use this function I can finish coding this. + + +@mock_rds2 +def test_modify_option_group_no_options(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.create_option_group('test', 'mysql', '5.6', 'test option group') + conn.modify_option_group.when.called_with('test').should.throw(BotoServerError) + + +@mock_rds2 +def test_modify_non_existant_option_group(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.modify_option_group.when.called_with('non-existant', [('OptionName', 'Port', 'DBSecurityGroupMemberships', 'VpcSecurityGroupMemberships', 'OptionSettings')]).should.throw(BotoServerError) + + #@disable_on_py3() #@mock_rds2 #def test_delete_database(): From 6fad81aabfbdf65f3d463e9ca568ae95f94698d8 Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Wed, 21 Jan 2015 07:15:47 +1100 Subject: [PATCH 07/35] I have delete options for the modify_option_group method but for the life of me. I cannot work out how to use this function to add options to an option_group. --- moto/rds2/models.py | 2 +- moto/rds2/responses.py | 24 ++++++++++++++++++------ tests/test_rds2/test_rds2.py | 10 +++++++++- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 63d31324115f..6d9f7c1f51a6 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -548,7 +548,7 @@ def modify_option_group(self, option_group_name, options_to_include=None, option self.option_groups[option_group_name].remove_options(options_to_remove) if options_to_include: self.option_groups[option_group_name].add_options(options_to_include) - return ['a'] + return self.option_groups[option_group_name] class OptionGroup(object): diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index 35fccd714b48..f0ac52faa69f 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -198,17 +198,19 @@ def modify_option_group(self): 'VpcSecurityGroupMemberships': self._get_param('OptionsToInclude.member.{}.VpcSecurityGroupMemberships'.format(count)) }) count += 1 + count = 1 options_to_remove = [] while self._get_param('OptionsToRemove.member.{}'.format(count)): options_to_remove.append(self._get_param('OptionsToRemove.member.{}'.format(count))) count += 1 apply_immediately = self._get_param('ApplyImmediately') - result = self.backend.modify_option_group(option_group_name, - options_to_include, - options_to_remove, - apply_immediately) - return json.dumps('{"DescribeOptionGroupOptionsResponse": {"DescribeOptionGroupOptionsResult": {"Marker": null, "OptionGroupOptions": [{"MinimumRequiredMinorEngineVersion": "2789.0.v1", "OptionsDependedOn": [], "MajorEngineVersion": "10.50", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "sqlserver-ee", "Name": "Mirroring", "PortRequired": false, "Description": "SQLServer Database Mirroring"}, {"MinimumRequiredMinorEngineVersion": "2789.0.v1", "OptionsDependedOn": [], "MajorEngineVersion": "10.50", "Persistent": true, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "sqlserver-ee", "Name": "TDE", "PortRequired": false, "Description": "SQL Server - Transparent Data Encryption"}, {"MinimumRequiredMinorEngineVersion": "2100.60.v1", "OptionsDependedOn": [], "MajorEngineVersion": "11.00", "Persistent": false, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "sqlserver-ee", "Name": "Mirroring", "PortRequired": false, "Description": "SQLServer Database Mirroring"}, {"MinimumRequiredMinorEngineVersion": "2100.60.v1", "OptionsDependedOn": [], "MajorEngineVersion": "11.00", "Persistent": true, "DefaultPort": null, "Permanent": false, "OptionGroupOptionSettings": [], "EngineName": "sqlserver-ee", "Name": "TDE", "PortRequired": false, "Description": "SQL Server - Transparent Data Encryption"}]}, "ResponseMetadata": {"RequestId": "c9f2fd9b-9fcb-11e4-8add-31b6fe33145f"}}}') + option_group = self.backend.modify_option_group(option_group_name, + options_to_include, + options_to_remove, + apply_immediately) + template = self.response_template(MODIFY_OPTION_GROUP_TEMPLATE) + return template.render(option_group=option_group) CREATE_DATABASE_TEMPLATE = """{ @@ -362,4 +364,14 @@ def modify_option_group(self): {%- endfor -%} ]}, "ResponseMetadata": {"RequestId": "457f7bb8-9fbf-11e4-9084-5754f80d5144"} - }}""" \ No newline at end of file + }}""" + +MODIFY_OPTION_GROUP_TEMPLATE = \ + """{"ModifyOptionGroupResponse": { + "ResponseMetadata": { + "RequestId": "ce9284a5-a0de-11e4-b984-a11a53e1f328" + }, + "ModifyOptionGroupResult": + {{ option_group.to_json() }} + } + }""" diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 44c684721b0a..0178697892f5 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -73,6 +73,7 @@ def test_create_option_group(): option_group['CreateOptionGroupResponse']['CreateOptionGroupResult']['OptionGroup']['OptionGroupDescription'].should.equal('test option group') option_group['CreateOptionGroupResponse']['CreateOptionGroupResult']['OptionGroup']['MajorEngineVersion'].should.equal('5.6') + @mock_rds2 def test_create_option_group_bad_engine_name(): conn = boto.rds2.connect_to_region("us-west-2") @@ -144,7 +145,14 @@ def test_describe_option_group_options(): @mock_rds2 def test_modify_option_group(): conn = boto.rds2.connect_to_region("us-west-2") - # if Someone can tell me how the hell to use this function I can finish coding this. + conn.create_option_group('test', 'mysql', '5.6', 'test option group') + # if Someone can tell me how the hell to use this function + # to add options to an option_group, I can finish coding this. + result = conn.modify_option_group('test', [], ['MEMCACHED'], True) + result['ModifyOptionGroupResponse']['ModifyOptionGroupResult']['EngineName'].should.equal('mysql') + result['ModifyOptionGroupResponse']['ModifyOptionGroupResult']['Options'].should.equal([]) + result['ModifyOptionGroupResponse']['ModifyOptionGroupResult']['OptionGroupName'].should.equal('test') + @mock_rds2 From 4b838f6114ca0338cd7496d4b06e38adca794469 Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Wed, 21 Jan 2015 07:18:56 +1100 Subject: [PATCH 08/35] Added some TODOs to ensure I dont forget. --- moto/rds2/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 6d9f7c1f51a6..6aefd81f1726 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -574,9 +574,11 @@ def to_json(self): return template.render(option_group=self) def remove_options(self, options_to_remove): + # TODO: Check for option in self.options and remove if exists. Raise error otherwise return def add_options(self, options_to_add): + # TODO: Validate option and add it to self.options. If invalid raise error return @@ -584,6 +586,8 @@ class OptionGroupOption(object): def __init__(self, engine_name, major_engine_version): self.engine_name = engine_name self.major_engine_version = major_engine_version + #TODO: Create validation for Options + #TODO: formulate way to store options settings def to_json(self): template = Template("""{ "MinimumRequiredMinorEngineVersion": From 242e29a7c1685022aefe01d2192f7b7df6720148 Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Thu, 22 Jan 2015 07:21:49 +1100 Subject: [PATCH 09/35] rds2 modify_db_instance, small code tidy ups --- moto/rds2/models.py | 8 ++- moto/rds2/responses.py | 57 +++++++++-------- tests/test_rds2/test_rds2.py | 121 +++++++++++++++++++++++++---------- 3 files changed, 124 insertions(+), 62 deletions(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 6aefd81f1726..ad2f9b3f1b2c 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -88,12 +88,14 @@ def __init__(self, **kwargs): def address(self): return "{0}.aaaaaaaaaa.{1}.rds.amazonaws.com".format(self.db_instance_identifier, self.region) + # TODO: confirm how this should represent in the RESULT JSON def add_replica(self, replica): self.replicas.append(replica.db_instance_identifier) def remove_replica(self, replica): self.replicas.remove(replica.db_instance_identifier) + # TODO: confirm how this should represent in the RESULT JSON def set_as_replica(self): self.is_replica = True self.replicas = [] @@ -223,7 +225,11 @@ def to_json(self): "Endpoint": null, "InstanceCreateTime": null, "Iops": null, - "ReadReplicaDBInstanceIdentifiers": [], + "ReadReplicaDBInstanceIdentifiers": [{%- for replica in database.replicas -%} + {%- if not loop.first -%},{%- endif -%} + "{{ replica }}" + {%- endfor -%} + ], "ReadReplicaSourceDBInstanceIdentifier": null, "SecondaryAvailabilityZone": null, "StatusInfos": null, diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index f0ac52faa69f..918de9969e7e 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -90,16 +90,20 @@ def describe_dbinstances(self): template = self.response_template(DESCRIBE_DATABASES_TEMPLATE) return template.render(databases=databases) - # TODO: Update function to new method def modify_dbinstance(self): + return self.modify_db_instance() + + def modify_db_instance(self): db_instance_identifier = self._get_param('DBInstanceIdentifier') db_kwargs = self._get_db_kwargs() database = self.backend.modify_database(db_instance_identifier, db_kwargs) template = self.response_template(MODIFY_DATABASE_TEMPLATE) return template.render(database=database) - # TODO: Update function to new method def delete_dbinstance(self): + return self.delete_db_instance() + + def delete_db_instance(self): db_instance_identifier = self._get_param('DBInstanceIdentifier') database = self.backend.delete_database(db_instance_identifier) template = self.response_template(DELETE_DATABASE_TEMPLATE) @@ -222,14 +226,14 @@ def modify_option_group(self): } }""" -CREATE_DATABASE_REPLICA_TEMPLATE = """ - - {{ database.to_xml() }} - - - ba8dedf0-bb9a-11d3-855b-576787000e19 - -""" +CREATE_DATABASE_REPLICA_TEMPLATE = """{ + "CreateDBInstanceResponse": { + "CreateDBInstanceResult": { + {{ database.to_json() }} + }, + "ResponseMetadata": { "RequestId": "523e3218-afc7-11c3-90f5-f90431260ab4" } + } +}""" DESCRIBE_DATABASES_TEMPLATE = """{ "DescribeDBInstanceResponse": { @@ -243,23 +247,24 @@ def modify_option_group(self): } }""" -MODIFY_DATABASE_TEMPLATE = """ - - {{ database.to_xml() }} - - - f643f1ac-bbfe-11d3-f4c6-37db295f7674 - -""" +MODIFY_DATABASE_TEMPLATE = """{"ModifyDBInstanceResponse": { + "ModifyDBInstanceResult": { + {{ database.to_json() }}, + "ResponseMetadata": { + "RequestId": "bb58476c-a1a8-11e4-99cf-55e92d4bbada" + } + } + } +}""" -DELETE_DATABASE_TEMPLATE = """ - - {{ database.to_xml() }} - - - 7369556f-b70d-11c3-faca-6ba18376ea1b - -""" +# TODO: update delete DB TEMPLATE +DELETE_DATABASE_TEMPLATE = """{ + "DeleteDBInstanceResponse": { + "DeleteDBInstanceResult": { + }, + "ResponseMetadata": { "RequestId": "523e3218-afc7-11c3-90f5-f90431260ab4" } + } +}""" CREATE_SECURITY_GROUP_TEMPLATE = """ diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 0178697892f5..e2801c1c7882 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -58,12 +58,53 @@ def test_get_databases(): instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult'][0]['DBInstance']['DBInstanceIdentifier'].should.equal("db-master-1") +@disable_on_py3() @mock_rds2 def test_describe_non_existant_database(): conn = boto.rds2.connect_to_region("us-west-2") conn.describe_db_instances.when.called_with("not-a-db").should.throw(BotoServerError) +@disable_on_py3() +@mock_rds2 +def test_modify_db_instance(): + conn = boto.rds2.connect_to_region("us-west-2") + database = conn.create_db_instance(db_instance_identifier='db-master-1', + allocated_storage=10, + engine='postgres', + db_instance_class='db.m1.small', + master_username='root', + master_user_password='hunter2', + db_security_groups=["my_sg"]) + instances = conn.describe_db_instances('db-master-1') + instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult'][0]['DBInstance']['AllocatedStorage'].should.equal('10') + conn.modify_db_instance(db_instance_identifier='db-master-1', allocated_storage=20, apply_immediately=True) + instances = conn.describe_db_instances('db-master-1') + instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult'][0]['DBInstance']['AllocatedStorage'].should.equal('20') + + +@disable_on_py3() +@mock_rds2 +def test_delete_database(): + conn = boto.rds2.connect_to_region("us-west-2") + instances = conn.describe_db_instances() + list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(0) + conn.create_db_instance(db_instance_identifier='db-master-1', + allocated_storage=10, + engine='postgres', + db_instance_class='db.m1.small', + master_username='root', + master_user_password='hunter2', + db_security_groups=["my_sg"]) + instances = conn.describe_db_instances() + list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(1) + + conn.delete_db_instance("db-master-1") + instances = conn.describe_db_instances() + list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(0) + + +@disable_on_py3() @mock_rds2 def test_create_option_group(): conn = boto.rds2.connect_to_region("us-west-2") @@ -74,24 +115,28 @@ def test_create_option_group(): option_group['CreateOptionGroupResponse']['CreateOptionGroupResult']['OptionGroup']['MajorEngineVersion'].should.equal('5.6') +@disable_on_py3() @mock_rds2 def test_create_option_group_bad_engine_name(): conn = boto.rds2.connect_to_region("us-west-2") conn.create_option_group.when.called_with('test', 'invalid_engine', '5.6', 'test invalid engine').should.throw(BotoServerError) +@disable_on_py3() @mock_rds2 def test_create_option_group_bad_engine_major_version(): conn = boto.rds2.connect_to_region("us-west-2") conn.create_option_group.when.called_with('test', 'mysql', '6.6.6', 'test invalid engine version').should.throw(BotoServerError) +@disable_on_py3() @mock_rds2 def test_create_option_group_empty_description(): conn = boto.rds2.connect_to_region("us-west-2") conn.create_option_group.when.called_with('test', 'mysql', '5.6', '').should.throw(BotoServerError) +@disable_on_py3() @mock_rds2 def test_create_option_group_duplicate(): conn = boto.rds2.connect_to_region("us-west-2") @@ -99,6 +144,7 @@ def test_create_option_group_duplicate(): conn.create_option_group.when.called_with('test', 'mysql', '5.6', 'foo').should.throw(BotoServerError) +@disable_on_py3() @mock_rds2 def test_describe_option_group(): conn = boto.rds2.connect_to_region("us-west-2") @@ -107,12 +153,14 @@ def test_describe_option_group(): option_groups['DescribeOptionGroupsResponse']['DescribeOptionGroupsResult']['OptionGroupsList'][0]['OptionGroupName'].should.equal('test') +@disable_on_py3() @mock_rds2 def test_describe_non_existant_option_group(): conn = boto.rds2.connect_to_region("us-west-2") conn.describe_option_groups.when.called_with("not-a-option-group").should.throw(BotoServerError) +@disable_on_py3() @mock_rds2 def test_delete_option_group(): conn = boto.rds2.connect_to_region("us-west-2") @@ -123,6 +171,7 @@ def test_delete_option_group(): conn.describe_option_groups.when.called_with('test').should.throw(BotoServerError) +@disable_on_py3() @mock_rds2 def test_delete_non_existant_option_group(): conn = boto.rds2.connect_to_region("us-west-2") @@ -142,10 +191,12 @@ def test_describe_option_group_options(): conn.describe_option_group_options.when.called_with('mysql', 'non-existent').should.throw(BotoServerError) +@disable_on_py3() @mock_rds2 def test_modify_option_group(): conn = boto.rds2.connect_to_region("us-west-2") conn.create_option_group('test', 'mysql', '5.6', 'test option group') + # TODO: create option and validate before deleting. # if Someone can tell me how the hell to use this function # to add options to an option_group, I can finish coding this. result = conn.modify_option_group('test', [], ['MEMCACHED'], True) @@ -154,7 +205,7 @@ def test_modify_option_group(): result['ModifyOptionGroupResponse']['ModifyOptionGroupResult']['OptionGroupName'].should.equal('test') - +@disable_on_py3() @mock_rds2 def test_modify_option_group_no_options(): conn = boto.rds2.connect_to_region("us-west-2") @@ -162,31 +213,21 @@ def test_modify_option_group_no_options(): conn.modify_option_group.when.called_with('test').should.throw(BotoServerError) +@disable_on_py3() @mock_rds2 def test_modify_non_existant_option_group(): conn = boto.rds2.connect_to_region("us-west-2") conn.modify_option_group.when.called_with('non-existant', [('OptionName', 'Port', 'DBSecurityGroupMemberships', 'VpcSecurityGroupMemberships', 'OptionSettings')]).should.throw(BotoServerError) -#@disable_on_py3() -#@mock_rds2 -#def test_delete_database(): -# conn = boto.rds2.connect_to_region("us-west-2") -# list(conn.get_all_dbinstances()).should.have.length_of(0) -# -# conn.create_dbinstance("db-master-1", 10, 'db.m1.small', 'root', 'hunter2') -# list(conn.get_all_dbinstances()).should.have.length_of(1) -# -# conn.delete_dbinstance("db-master-1") -# list(conn.get_all_dbinstances()).should.have.length_of(0) -# -# -#@mock_rds2 -#def test_delete_non_existant_database(): -# conn = boto.rds2.connect_to_region("us-west-2") -# conn.delete_dbinstance.when.called_with("not-a-db").should.throw(BotoServerError) +@disable_on_py3() +@mock_rds2 +def test_delete_non_existant_database(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.delete_db_instance.when.called_with("not-a-db").should.throw(BotoServerError) +#@disable_on_py3() #@mock_rds2 #def test_create_database_security_group(): # conn = boto.rds2.connect_to_region("us-west-2") @@ -338,20 +379,30 @@ def test_modify_non_existant_option_group(): #def test_create_database_replica(): # conn = boto.rds2.connect_to_region("us-west-2") # -# primary = conn.create_dbinstance("db-master-1", 10, 'db.m1.small', 'root', 'hunter2') -# -# replica = conn.create_dbinstance_read_replica("replica", "db-master-1", "db.m1.small") -# replica.id.should.equal("replica") -# replica.instance_class.should.equal("db.m1.small") -# status_info = replica.status_infos[0] -# status_info.normal.should.equal(True) -# status_info.status_type.should.equal('read replication') -# status_info.status.should.equal('replicating') -# -# primary = conn.get_all_dbinstances("db-master-1")[0] -# primary.read_replica_dbinstance_identifiers[0].should.equal("replica") -# -# conn.delete_dbinstance("replica") -# -# primary = conn.get_all_dbinstances("db-master-1")[0] -# list(primary.read_replica_dbinstance_identifiers).should.have.length_of(0) +# conn.create_db_instance(db_instance_identifier='db-master-1', +# allocated_storage=10, +# engine='postgres', +# db_instance_class='db.m1.small', +# master_username='root', +# master_user_password='hunter2', +# db_security_groups=["my_sg"]) +# +# # TODO: confirm the RESULT JSON +# replica = conn.create_db_instance_read_replica("replica", "db-master-1", "db.m1.small") +# print replica + #replica.id.should.equal("replica") + #replica.instance_class.should.equal("db.m1.small") + #status_info = replica.status_infos[0] + #status_info.normal.should.equal(True) + #status_info.status_type.should.equal('read replication') + #status_info.status.should.equal('replicating') + + # TODO: formulate checks on read replica status +# primary = conn.describe_db_instances("db-master-1") +# print primary + #primary.read_replica_dbinstance_identifiers[0].should.equal("replica") + + #conn.delete_dbinstance("replica") + + #primary = conn.get_all_dbinstances("db-master-1")[0] + #list(primary.read_replica_dbinstance_identifiers).should.have.length_of(0) From 4f822f58e87a23b5e08c1ea09d5f7798ae2ee486 Mon Sep 17 00:00:00 2001 From: Chris Henry Date: Mon, 26 Jan 2015 13:33:11 -0500 Subject: [PATCH 10/35] Loosen the url pattern a bit. I was seeing boto send requests to rds.amazonaws.com --- moto/rds2/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/rds2/urls.py b/moto/rds2/urls.py index 3c8a129a88dd..6881a11191eb 100644 --- a/moto/rds2/urls.py +++ b/moto/rds2/urls.py @@ -2,7 +2,7 @@ from .responses import RDS2Response url_bases = [ - "https?://rds.(.+).amazonaws.com", + "https?://rds(.+).amazonaws.com", ] url_paths = { From 850b15d849f43918db49d0b848f74b4feec7e0f0 Mon Sep 17 00:00:00 2001 From: Chris Henry Date: Mon, 26 Jan 2015 13:33:46 -0500 Subject: [PATCH 11/35] Add a to_json method to SubnetGroup. --- moto/rds2/models.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index ad2f9b3f1b2c..846d070ab80a 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -339,6 +339,29 @@ def to_xml(self): """) return template.render(subnet_group=self) + def to_json(self): + template = Template("""{ + "DBSubnetGroup": { + "VpcId": "{{ subnet_group.vpc_id }}", + "SubnetGroupStatus": "{{ subnet_group.status }}", + "DBSubnetGroupDescription": "{{ subnet_group.description }}", + "DBSubnetGroupName": "{{ subnet_group.subnet_name }}", + "Subnets": { + "Subnet": [ + {% for subnet in subnet_group.subnets %}{ + "SubnetStatus": "Active", + "SubnetIdentifier": "{{ subnet.id }}", + "SubnetAvailabilityZone": { + "Name": "{{ subnet.availability_zone }}", + "ProvisionedIopsCapable": "false" + } + }{%- if not loop.last -%},{%- endif -%}{% endfor %} + ] + } + } + }""") + return template.render(subnet_group=self) + @classmethod def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): properties = cloudformation_json['Properties'] From 25d0c0926d9cb34bd9a19cc59414ddbd7ac26bb8 Mon Sep 17 00:00:00 2001 From: Chris Henry Date: Mon, 26 Jan 2015 13:34:21 -0500 Subject: [PATCH 12/35] Change CREATE_SUBNET_GROUP_TEMPLATE to json. --- moto/rds2/responses.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index 918de9969e7e..91f4c381ee09 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -303,14 +303,13 @@ def modify_option_group(self): """ -CREATE_SUBNET_GROUP_TEMPLATE = """ - - {{ subnet_group.to_xml() }} - - - 3a401b3f-bb9e-11d3-f4c6-37db295f7674 - -""" +CREATE_SUBNET_GROUP_TEMPLATE = """{ + "CreateDBSubnetGroupResponse": { + "CreateDBSubnetGroupResult": + {{ subnet_group.to_json() }}, + "ResponseMetadata": { "RequestId": "3a401b3f-bb9e-11d3-f4c6-37db295f7674" } + } +}""" DESCRIBE_SUBNET_GROUPS_TEMPLATE = """ From 2352e27c3ec58e62b1c5b32bfb3c75a849aca586 Mon Sep 17 00:00:00 2001 From: Chris Henry Date: Mon, 26 Jan 2015 13:35:28 -0500 Subject: [PATCH 13/35] Remove todo. --- moto/rds2/responses.py | 1 - 1 file changed, 1 deletion(-) diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index 91f4c381ee09..b6325b32131e 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -139,7 +139,6 @@ def authorize_dbsecurity_group_ingress(self): template = self.response_template(AUTHORIZE_SECURITY_GROUP_TEMPLATE) return template.render(security_group=security_group) - # TODO: Update function to new method def create_dbsubnet_group(self): subnet_name = self._get_param('DBSubnetGroupName') description = self._get_param('DBSubnetGroupDescription') From 1ccf1191cdf865d050f261952c6581010c1a9f5e Mon Sep 17 00:00:00 2001 From: Chris Henry Date: Mon, 26 Jan 2015 13:54:40 -0500 Subject: [PATCH 14/35] Instead of a regex, add a second url pattern --- moto/rds2/urls.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/moto/rds2/urls.py b/moto/rds2/urls.py index 6881a11191eb..d21084766e77 100644 --- a/moto/rds2/urls.py +++ b/moto/rds2/urls.py @@ -2,7 +2,8 @@ from .responses import RDS2Response url_bases = [ - "https?://rds(.+).amazonaws.com", + "https?://rds.(.+).amazonaws.com", + "https?://rds.amazonaws.com", ] url_paths = { From bba08f05b1607ce1fd6ae40021367ac63233f285 Mon Sep 17 00:00:00 2001 From: Chris Henry Date: Mon, 26 Jan 2015 15:38:56 -0500 Subject: [PATCH 15/35] Change the describe subnet groups over to json. --- moto/rds2/responses.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index b6325b32131e..f013be0f4fc5 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -148,7 +148,6 @@ def create_dbsubnet_group(self): template = self.response_template(CREATE_SUBNET_GROUP_TEMPLATE) return template.render(subnet_group=subnet_group) - # TODO: Update function to new method def describe_dbsubnet_groups(self): subnet_name = self._get_param('DBSubnetGroupName') subnet_groups = self.backend.describe_subnet_groups(subnet_name) @@ -310,18 +309,20 @@ def modify_option_group(self): } }""" -DESCRIBE_SUBNET_GROUPS_TEMPLATE = """ - - - {% for subnet_group in subnet_groups %} - {{ subnet_group.to_xml() }} - {% endfor %} - - - - b783db3b-b98c-11d3-fbc7-5c0aad74da7c - -""" +DESCRIBE_SUBNET_GROUPS_TEMPLATE = """{ + "DescribeDBSubnetGroupsResponse": { + "DescribeDBSubnetGroupsResult": { + "DBSubnetGroups": [ + {% for subnet_group in subnet_groups %} + {{ subnet_group.to_json() }}{%- if not loop.last -%},{%- endif -%} + {% endfor %} + ], + "Marker": null + }, + "ResponseMetadata": { "RequestId": "b783db3b-b98c-11d3-fbc7-5c0aad74da7c" } + } +}""" + DELETE_SUBNET_GROUP_TEMPLATE = """ From bcf4e97752d882d2749d66559f1959c3bc4c0b84 Mon Sep 17 00:00:00 2001 From: Chris Henry Date: Mon, 26 Jan 2015 15:39:34 -0500 Subject: [PATCH 16/35] Uncomment existing subnet groups test and add more assertions. --- tests/test_rds2/test_rds2.py | 40 ++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index e2801c1c7882..40e3d24b2856 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -322,21 +322,31 @@ def test_delete_non_existant_database(): # list(subnet_group.subnet_ids).should.equal(subnet_ids) # # -#@mock_ec2 -#@mock_rds2 -#def test_describe_database_subnet_group(): -# vpc_conn = boto.vpc.connect_to_region("us-west-2") -# vpc = vpc_conn.create_vpc("10.0.0.0/16") -# subnet = vpc_conn.create_subnet(vpc.id, "10.1.0.0/24") -# -# conn = boto.rds2.connect_to_region("us-west-2") -# conn.create_db_subnet_group("db_subnet1", "my db subnet", [subnet.id]) -# conn.create_db_subnet_group("db_subnet2", "my db subnet", [subnet.id]) -# -# list(conn.get_all_db_subnet_groups()).should.have.length_of(2) -# list(conn.get_all_db_subnet_groups("db_subnet1")).should.have.length_of(1) -# -# conn.get_all_db_subnet_groups.when.called_with("not-a-subnet").should.throw(BotoServerError) +@mock_ec2 +@mock_rds2 +def test_describe_database_subnet_group(): + vpc_conn = boto.vpc.connect_to_region("us-west-2") + vpc = vpc_conn.create_vpc("10.0.0.0/16") + subnet = vpc_conn.create_subnet(vpc.id, "10.1.0.0/24") + + conn = boto.rds2.connect_to_region("us-west-2") + conn.create_db_subnet_group("db_subnet1", "my db subnet", [subnet.id]) + conn.create_db_subnet_group("db_subnet2", "my db subnet", [subnet.id]) + + resp = conn.describe_db_subnet_groups() + groups_resp = resp['DescribeDBSubnetGroupsResponse'] + + subnet_groups = groups_resp['DescribeDBSubnetGroupsResult']['DBSubnetGroups'] + subnet_groups.should.have.length_of(2) + + subnets = groups_resp['DescribeDBSubnetGroupsResult']['DBSubnetGroups'][0]['DBSubnetGroup']['Subnets'] + subnets.should.have.length_of(1) + + list(resp).should.have.length_of(1) + list(groups_resp).should.have.length_of(2) + list(conn.describe_db_subnet_groups("db_subnet1")).should.have.length_of(1) + + conn.describe_db_subnet_groups.when.called_with("not-a-subnet").should.throw(BotoServerError) # # #@mock_ec2 From 6232abfe2d57797bfbd6e1f2cf8efff12ece41b3 Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Tue, 27 Jan 2015 08:21:48 +1100 Subject: [PATCH 17/35] Added reboot instance and list_tags_for_resource. Still need to get the tags populated. --- README.md | 5 +++ moto/rds2/models.py | 23 ++++++++++++-- moto/rds2/responses.py | 59 ++++++++++++++++++++++++++++++----- tests/test_rds2/test_rds2.py | 60 ++++++++++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index dc582b608226..a20e3f379577 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,11 @@ It gets even better! Moto isn't just S3. Here's the status of the other AWS serv |------------------------------------------------------------------------------| | RDS | @mock_rds | core endpoints done | |------------------------------------------------------------------------------| +| RDS2 | @mock_rds2 | core endpoints done | +| - Database | | core endpoints done | +| - Security Group | | not done | +| - Option Group | | core endpoints done | +|------------------------------------------------------------------------------| | Route53 | @mock_route53 | core endpoints done | |------------------------------------------------------------------------------| | S3 | @mock_s3 | core endpoints done | diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 846d070ab80a..228593dcccb5 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -5,7 +5,7 @@ import boto.rds2 import json from jinja2 import Template - +from re import compile as re_compile from moto.cloudformation.exceptions import UnformattedGetAttTemplateException from moto.core import BaseBackend from moto.core.utils import get_random_hex @@ -82,7 +82,7 @@ def __init__(self, **kwargs): if not self.option_group_name and self.engine in self.default_option_groups: self.option_group_name = self.default_option_groups[self.engine] self.character_set_name = kwargs.get('character_set_name', None) - self.tags = kwargs.get('tags', None) + self.tags = kwargs.get('tags', []) @property def address(self): @@ -242,6 +242,9 @@ def to_json(self): }""") return template.render(database=self) + def get_tags(self): + return self.tags + class SecurityGroup(object): def __init__(self, group_name, description): @@ -384,6 +387,7 @@ def create_from_cloudformation_json(cls, resource_name, cloudformation_json, reg class RDS2Backend(BaseBackend): def __init__(self): + self.arn_regex = re_compile(r'^arn:aws:rds:.*:[0-9]*:db:.*$') self.databases = {} self.security_groups = {} self.subnet_groups = {} @@ -419,6 +423,10 @@ def modify_database(self, db_instance_identifier, db_kwargs): database.update(db_kwargs) return database + def reboot_db_instance(self, db_instance_identifier): + database = self.describe_databases(db_instance_identifier)[0] + return database + def delete_database(self, db_instance_identifier): if db_instance_identifier in self.databases: database = self.databases.pop(db_instance_identifier) @@ -579,6 +587,17 @@ def modify_option_group(self, option_group_name, options_to_include=None, option self.option_groups[option_group_name].add_options(options_to_include) return self.option_groups[option_group_name] + def list_tags_for_resource(self, arn): + if self.arn_regex.match(arn): + arn_breakdown = arn.split(':') + db_instance_name = arn_breakdown[len(arn_breakdown)-1] + if db_instance_name in self.databases: + return self.databases[db_instance_name].get_tags() + else: + return [] + else: + raise RDSClientError('InvalidParameterValue', + 'Invalid resource name: {}'.format(arn)) class OptionGroup(object): def __init__(self, name, engine_name, major_engine_version, description=None): diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index f013be0f4fc5..8b483844f466 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -4,6 +4,7 @@ from moto.ec2.models import ec2_backends from .models import rds2_backends import json +import re class RDS2Response(BaseResponse): @@ -73,8 +74,10 @@ def create_db_instance(self): result = template.render(database=database) return result - # TODO: Update function to new method def create_dbinstance_read_replica(self): + return self.create_db_instance_read_replica() + + def create_db_instance_read_replica(self): db_kwargs = self._get_db_replica_kwargs() database = self.backend.create_database_replica(db_kwargs) @@ -84,7 +87,7 @@ def create_dbinstance_read_replica(self): def describe_dbinstances(self): return self.describe_db_instances() - def describe_dbinstances(self): + def describe_db_instances(self): db_instance_identifier = self._get_param('DBInstanceIdentifier') databases = self.backend.describe_databases(db_instance_identifier) template = self.response_template(DESCRIBE_DATABASES_TEMPLATE) @@ -109,6 +112,21 @@ def delete_db_instance(self): template = self.response_template(DELETE_DATABASE_TEMPLATE) return template.render(database=database) + def reboot_dbinstance(self): + return self.reboot_db_instance() + + def reboot_db_instance(self): + db_instance_identifier = self._get_param('DBInstanceIdentifier') + database = self.backend.reboot_db_instance(db_instance_identifier) + template = self.response_template(REBOOT_DATABASE_TEMPLATE) + return template.render(database=database) + + def list_tags_for_resource(self): + arn = self._get_param('ResourceName') + template = self.response_template(LIST_TAGS_FOR_RESOURCE_TEMPLATE) + tags = self.backend.list_tags_for_resource(arn) + return template.render(tags=tags) + # TODO: Update function to new method def create_dbsecurity_group(self): group_name = self._get_param('DBSecurityGroupName') @@ -255,14 +273,24 @@ def modify_option_group(self): } }""" +REBOOT_DATABASE_TEMPLATE = """{"RebootDBInstanceResponse": { + "RebootDBInstanceResult": { + {{ database.to_json() }}, + "ResponseMetadata": { + "RequestId": "d55711cb-a1ab-11e4-99cf-55e92d4bbada" + } + } +}}""" + # TODO: update delete DB TEMPLATE -DELETE_DATABASE_TEMPLATE = """{ - "DeleteDBInstanceResponse": { +DELETE_DATABASE_TEMPLATE = """{ "DeleteDBInstanceResponse": { "DeleteDBInstanceResult": { - }, - "ResponseMetadata": { "RequestId": "523e3218-afc7-11c3-90f5-f90431260ab4" } + {{ database.to_json() }}, + "ResponseMetadata": { + "RequestId": "523e3218-afc7-11c3-90f5-f90431260ab4" + } } -}""" +}}""" CREATE_SECURITY_GROUP_TEMPLATE = """ @@ -379,3 +407,20 @@ def modify_option_group(self): {{ option_group.to_json() }} } }""" + +LIST_TAGS_FOR_RESOURCE_TEMPLATE = \ + """{"ListTagsForResourceResponse": + {"ListTagsForResourceResult": + {"TagList": [ + {%- for tag in tags -%} + {%- if loop.index != 1 -%},{%- endif -%} + {%- for key in tag -%} + {"Value": "{{ tag[key] }}", "Key": "{{ key }}"} + {%- endfor -%} + {%- endfor -%} + ]}, + "ResponseMetadata": { + "RequestId": "8c21ba39-a598-11e4-b688-194eaf8658fa" + } + } + }""" diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 40e3d24b2856..8fa6c91d0405 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -83,6 +83,36 @@ def test_modify_db_instance(): instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult'][0]['DBInstance']['AllocatedStorage'].should.equal('20') +@disable_on_py3() +@mock_rds2 +def test_modify_non_existant_database(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.modify_db_instance.when.called_with(db_instance_identifier='not-a-db', + allocated_storage=20, + apply_immediately=True).should.throw(BotoServerError) + +@disable_on_py3() +@mock_rds2 +def test_reboot_db_instance(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.create_db_instance(db_instance_identifier='db-master-1', + allocated_storage=10, + engine='postgres', + db_instance_class='db.m1.small', + master_username='root', + master_user_password='hunter2', + db_security_groups=["my_sg"]) + database = conn.reboot_db_instance('db-master-1') + database['RebootDBInstanceResponse']['RebootDBInstanceResult']['DBInstance']['DBInstanceIdentifier'].should.equal("db-master-1") + + +@disable_on_py3() +@mock_rds2 +def test_reboot_non_existant_database(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.reboot_db_instance.when.called_with("not-a-db").should.throw(BotoServerError) + + @disable_on_py3() @mock_rds2 def test_delete_database(): @@ -104,6 +134,13 @@ def test_delete_database(): list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(0) +@disable_on_py3() +@mock_rds2 +def test_delete_non_existant_database(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.delete_db_instance.when.called_with("not-a-db").should.throw(BotoServerError) + + @disable_on_py3() @mock_rds2 def test_create_option_group(): @@ -227,6 +264,29 @@ def test_delete_non_existant_database(): conn.delete_db_instance.when.called_with("not-a-db").should.throw(BotoServerError) +@disable_on_py3() +@mock_rds2 +def test_list_tags_invalid_arn(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.list_tags_for_resource.when.called_with('arn:aws:rds:bad-arn').should.throw(BotoServerError) + + +@disable_on_py3() +@mock_rds2 +def test_list_tags(): + conn = boto.rds2.connect_to_region("us-west-2") + result = conn.list_tags_for_resource('arn:aws:rds:us-west-2:1234567890:db:foo') + result['ListTagsForResourceResponse']['ListTagsForResourceResult']['TagList'].should.equal([]) + conn.create_db_instance(db_instance_identifier='db-master-1', + allocated_storage=10, + engine='postgres', + db_instance_class='db.m1.small', + master_username='root', + master_user_password='hunter2', + db_security_groups=["my_sg"], + tags=[{'Key': 'foo', 'Value': 'bar'}]) + result = conn.list_tags_for_resource('arn:aws:rds:us-west-2:1234567890:db:db-master-1') + #@disable_on_py3() #@mock_rds2 #def test_create_database_security_group(): From 2dde94c9be9d076a675c33e32e138bb9f6955e18 Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Tue, 27 Jan 2015 09:04:39 +1100 Subject: [PATCH 18/35] Storing tags on create db instance and getting tags back in list_tags_for_resource --- moto/rds2/responses.py | 18 ++++++++++++++---- tests/test_rds2/test_rds2.py | 10 +++++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index 8b483844f466..54a13202776b 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -14,7 +14,7 @@ def backend(self): return rds2_backends[self.region] def _get_db_kwargs(self): - return { + args = { "auto_minor_version_upgrade": self._get_param('AutoMinorVersionUpgrade'), "allocated_storage": self._get_int_param('AllocatedStorage'), "availability_zone": self._get_param("AvailabilityZone"), @@ -39,7 +39,16 @@ def _get_db_kwargs(self): "security_groups": self._get_multi_param('DBSecurityGroups.member'), "storage_type": self._get_param("StorageType"), # VpcSecurityGroupIds.member.N + "tags": [] } + count = 1 + while self._get_param('Tags.member.{}.Key'.format(count)): + args["tags"].append({ + "Key": self._get_param('Tags.member.{}.Key'.format(count)), + "Value": self._get_param('Tags.member.{}.Value'.format(count)) + }) + count += 1 + return args def _get_db_replica_kwargs(self): return { @@ -414,9 +423,10 @@ def modify_option_group(self): {"TagList": [ {%- for tag in tags -%} {%- if loop.index != 1 -%},{%- endif -%} - {%- for key in tag -%} - {"Value": "{{ tag[key] }}", "Key": "{{ key }}"} - {%- endfor -%} + { + "Key": "{{ tag['Key'] }}", + "Value": "{{ tag['Value'] }}" + } {%- endfor -%} ]}, "ResponseMetadata": { diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 8fa6c91d0405..0b72b8ed7cc6 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -277,15 +277,19 @@ def test_list_tags(): conn = boto.rds2.connect_to_region("us-west-2") result = conn.list_tags_for_resource('arn:aws:rds:us-west-2:1234567890:db:foo') result['ListTagsForResourceResponse']['ListTagsForResourceResult']['TagList'].should.equal([]) - conn.create_db_instance(db_instance_identifier='db-master-1', + conn.create_db_instance(db_instance_identifier='db-with-tags', allocated_storage=10, engine='postgres', db_instance_class='db.m1.small', master_username='root', master_user_password='hunter2', db_security_groups=["my_sg"], - tags=[{'Key': 'foo', 'Value': 'bar'}]) - result = conn.list_tags_for_resource('arn:aws:rds:us-west-2:1234567890:db:db-master-1') + tags=[('foo', 'bar'), ('foo1', 'bar1')]) + result = conn.list_tags_for_resource('arn:aws:rds:us-west-2:1234567890:db:db-with-tags') + result['ListTagsForResourceResponse']['ListTagsForResourceResult']['TagList'].should.equal([{'Value': 'bar', + 'Key': 'foo'}, + {'Value': 'bar1', + 'Key': 'foo1'}]) #@disable_on_py3() #@mock_rds2 From df036fe207ad38f1821b56905c14836e7c375176 Mon Sep 17 00:00:00 2001 From: Chris Henry Date: Tue, 27 Jan 2015 01:28:38 -0500 Subject: [PATCH 19/35] rds2 model fixes. * Add the endpoint data to the to_json function. * Remove the DBInstance key from the to_json template. --- moto/rds2/models.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 228593dcccb5..63f0c07aff0f 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -158,7 +158,7 @@ def create_from_cloudformation_json(cls, resource_name, cloudformation_json, reg return database def to_json(self): - template = Template(""""DBInstance": { + template = Template("""{ "AllocatedStorage": 10, "AutoMinorVersionUpgrade": "{{ database.auto_minor_version_upgrade }}", "AvailabilityZone": "{{ database.availability_zone }}", @@ -222,7 +222,10 @@ def to_json(self): "PreferredMaintenanceWindow": "{{ database.preferred_maintenance_window }}", "PubliclyAccessible": "{{ database.publicly_accessible }}", "AllocatedStorage": "{{ database.allocated_storage }}", - "Endpoint": null, + "Endpoint": { + "Address": "{{ database.address }}", + "Port": "{{ database.port }}" + }, "InstanceCreateTime": null, "Iops": null, "ReadReplicaDBInstanceIdentifiers": [{%- for replica in database.replicas -%} From f78d3b79dfc33a87c94e4e55f044e18fcd0586d5 Mon Sep 17 00:00:00 2001 From: Chris Henry Date: Tue, 27 Jan 2015 01:53:42 -0500 Subject: [PATCH 20/35] Fix json templates to include DBInstance element. --- moto/rds2/responses.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index 54a13202776b..eb75ba1f4535 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -80,8 +80,7 @@ def create_db_instance(self): db_kwargs = self._get_db_kwargs() database = self.backend.create_database(db_kwargs) template = self.response_template(CREATE_DATABASE_TEMPLATE) - result = template.render(database=database) - return result + return template.render(database=database) def create_dbinstance_read_replica(self): return self.create_db_instance_read_replica() @@ -245,7 +244,7 @@ def modify_option_group(self): CREATE_DATABASE_TEMPLATE = """{ "CreateDBInstanceResponse": { "CreateDBInstanceResult": { - {{ database.to_json() }} + "DBInstance": {{ database.to_json() }} }, "ResponseMetadata": { "RequestId": "523e3218-afc7-11c3-90f5-f90431260ab4" } } @@ -261,20 +260,22 @@ def modify_option_group(self): }""" DESCRIBE_DATABASES_TEMPLATE = """{ - "DescribeDBInstanceResponse": { - "DescribeDBInstanceResult": [ - {%- for database in databases -%} - {%- if loop.index != 1 -%},{%- endif -%} - { {{ database.to_json() }} } - {%- endfor -%} - ], + "DescribeDBInstancesResponse": { + "DescribeDBInstancesResult": { + "DBInstances": [ + {%- for database in databases -%} + {%- if loop.index != 1 -%},{%- endif -%} + {{ database.to_json() }} + {%- endfor -%} + ] + }, "ResponseMetadata": { "RequestId": "523e3218-afc7-11c3-90f5-f90431260ab4" } } }""" MODIFY_DATABASE_TEMPLATE = """{"ModifyDBInstanceResponse": { "ModifyDBInstanceResult": { - {{ database.to_json() }}, + "DBInstance": {{ database.to_json() }}, "ResponseMetadata": { "RequestId": "bb58476c-a1a8-11e4-99cf-55e92d4bbada" } @@ -284,22 +285,24 @@ def modify_option_group(self): REBOOT_DATABASE_TEMPLATE = """{"RebootDBInstanceResponse": { "RebootDBInstanceResult": { - {{ database.to_json() }}, - "ResponseMetadata": { - "RequestId": "d55711cb-a1ab-11e4-99cf-55e92d4bbada" + "DBInstance": {{ database.to_json() }}, + "ResponseMetadata": { + "RequestId": "d55711cb-a1ab-11e4-99cf-55e92d4bbada" + } } } -}}""" +}""" # TODO: update delete DB TEMPLATE DELETE_DATABASE_TEMPLATE = """{ "DeleteDBInstanceResponse": { "DeleteDBInstanceResult": { - {{ database.to_json() }}, + "DBInstance": {{ database.to_json() }} + }, "ResponseMetadata": { "RequestId": "523e3218-afc7-11c3-90f5-f90431260ab4" } } -}}""" +}""" CREATE_SECURITY_GROUP_TEMPLATE = """ From 7766588681f52c412e64f926969850181056d4ec Mon Sep 17 00:00:00 2001 From: Chris Henry Date: Tue, 27 Jan 2015 01:53:57 -0500 Subject: [PATCH 21/35] Tweak tests. --- Makefile | 2 +- tests/test_rds2/test_rds2.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 7521a6d88f46..e624689fc306 100644 --- a/Makefile +++ b/Makefile @@ -6,5 +6,5 @@ init: test: rm -f .coverage - @nosetests -sv --with-coverage ./tests/ + @nosetests -sv --nocapture ./tests/ diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 0b72b8ed7cc6..7e50d1edfec3 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -34,7 +34,7 @@ def test_get_databases(): conn = boto.rds2.connect_to_region("us-west-2") instances = conn.describe_db_instances() - list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(0) + list(instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances']).should.have.length_of(0) conn.create_db_instance(db_instance_identifier='db-master-1', allocated_storage=10, @@ -51,11 +51,11 @@ def test_get_databases(): master_user_password='hunter2', db_security_groups=["my_sg"]) instances = conn.describe_db_instances() - list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(2) + list(instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances']).should.have.length_of(2) instances = conn.describe_db_instances("db-master-1") - list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(1) - instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult'][0]['DBInstance']['DBInstanceIdentifier'].should.equal("db-master-1") + list(instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances']).should.have.length_of(1) + instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances'][0]['DBInstanceIdentifier'].should.equal("db-master-1") @disable_on_py3() @@ -77,10 +77,10 @@ def test_modify_db_instance(): master_user_password='hunter2', db_security_groups=["my_sg"]) instances = conn.describe_db_instances('db-master-1') - instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult'][0]['DBInstance']['AllocatedStorage'].should.equal('10') + instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances'][0]['AllocatedStorage'].should.equal('10') conn.modify_db_instance(db_instance_identifier='db-master-1', allocated_storage=20, apply_immediately=True) instances = conn.describe_db_instances('db-master-1') - instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult'][0]['DBInstance']['AllocatedStorage'].should.equal('20') + instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances'][0]['AllocatedStorage'].should.equal('20') @disable_on_py3() @@ -118,7 +118,7 @@ def test_reboot_non_existant_database(): def test_delete_database(): conn = boto.rds2.connect_to_region("us-west-2") instances = conn.describe_db_instances() - list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(0) + list(instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances']).should.have.length_of(0) conn.create_db_instance(db_instance_identifier='db-master-1', allocated_storage=10, engine='postgres', @@ -127,11 +127,11 @@ def test_delete_database(): master_user_password='hunter2', db_security_groups=["my_sg"]) instances = conn.describe_db_instances() - list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(1) + list(instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances']).should.have.length_of(1) conn.delete_db_instance("db-master-1") instances = conn.describe_db_instances() - list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(0) + list(instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances']).should.have.length_of(0) @disable_on_py3() From 9e2a57732401b1bf31312a15a5734bfb026b9a1d Mon Sep 17 00:00:00 2001 From: Chris Henry Date: Tue, 27 Jan 2015 01:53:57 -0500 Subject: [PATCH 22/35] Tweak tests. --- tests/test_rds2/test_rds2.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 0b72b8ed7cc6..7e50d1edfec3 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -34,7 +34,7 @@ def test_get_databases(): conn = boto.rds2.connect_to_region("us-west-2") instances = conn.describe_db_instances() - list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(0) + list(instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances']).should.have.length_of(0) conn.create_db_instance(db_instance_identifier='db-master-1', allocated_storage=10, @@ -51,11 +51,11 @@ def test_get_databases(): master_user_password='hunter2', db_security_groups=["my_sg"]) instances = conn.describe_db_instances() - list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(2) + list(instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances']).should.have.length_of(2) instances = conn.describe_db_instances("db-master-1") - list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(1) - instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult'][0]['DBInstance']['DBInstanceIdentifier'].should.equal("db-master-1") + list(instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances']).should.have.length_of(1) + instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances'][0]['DBInstanceIdentifier'].should.equal("db-master-1") @disable_on_py3() @@ -77,10 +77,10 @@ def test_modify_db_instance(): master_user_password='hunter2', db_security_groups=["my_sg"]) instances = conn.describe_db_instances('db-master-1') - instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult'][0]['DBInstance']['AllocatedStorage'].should.equal('10') + instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances'][0]['AllocatedStorage'].should.equal('10') conn.modify_db_instance(db_instance_identifier='db-master-1', allocated_storage=20, apply_immediately=True) instances = conn.describe_db_instances('db-master-1') - instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult'][0]['DBInstance']['AllocatedStorage'].should.equal('20') + instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances'][0]['AllocatedStorage'].should.equal('20') @disable_on_py3() @@ -118,7 +118,7 @@ def test_reboot_non_existant_database(): def test_delete_database(): conn = boto.rds2.connect_to_region("us-west-2") instances = conn.describe_db_instances() - list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(0) + list(instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances']).should.have.length_of(0) conn.create_db_instance(db_instance_identifier='db-master-1', allocated_storage=10, engine='postgres', @@ -127,11 +127,11 @@ def test_delete_database(): master_user_password='hunter2', db_security_groups=["my_sg"]) instances = conn.describe_db_instances() - list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(1) + list(instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances']).should.have.length_of(1) conn.delete_db_instance("db-master-1") instances = conn.describe_db_instances() - list(instances['DescribeDBInstanceResponse']['DescribeDBInstanceResult']).should.have.length_of(0) + list(instances['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances']).should.have.length_of(0) @disable_on_py3() From 8f463325690fb945ffb88437eced47f59b113208 Mon Sep 17 00:00:00 2001 From: Chris Henry Date: Tue, 27 Jan 2015 09:29:51 -0500 Subject: [PATCH 23/35] Change Makefile back to original. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e624689fc306..7521a6d88f46 100644 --- a/Makefile +++ b/Makefile @@ -6,5 +6,5 @@ init: test: rm -f .coverage - @nosetests -sv --nocapture ./tests/ + @nosetests -sv --with-coverage ./tests/ From 10c0ffc2f2709f475e3fae4b1b8a03d0603ca3b5 Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Wed, 28 Jan 2015 10:17:55 +1100 Subject: [PATCH 24/35] Added remove_tags_from_resource for rds db and add_tags_to_resource for rds db. --- moto/rds2/models.py | 30 +++++++++++++++++++++ moto/rds2/responses.py | 51 ++++++++++++++++++++++++++++++------ tests/test_rds2/test_rds2.py | 40 ++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 8 deletions(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 63f0c07aff0f..74f5efef7f11 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -248,6 +248,14 @@ def to_json(self): def get_tags(self): return self.tags + def add_tags(self, tags): + new_keys = [tag_set['Key'] for tag_set in tags] + self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in new_keys] + self.tags.extend(tags) + + def remove_tags(self, tag_keys): + self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in tag_keys] + class SecurityGroup(object): def __init__(self, group_name, description): @@ -602,6 +610,28 @@ def list_tags_for_resource(self, arn): raise RDSClientError('InvalidParameterValue', 'Invalid resource name: {}'.format(arn)) + def remove_tags_from_resource(self, arn, tag_keys): + if self.arn_regex.match(arn): + arn_breakdown = arn.split(':') + db_instance_name = arn_breakdown[len(arn_breakdown)-1] + if db_instance_name in self.databases: + self.databases[db_instance_name].remove_tags(tag_keys) + else: + raise RDSClientError('InvalidParameterValue', + 'Invalid resource name: {}'.format(arn)) + + def add_tags_to_resource(self, arn, tags): + if self.arn_regex.match(arn): + arn_breakdown = arn.split(':') + db_instance_name = arn_breakdown[len(arn_breakdown)-1] + if db_instance_name in self.databases: + return self.databases[db_instance_name].add_tags(tags) + else: + return [] + else: + raise RDSClientError('InvalidParameterValue', + 'Invalid resource name: {}'.format(arn)) + class OptionGroup(object): def __init__(self, name, engine_name, major_engine_version, description=None): self.engine_name = engine_name diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index eb75ba1f4535..bade4a4f3417 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -39,15 +39,9 @@ def _get_db_kwargs(self): "security_groups": self._get_multi_param('DBSecurityGroups.member'), "storage_type": self._get_param("StorageType"), # VpcSecurityGroupIds.member.N - "tags": [] + "tags": list() } - count = 1 - while self._get_param('Tags.member.{}.Key'.format(count)): - args["tags"].append({ - "Key": self._get_param('Tags.member.{}.Key'.format(count)), - "Value": self._get_param('Tags.member.{}.Value'.format(count)) - }) - count += 1 + args['tags'] = self.unpack_complex_list_params('Tags.member', ('Key', 'Value')) return args def _get_db_replica_kwargs(self): @@ -73,6 +67,25 @@ def _get_option_group_kwargs(self): 'name': self._get_param('OptionGroupName') } + def unpack_complex_list_params(self, label, names): + unpacked_list = list() + count = 1 + while self._get_param('{0}.{1}.{2}'.format(label, count, names[0])): + param = dict() + for i in range(len(names)): + param[names[i]] = self._get_param('{0}.{1}.{2}'.format(label, count, names[i])) + unpacked_list.append(param) + count += 1 + return unpacked_list + + def unpack_list_params(self, label): + unpacked_list = list() + count = 1 + while self._get_param('{0}.{1}'.format(label, count)): + unpacked_list.append(self._get_param('{0}.{1}'.format(label, count))) + count += 1 + return unpacked_list + def create_dbinstance(self): return self.create_db_instance() @@ -135,6 +148,21 @@ def list_tags_for_resource(self): tags = self.backend.list_tags_for_resource(arn) return template.render(tags=tags) + def add_tags_to_resource(self): + arn = self._get_param('ResourceName') + tags = self.unpack_complex_list_params('Tags.member', ('Key', 'Value')) + self.backend.add_tags_to_resource(arn, tags) + template = self.response_template(ADD_TAGS_TO_RESOURCE_TEMPLATE) + return template.render() + + + def remove_tags_from_resource(self): + arn = self._get_param('ResourceName') + tag_keys = self.unpack_list_params('TagKeys.member') + self.backend.remove_tags_from_resource(arn, tag_keys) + template = self.response_template(REMOVE_TAGS_FROM_RESOURCE_TEMPLATE) + return template.render() + # TODO: Update function to new method def create_dbsecurity_group(self): group_name = self._get_param('DBSecurityGroupName') @@ -437,3 +465,10 @@ def modify_option_group(self): } } }""" + +ADD_TAGS_TO_RESOURCE_TEMPLATE = \ + """{"ListTagsForResourceResponse": {"ListTagsForResourceResult": {"TagList": [{"Value": "production", "Key": "workload-type"}, {"Value": "testvalue", "Key": "testkey"}]}, "ResponseMetadata": {"RequestId": "b194d9ca-a664-11e4-b688-194eaf8658fa"}}}""" + +REMOVE_TAGS_FROM_RESOURCE_TEMPLATE = \ + """{"RemoveTagsFromResourceResponse": {"ResponseMetadata": {"RequestId": "c6499a01-a664-11e4-8069-fb454b71a80e"}}} + """ \ No newline at end of file diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 7e50d1edfec3..07ee8aa5d347 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -291,6 +291,46 @@ def test_list_tags(): {'Value': 'bar1', 'Key': 'foo1'}]) + +@disable_on_py3() +@mock_rds2 +def test_add_tags(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.create_db_instance(db_instance_identifier='db-without-tags', + allocated_storage=10, + engine='postgres', + db_instance_class='db.m1.small', + master_username='root', + master_user_password='hunter2', + db_security_groups=["my_sg"], + tags=[('foo', 'bar'), ('foo1', 'bar1')]) + result = conn.list_tags_for_resource('arn:aws:rds:us-west-2:1234567890:db:db-without-tags') + list(result['ListTagsForResourceResponse']['ListTagsForResourceResult']['TagList']).should.have.length_of(2) + conn.add_tags_to_resource('arn:aws:rds:us-west-2:1234567890:db:db-without-tags', + [('foo', 'fish'), ('foo2', 'bar2')]) + result = conn.list_tags_for_resource('arn:aws:rds:us-west-2:1234567890:db:db-without-tags') + list(result['ListTagsForResourceResponse']['ListTagsForResourceResult']['TagList']).should.have.length_of(3) + + +@disable_on_py3() +@mock_rds2 +def test_remove_tags(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.create_db_instance(db_instance_identifier='db-with-tags', + allocated_storage=10, + engine='postgres', + db_instance_class='db.m1.small', + master_username='root', + master_user_password='hunter2', + db_security_groups=["my_sg"], + tags=[('foo', 'bar'), ('foo1', 'bar1')]) + result = conn.list_tags_for_resource('arn:aws:rds:us-west-2:1234567890:db:db-with-tags') + len(result['ListTagsForResourceResponse']['ListTagsForResourceResult']['TagList']).should.equal(2) + conn.remove_tags_from_resource('arn:aws:rds:us-west-2:1234567890:db:db-with-tags', ['foo']) + result = conn.list_tags_for_resource('arn:aws:rds:us-west-2:1234567890:db:db-with-tags') + len(result['ListTagsForResourceResponse']['ListTagsForResourceResult']['TagList']).should.equal(1) + + #@disable_on_py3() #@mock_rds2 #def test_create_database_security_group(): From 15fcec9c336e35de2559b631cf7617d8b5ea4fd5 Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Wed, 28 Jan 2015 10:24:11 +1100 Subject: [PATCH 25/35] Returning dymanic response for add_tags_to_resource --- moto/rds2/models.py | 1 + moto/rds2/responses.py | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 74f5efef7f11..bc73d16a5f93 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -252,6 +252,7 @@ def add_tags(self, tags): new_keys = [tag_set['Key'] for tag_set in tags] self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in new_keys] self.tags.extend(tags) + return self.tags def remove_tags(self, tag_keys): self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in tag_keys] diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index bade4a4f3417..21bec15d3245 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -151,9 +151,9 @@ def list_tags_for_resource(self): def add_tags_to_resource(self): arn = self._get_param('ResourceName') tags = self.unpack_complex_list_params('Tags.member', ('Key', 'Value')) - self.backend.add_tags_to_resource(arn, tags) + tags = self.backend.add_tags_to_resource(arn, tags) template = self.response_template(ADD_TAGS_TO_RESOURCE_TEMPLATE) - return template.render() + return template.render(tags=tags) def remove_tags_from_resource(self): @@ -467,7 +467,22 @@ def modify_option_group(self): }""" ADD_TAGS_TO_RESOURCE_TEMPLATE = \ - """{"ListTagsForResourceResponse": {"ListTagsForResourceResult": {"TagList": [{"Value": "production", "Key": "workload-type"}, {"Value": "testvalue", "Key": "testkey"}]}, "ResponseMetadata": {"RequestId": "b194d9ca-a664-11e4-b688-194eaf8658fa"}}}""" + """{"ListTagsForResourceResponse": { + "ListTagsForResourceResult": { + "TagList": [ + {%- for tag in tags -%} + {%- if loop.index != 1 -%},{%- endif -%} + { + "Key": "{{ tag['Key'] }}", + "Value": "{{ tag['Value'] }}" + } + {%- endfor -%} + ]}, + "ResponseMetadata": { + "RequestId": "b194d9ca-a664-11e4-b688-194eaf8658fa" + } + } + }""" REMOVE_TAGS_FROM_RESOURCE_TEMPLATE = \ """{"RemoveTagsFromResourceResponse": {"ResponseMetadata": {"RequestId": "c6499a01-a664-11e4-8069-fb454b71a80e"}}} From e42046aeda908acda36ee5e25179335b37bc9d20 Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Thu, 29 Jan 2015 07:15:03 +1100 Subject: [PATCH 26/35] extended the list/add/delete tags functions to support more resource types. --- moto/rds2/models.py | 118 +++++++++++++++++++++++++++++++---- moto/rds2/responses.py | 1 - tests/test_rds2/test_rds2.py | 14 ++--- 3 files changed, 113 insertions(+), 20 deletions(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index bc73d16a5f93..c9c617f5b483 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -265,6 +265,7 @@ def __init__(self, group_name, description): self.status = "authorized" self.ip_ranges = [] self.ec2_security_groups = [] + self.tags = [] def to_xml(self): template = Template(""" @@ -323,6 +324,18 @@ def create_from_cloudformation_json(cls, resource_name, cloudformation_json, reg security_group.authorize_security_group(subnet) return security_group + def get_tags(self): + return self.tags + + def add_tags(self, tags): + new_keys = [tag_set['Key'] for tag_set in tags] + self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in new_keys] + self.tags.extend(tags) + return self.tags + + def remove_tags(self, tag_keys): + self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in tag_keys] + class SubnetGroup(object): def __init__(self, subnet_name, description, subnets): @@ -330,7 +343,7 @@ def __init__(self, subnet_name, description, subnets): self.description = description self.subnets = subnets self.status = "Complete" - + self.tags = [] self.vpc_id = self.subnets[0].vpc_id def to_xml(self): @@ -395,6 +408,18 @@ def create_from_cloudformation_json(cls, resource_name, cloudformation_json, reg ) return subnet_group + def get_tags(self): + return self.tags + + def add_tags(self, tags): + new_keys = [tag_set['Key'] for tag_set in tags] + self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in new_keys] + self.tags.extend(tags) + return self.tags + + def remove_tags(self, tag_keys): + self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in tag_keys] + class RDS2Backend(BaseBackend): @@ -602,21 +627,58 @@ def modify_option_group(self, option_group_name, options_to_include=None, option def list_tags_for_resource(self, arn): if self.arn_regex.match(arn): arn_breakdown = arn.split(':') - db_instance_name = arn_breakdown[len(arn_breakdown)-1] - if db_instance_name in self.databases: - return self.databases[db_instance_name].get_tags() - else: + resource_type = arn_breakdown[len(arn_breakdown)-2] + resource_name = arn_breakdown[len(arn_breakdown)-1] + if resource_type == 'db': # Database + if resource_name in self.databases: + return self.databases[resource_name].get_tags() + elif resource_type == 'es': # Event Subscription + return [] + elif resource_type == 'og': # Option Group + if resource_name in self.option_groups: + return self.option_groups[resource_name].get_tags() + elif resource_type == 'pg': # Parameter Group return [] + elif resource_type == 'ri': # Reserved DB instance + return [] + elif resource_type == 'secgrp': # DB security group + if resource_type in self.security_groups: + return self.security_groups[resource_name].get_tags() + elif resource_type == 'snapshot': # DB Snapshot + return [] + elif resource_type == 'subgrp': # DB subnet group + if resource_type in self.subnet_groups: + return self.subnet_groups[resource_name].get_tags() else: raise RDSClientError('InvalidParameterValue', 'Invalid resource name: {}'.format(arn)) + return [] def remove_tags_from_resource(self, arn, tag_keys): if self.arn_regex.match(arn): arn_breakdown = arn.split(':') - db_instance_name = arn_breakdown[len(arn_breakdown)-1] - if db_instance_name in self.databases: - self.databases[db_instance_name].remove_tags(tag_keys) + resource_type = arn_breakdown[len(arn_breakdown)-2] + resource_name = arn_breakdown[len(arn_breakdown)-1] + if resource_type == 'db': # Database + if resource_name in self.databases: + self.databases[resource_name].remove_tags(tag_keys) + elif resource_type == 'es': # Event Subscription + return None + elif resource_type == 'og': # Option Group + if resource_name in self.option_groups: + return self.option_groups[resource_name].remove_tags(tag_keys) + elif resource_type == 'pg': # Parameter Group + return None + elif resource_type == 'ri': # Reserved DB instance + return None + elif resource_type == 'secgrp': # DB security group + if resource_type in self.security_groups: + return self.security_groups[resource_name].remove_tags(tag_keys) + elif resource_type == 'snapshot': # DB Snapshot + return None + elif resource_type == 'subgrp': # DB subnet group + if resource_type in self.subnet_groups: + return self.subnet_groups[resource_name].remove_tags(tag_keys) else: raise RDSClientError('InvalidParameterValue', 'Invalid resource name: {}'.format(arn)) @@ -624,15 +686,33 @@ def remove_tags_from_resource(self, arn, tag_keys): def add_tags_to_resource(self, arn, tags): if self.arn_regex.match(arn): arn_breakdown = arn.split(':') - db_instance_name = arn_breakdown[len(arn_breakdown)-1] - if db_instance_name in self.databases: - return self.databases[db_instance_name].add_tags(tags) - else: + resource_type = arn_breakdown[len(arn_breakdown)-2] + resource_name = arn_breakdown[len(arn_breakdown)-1] + if resource_type == 'db': # Database + if resource_name in self.databases: + return self.databases[resource_name].add_tags(tags) + elif resource_type == 'es': # Event Subscription return [] + elif resource_type == 'og': # Option Group + if resource_name in self.option_groups: + return self.option_groups[resource_name].add_tags(tags) + elif resource_type == 'pg': # Parameter Group + return [] + elif resource_type == 'ri': # Reserved DB instance + return [] + elif resource_type == 'secgrp': # DB security group + if resource_type in self.security_groups: + return self.security_groups[resource_name].add_tags(tags) + elif resource_type == 'snapshot': # DB Snapshot + return [] + elif resource_type == 'subgrp': # DB subnet group + if resource_type in self.subnet_groups: + return self.subnet_groups[resource_name].add_tags(tags) else: raise RDSClientError('InvalidParameterValue', 'Invalid resource name: {}'.format(arn)) + class OptionGroup(object): def __init__(self, name, engine_name, major_engine_version, description=None): self.engine_name = engine_name @@ -642,6 +722,7 @@ def __init__(self, name, engine_name, major_engine_version, description=None): self.vpc_and_non_vpc_instance_memberships = False self.options = {} self.vpcId = 'null' + self.tags = [] def to_json(self): template = Template("""{ @@ -663,6 +744,18 @@ def add_options(self, options_to_add): # TODO: Validate option and add it to self.options. If invalid raise error return + def get_tags(self): + return self.tags + + def add_tags(self, tags): + new_keys = [tag_set['Key'] for tag_set in tags] + self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in new_keys] + self.tags.extend(tags) + return self.tags + + def remove_tags(self, tag_keys): + self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in tag_keys] + class OptionGroupOption(object): def __init__(self, engine_name, major_engine_version): @@ -687,6 +780,7 @@ def to_json(self): }""") return template.render(option_group=self) + rds2_backends = {} for region in boto.rds2.regions(): rds2_backends[region.name] = RDS2Backend() diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index 21bec15d3245..1428765f42ff 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -155,7 +155,6 @@ def add_tags_to_resource(self): template = self.response_template(ADD_TAGS_TO_RESOURCE_TEMPLATE) return template.render(tags=tags) - def remove_tags_from_resource(self): arn = self._get_param('ResourceName') tag_keys = self.unpack_list_params('TagKeys.member') diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 07ee8aa5d347..a78657c095bd 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -273,7 +273,7 @@ def test_list_tags_invalid_arn(): @disable_on_py3() @mock_rds2 -def test_list_tags(): +def test_list_tags_db(): conn = boto.rds2.connect_to_region("us-west-2") result = conn.list_tags_for_resource('arn:aws:rds:us-west-2:1234567890:db:foo') result['ListTagsForResourceResponse']['ListTagsForResourceResult']['TagList'].should.equal([]) @@ -294,7 +294,7 @@ def test_list_tags(): @disable_on_py3() @mock_rds2 -def test_add_tags(): +def test_add_tags_db(): conn = boto.rds2.connect_to_region("us-west-2") conn.create_db_instance(db_instance_identifier='db-without-tags', allocated_storage=10, @@ -314,7 +314,7 @@ def test_add_tags(): @disable_on_py3() @mock_rds2 -def test_remove_tags(): +def test_remove_tags_db(): conn = boto.rds2.connect_to_region("us-west-2") conn.create_db_instance(db_instance_identifier='db-with-tags', allocated_storage=10, @@ -424,8 +424,8 @@ def test_remove_tags(): # subnet_group.name.should.equal('db_subnet') # subnet_group.description.should.equal("my db subnet") # list(subnet_group.subnet_ids).should.equal(subnet_ids) -# -# + + @mock_ec2 @mock_rds2 def test_describe_database_subnet_group(): @@ -451,8 +451,8 @@ def test_describe_database_subnet_group(): list(conn.describe_db_subnet_groups("db_subnet1")).should.have.length_of(1) conn.describe_db_subnet_groups.when.called_with("not-a-subnet").should.throw(BotoServerError) -# -# + + #@mock_ec2 #@mock_rds2 #def test_delete_database_subnet_group(): From 884bd51604317fd5a5d7e28dba1c4a0ba512d4d7 Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Thu, 29 Jan 2015 07:24:03 +1100 Subject: [PATCH 27/35] Added tests add/remove/list tags on option groups --- moto/rds2/models.py | 2 +- tests/test_rds2/test_rds2.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index c9c617f5b483..728cb9e25291 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -424,7 +424,7 @@ def remove_tags(self, tag_keys): class RDS2Backend(BaseBackend): def __init__(self): - self.arn_regex = re_compile(r'^arn:aws:rds:.*:[0-9]*:db:.*$') + self.arn_regex = re_compile(r'^arn:aws:rds:.*:[0-9]*:(db|es|og|pg|ri|secgrp|snapshot|subgrp):.*$') self.databases = {} self.security_groups = {} self.subnet_groups = {} diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index a78657c095bd..dde046c61185 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -331,6 +331,34 @@ def test_remove_tags_db(): len(result['ListTagsForResourceResponse']['ListTagsForResourceResult']['TagList']).should.equal(1) +@disable_on_py3() +@mock_rds2 +def test_add_tags_option_group(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.create_option_group('test', 'mysql', '5.6', 'test option group') + result = conn.list_tags_for_resource('arn:aws:rds:us-west-2:1234567890:og:test') + list(result['ListTagsForResourceResponse']['ListTagsForResourceResult']['TagList']).should.have.length_of(0) + conn.add_tags_to_resource('arn:aws:rds:us-west-2:1234567890:og:test', + [('foo', 'fish'), ('foo2', 'bar2')]) + result = conn.list_tags_for_resource('arn:aws:rds:us-west-2:1234567890:og:test') + list(result['ListTagsForResourceResponse']['ListTagsForResourceResult']['TagList']).should.have.length_of(2) + + +@disable_on_py3() +@mock_rds2 +def test_remove_tags_option_group(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.create_option_group('test', 'mysql', '5.6', 'test option group') + conn.add_tags_to_resource('arn:aws:rds:us-west-2:1234567890:og:test', + [('foo', 'fish'), ('foo2', 'bar2')]) + result = conn.list_tags_for_resource('arn:aws:rds:us-west-2:1234567890:og:test') + list(result['ListTagsForResourceResponse']['ListTagsForResourceResult']['TagList']).should.have.length_of(2) + conn.remove_tags_from_resource('arn:aws:rds:us-west-2:1234567890:og:test', + ['foo']) + result = conn.list_tags_for_resource('arn:aws:rds:us-west-2:1234567890:og:test') + list(result['ListTagsForResourceResponse']['ListTagsForResourceResult']['TagList']).should.have.length_of(1) + + #@disable_on_py3() #@mock_rds2 #def test_create_database_security_group(): From 3d431664f7fea4a129bfe5c67ef36c03fd6efcce Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Thu, 29 Jan 2015 07:31:11 +1100 Subject: [PATCH 28/35] Added some TODOs for my memory. --- moto/rds2/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 728cb9e25291..c2fd38935781 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -325,6 +325,7 @@ def create_from_cloudformation_json(cls, resource_name, cloudformation_json, reg return security_group def get_tags(self): + # TODO: Write tags add/remove/list tests for SecurityGroups return self.tags def add_tags(self, tags): @@ -409,6 +410,7 @@ def create_from_cloudformation_json(cls, resource_name, cloudformation_json, reg return subnet_group def get_tags(self): + # TODO: Write tags add/remove/list tests for SubnetGroups return self.tags def add_tags(self, tags): @@ -633,18 +635,22 @@ def list_tags_for_resource(self, arn): if resource_name in self.databases: return self.databases[resource_name].get_tags() elif resource_type == 'es': # Event Subscription + # TODO: Complete call to tags on resource type Event Subscription return [] elif resource_type == 'og': # Option Group if resource_name in self.option_groups: return self.option_groups[resource_name].get_tags() elif resource_type == 'pg': # Parameter Group + # TODO: Complete call to tags on resource type Parameter Group return [] elif resource_type == 'ri': # Reserved DB instance + # TODO: Complete call to tags on resource type Reserved DB instance return [] elif resource_type == 'secgrp': # DB security group if resource_type in self.security_groups: return self.security_groups[resource_name].get_tags() elif resource_type == 'snapshot': # DB Snapshot + # TODO: Complete call to tags on resource type DB Snapshot return [] elif resource_type == 'subgrp': # DB subnet group if resource_type in self.subnet_groups: From eb2398093775c16a7b825f0ce4c4f5214becd056 Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Thu, 29 Jan 2015 07:36:16 +1100 Subject: [PATCH 29/35] Added myself and Chris Henry to Contributors --- AUTHORS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS.md b/AUTHORS.md index 8b086229e004..fa286f1a4e56 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -33,3 +33,5 @@ Moto is written by Steve Pulec with contributions from: * [Peter](https://github.com/pvbouwel) * [Tyler Sanders](https://github.com/tsanders) * [Gary Dalton](https://github.com/gary-dalton) +* [Chris Henry](https://github.com/chrishenry) +* [Mike Fuller](https://github.com/mfulleratlassian) From 8614b508987b2224cf461716753deab0f04d9130 Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Thu, 29 Jan 2015 17:25:39 +1100 Subject: [PATCH 30/35] Added DB Security Group Create/List/Delete/Authorize --- moto/rds2/models.py | 17 +++++ moto/rds2/responses.py | 101 +++++++++++++++---------- tests/test_rds2/test_rds2.py | 140 +++++++++++++++++++---------------- 3 files changed, 155 insertions(+), 103 deletions(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index c2fd38935781..a3f06203baac 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -266,6 +266,8 @@ def __init__(self, group_name, description): self.ip_ranges = [] self.ec2_security_groups = [] self.tags = [] + self.owner_id = '1234567890' + self.vpc_id = None def to_xml(self): template = Template(""" @@ -294,6 +296,21 @@ def to_xml(self): """) return template.render(security_group=self) + def to_json(self): + template = Template("""{ + "DBSecurityGroupDescription": "{{ security_group.description }}", + "DBSecurityGroupName": "{{ security_group.group_name }}", + "EC2SecurityGroups": {{ security_group.ec2_security_groups }}, + "IPRanges": [{%- for ip in security_group.ip_ranges -%} + {%- if loop.index != 1 -%},{%- endif -%} + "{{ ip }}" + {%- endfor -%} + ], + "OwnerId": "{{ security_group.owner_id }}", + "VpcId": "{{ security_group.vpc_id }}" + }""") + return template.render(security_group=self) + def authorize_cidr(self, cidr_ip): self.ip_ranges.append(cidr_ip) diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index 1428765f42ff..d262c1a5f939 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -162,30 +162,38 @@ def remove_tags_from_resource(self): template = self.response_template(REMOVE_TAGS_FROM_RESOURCE_TEMPLATE) return template.render() - # TODO: Update function to new method def create_dbsecurity_group(self): + return self.create_db_security_group() + + def create_db_security_group(self): group_name = self._get_param('DBSecurityGroupName') description = self._get_param('DBSecurityGroupDescription') security_group = self.backend.create_security_group(group_name, description) template = self.response_template(CREATE_SECURITY_GROUP_TEMPLATE) return template.render(security_group=security_group) - # TODO: Update function to new method def describe_dbsecurity_groups(self): + return self.describe_db_security_groups() + + def describe_db_security_groups(self): security_group_name = self._get_param('DBSecurityGroupName') security_groups = self.backend.describe_security_groups(security_group_name) template = self.response_template(DESCRIBE_SECURITY_GROUPS_TEMPLATE) return template.render(security_groups=security_groups) - # TODO: Update function to new method def delete_dbsecurity_group(self): + return self.delete_db_security_group() + + def delete_db_security_group(self): security_group_name = self._get_param('DBSecurityGroupName') security_group = self.backend.delete_security_group(security_group_name) template = self.response_template(DELETE_SECURITY_GROUP_TEMPLATE) return template.render(security_group=security_group) - # TODO: Update function to new method def authorize_dbsecurity_group_ingress(self): + return self.authorize_db_security_group_ingress() + + def authorize_db_security_group_ingress(self): security_group_name = self._get_param('DBSecurityGroupName') cidr_ip = self._get_param('CIDRIP') security_group = self.backend.authorize_security_group(security_group_name, cidr_ip) @@ -193,6 +201,9 @@ def authorize_dbsecurity_group_ingress(self): return template.render(security_group=security_group) def create_dbsubnet_group(self): + return self.create_db_subnet_group() + + def create_db_subnet_group(self): subnet_name = self._get_param('DBSubnetGroupName') description = self._get_param('DBSubnetGroupDescription') subnet_ids = self._get_multi_param('SubnetIds.member') @@ -202,13 +213,18 @@ def create_dbsubnet_group(self): return template.render(subnet_group=subnet_group) def describe_dbsubnet_groups(self): + return self.describe_db_subnet_groups() + + def describe_db_subnet_groups(self): subnet_name = self._get_param('DBSubnetGroupName') subnet_groups = self.backend.describe_subnet_groups(subnet_name) template = self.response_template(DESCRIBE_SUBNET_GROUPS_TEMPLATE) return template.render(subnet_groups=subnet_groups) - # TODO: Update function to new method def delete_dbsubnet_group(self): + return self.delete_db_subnet_group() + + def delete_db_subnet_group(self): subnet_name = self._get_param('DBSubnetGroupName') subnet_group = self.backend.delete_subnet_group(subnet_name) template = self.response_template(DELETE_SUBNET_GROUP_TEMPLATE) @@ -331,42 +347,49 @@ def modify_option_group(self): } }""" -CREATE_SECURITY_GROUP_TEMPLATE = """ - - {{ security_group.to_xml() }} - - - e68ef6fa-afc1-11c3-845a-476777009d19 - -""" - -DESCRIBE_SECURITY_GROUPS_TEMPLATE = """ - - - {% for security_group in security_groups %} - {{ security_group.to_xml() }} - {% endfor %} - - - - b76e692c-b98c-11d3-a907-5a2c468b9cb0 - -""" +CREATE_SECURITY_GROUP_TEMPLATE = """{"CreateDBSecurityGroupResponse": { + "CreateDBSecurityGroupResult": { + "DBSecurityGroup": + {{ security_group.to_json() }}, + "ResponseMetadata": { + "RequestId": "462165d0-a77a-11e4-a5fa-75b30c556f97" + }} + } +}""" -DELETE_SECURITY_GROUP_TEMPLATE = """ - - 7aec7454-ba25-11d3-855b-576787000e19 - -""" +DESCRIBE_SECURITY_GROUPS_TEMPLATE = """{ + "DescribeDBSecurityGroupsResponse": { + "ResponseMetadata": { + "RequestId": "5df2014e-a779-11e4-bdb0-594def064d0c" + }, + "DescribeDBSecurityGroupsResult": { + "Marker": "null", + "DBSecurityGroups": [ + {% for security_group in security_groups %} + {%- if loop.index != 1 -%},{%- endif -%} + {{ security_group.to_json() }} + {% endfor %} + ] + } + } +}""" -AUTHORIZE_SECURITY_GROUP_TEMPLATE = """ - - {{ security_group.to_xml() }} - - - 6176b5f8-bfed-11d3-f92b-31fa5e8dbc99 - -""" +DELETE_SECURITY_GROUP_TEMPLATE = """{"DeleteDBSecurityGroupResponse": { + "ResponseMetadata": { + "RequestId": "97e846bd-a77d-11e4-ac58-91351c0f3426" + } +}}""" + +AUTHORIZE_SECURITY_GROUP_TEMPLATE = """{ + "AuthorizeDBSecurityGroupIngressResponse": { + "AuthorizeDBSecurityGroupIngressResult": { + "DBSecurityGroup": {{ security_group.to_json() }} + }, + "ResponseMetadata": { + "RequestId": "75d32fd5-a77e-11e4-8892-b10432f7a87d" + } + } +}""" CREATE_SUBNET_GROUP_TEMPLATE = """{ "CreateDBSubnetGroupResponse": { diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index dde046c61185..1a191e492c4f 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -359,70 +359,82 @@ def test_remove_tags_option_group(): list(result['ListTagsForResourceResponse']['ListTagsForResourceResult']['TagList']).should.have.length_of(1) -#@disable_on_py3() -#@mock_rds2 -#def test_create_database_security_group(): -# conn = boto.rds2.connect_to_region("us-west-2") -# -# security_group = conn.create_dbsecurity_group('db_sg', 'DB Security Group') -# security_group.name.should.equal('db_sg') -# security_group.description.should.equal("DB Security Group") -# list(security_group.ip_ranges).should.equal([]) -# -# -#@mock_rds2 -#def test_get_security_groups(): -# conn = boto.rds2.connect_to_region("us-west-2") -# -# list(conn.get_all_dbsecurity_groups()).should.have.length_of(0) -# -# conn.create_dbsecurity_group('db_sg1', 'DB Security Group') -# conn.create_dbsecurity_group('db_sg2', 'DB Security Group') -# -# list(conn.get_all_dbsecurity_groups()).should.have.length_of(2) -# -# databases = conn.get_all_dbsecurity_groups("db_sg1") -# list(databases).should.have.length_of(1) -# -# databases[0].name.should.equal("db_sg1") -# -# -#@mock_rds2 -#def test_get_non_existant_security_group(): -# conn = boto.rds2.connect_to_region("us-west-2") -# conn.get_all_dbsecurity_groups.when.called_with("not-a-sg").should.throw(BotoServerError) -# -# -#@mock_rds2 -#def test_delete_database_security_group(): -# conn = boto.rds2.connect_to_region("us-west-2") -# conn.create_dbsecurity_group('db_sg', 'DB Security Group') -# -# list(conn.get_all_dbsecurity_groups()).should.have.length_of(1) -# -# conn.delete_dbsecurity_group("db_sg") -# list(conn.get_all_dbsecurity_groups()).should.have.length_of(0) -# -# -#@mock_rds2 -#def test_delete_non_existant_security_group(): -# conn = boto.rds2.connect_to_region("us-west-2") -# conn.delete_dbsecurity_group.when.called_with("not-a-db").should.throw(BotoServerError) -# -# -#@disable_on_py3() -#@mock_rds2 -#def test_security_group_authorize(): -# conn = boto.rds2.connect_to_region("us-west-2") -# security_group = conn.create_dbsecurity_group('db_sg', 'DB Security Group') -# list(security_group.ip_ranges).should.equal([]) -# -# security_group.authorize(cidr_ip='10.3.2.45/32') -# security_group = conn.get_all_dbsecurity_groups()[0] -# list(security_group.ip_ranges).should.have.length_of(1) -# security_group.ip_ranges[0].cidr_ip.should.equal('10.3.2.45/32') -# -# +@disable_on_py3() +@mock_rds2 +def test_create_database_security_group(): + conn = boto.rds2.connect_to_region("us-west-2") + + result = conn.create_db_security_group('db_sg', 'DB Security Group') + result['CreateDBSecurityGroupResponse']['CreateDBSecurityGroupResult']['DBSecurityGroup']['DBSecurityGroupName'].should.equal("db_sg") + result['CreateDBSecurityGroupResponse']['CreateDBSecurityGroupResult']['DBSecurityGroup']['DBSecurityGroupDescription'].should.equal("DB Security Group") + result['CreateDBSecurityGroupResponse']['CreateDBSecurityGroupResult']['DBSecurityGroup']['IPRanges'].should.equal([]) + + +@mock_rds2 +def test_get_security_groups(): + conn = boto.rds2.connect_to_region("us-west-2") + + result = conn.describe_db_security_groups() + result['DescribeDBSecurityGroupsResponse']['DescribeDBSecurityGroupsResult']['DBSecurityGroups'].should.have.length_of(0) + + conn.create_db_security_group('db_sg1', 'DB Security Group') + conn.create_db_security_group('db_sg2', 'DB Security Group') + + result = conn.describe_db_security_groups() + result['DescribeDBSecurityGroupsResponse']['DescribeDBSecurityGroupsResult']['DBSecurityGroups'].should.have.length_of(2) + + result = conn.describe_db_security_groups("db_sg1") + result['DescribeDBSecurityGroupsResponse']['DescribeDBSecurityGroupsResult']['DBSecurityGroups'].should.have.length_of(1) + result['DescribeDBSecurityGroupsResponse']['DescribeDBSecurityGroupsResult']['DBSecurityGroups'][0]['DBSecurityGroupName'].should.equal("db_sg1") + + +@mock_rds2 +def test_get_non_existant_security_group(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.describe_db_security_groups.when.called_with("not-a-sg").should.throw(BotoServerError) + + +@mock_rds2 +def test_delete_database_security_group(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.create_db_security_group('db_sg', 'DB Security Group') + + result = conn.describe_db_security_groups() + result['DescribeDBSecurityGroupsResponse']['DescribeDBSecurityGroupsResult']['DBSecurityGroups'].should.have.length_of(1) + + conn.delete_db_security_group("db_sg") + result = conn.describe_db_security_groups() + result['DescribeDBSecurityGroupsResponse']['DescribeDBSecurityGroupsResult']['DBSecurityGroups'].should.have.length_of(0) + + +@mock_rds2 +def test_delete_non_existant_security_group(): + conn = boto.rds2.connect_to_region("us-west-2") + conn.delete_db_security_group.when.called_with("not-a-db").should.throw(BotoServerError) + + +@disable_on_py3() +@mock_rds2 +def test_security_group_authorize(): + conn = boto.rds2.connect_to_region("us-west-2") + security_group = conn.create_db_security_group('db_sg', 'DB Security Group') + security_group['CreateDBSecurityGroupResponse']['CreateDBSecurityGroupResult']['DBSecurityGroup']['IPRanges'].should.equal([]) + + + conn.authorize_db_security_group_ingress(db_security_group_name='db_sg', + cidrip='10.3.2.45/32') + + result = conn.describe_db_security_groups("db_sg") + result['DescribeDBSecurityGroupsResponse']['DescribeDBSecurityGroupsResult']['DBSecurityGroups'][0]['IPRanges'].should.have.length_of(1) + result['DescribeDBSecurityGroupsResponse']['DescribeDBSecurityGroupsResult']['DBSecurityGroups'][0]['IPRanges'].should.equal(['10.3.2.45/32']) + + conn.authorize_db_security_group_ingress(db_security_group_name='db_sg', + cidrip='10.3.2.46/32') + result = conn.describe_db_security_groups("db_sg") + result['DescribeDBSecurityGroupsResponse']['DescribeDBSecurityGroupsResult']['DBSecurityGroups'][0]['IPRanges'].should.have.length_of(2) + result['DescribeDBSecurityGroupsResponse']['DescribeDBSecurityGroupsResult']['DBSecurityGroups'][0]['IPRanges'].should.equal(['10.3.2.45/32', '10.3.2.46/32']) + + #@disable_on_py3() #@mock_rds2 #def test_add_security_group_to_database(): From 42ab9312bb26f09f9c696f7751e9ebb4e7deb9a4 Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Fri, 30 Jan 2015 08:18:15 +1100 Subject: [PATCH 31/35] Added db_subnet_group support --- moto/rds2/models.py | 45 +++-------- moto/rds2/responses.py | 10 +-- tests/test_rds2/test_rds2.py | 146 +++++++++++++++++++---------------- 3 files changed, 94 insertions(+), 107 deletions(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index a3f06203baac..915386c00ed0 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -60,8 +60,8 @@ def __init__(self, **kwargs): if self.db_subnet_group_name: self.db_subnet_group = rds2_backends[self.region].describe_subnet_groups(self.db_subnet_group_name)[0] else: - self.db_subnet_group = [] - self.db_security_groups = kwargs.get('security_groups', ['a']) + self.db_subnet_group = None + self.security_groups = kwargs.get('security_groups', []) self.vpc_security_group_ids = kwargs.get('vpc_security_group_ids', []) self.preferred_maintenance_window = kwargs.get('preferred_maintenance_window', 'wed:06:38-wed:07:08') self.db_parameter_group_name = kwargs.get('db_parameter_group_name', None) @@ -174,37 +174,14 @@ def to_json(self): "DBParameterGroupName": "{{ database.db_parameter_group_name }}" } },{%- endif %} - "DBSecurityGroups": [{ - {% for security_group in database.db_security_groups -%}{%- if loop.index != 1 -%},{%- endif -%} - "DBSecurityGroup": { + "DBSecurityGroups": [ + {% for security_group in database.security_groups -%}{%- if loop.index != 1 -%},{%- endif -%} + {"DBSecurityGroup": { "Status": "active", "DBSecurityGroupName": "{{ security_group }}" - }{% endfor %} - }],{%- if database.db_subnet_group -%} - "DBSubnetGroup": { - "DBSubnetGroupDescription": "nabil-db-subnet-group", - "DBSubnetGroupName": "nabil-db-subnet-group", - "SubnetGroupStatus": "Complete", - "Subnets": [ - { - "SubnetAvailabilityZone": { - "Name": "us-west-2c", - "ProvisionedIopsCapable": false - }, - "SubnetIdentifier": "subnet-c0ea0099", - "SubnetStatus": "Active" - }, - { - "SubnetAvailabilityZone": { - "Name": "us-west-2a", - "ProvisionedIopsCapable": false - }, - "SubnetIdentifier": "subnet-ff885d88", - "SubnetStatus": "Active" - } - ], - "VpcId": "vpc-8e6ab6eb" - },{%- endif %} + }}{% endfor %} + ], + {%- if database.db_subnet_group -%}{{ database.db_subnet_group.to_json() }},{%- endif %} "Engine": "{{ database.engine }}", "EngineVersion": "{{ database.engine_version }}", "LatestRestorableTime": null, @@ -386,8 +363,7 @@ def to_xml(self): return template.render(subnet_group=self) def to_json(self): - template = Template("""{ - "DBSubnetGroup": { + template = Template(""""DBSubnetGroup": { "VpcId": "{{ subnet_group.vpc_id }}", "SubnetGroupStatus": "{{ subnet_group.status }}", "DBSubnetGroupDescription": "{{ subnet_group.description }}", @@ -404,8 +380,7 @@ def to_json(self): }{%- if not loop.last -%},{%- endif -%}{% endfor %} ] } - } - }""") + }""") return template.render(subnet_group=self) @classmethod diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index d262c1a5f939..c4753e6034d7 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -394,7 +394,7 @@ def modify_option_group(self): CREATE_SUBNET_GROUP_TEMPLATE = """{ "CreateDBSubnetGroupResponse": { "CreateDBSubnetGroupResult": - {{ subnet_group.to_json() }}, + { {{ subnet_group.to_json() }} }, "ResponseMetadata": { "RequestId": "3a401b3f-bb9e-11d3-f4c6-37db295f7674" } } }""" @@ -404,7 +404,7 @@ def modify_option_group(self): "DescribeDBSubnetGroupsResult": { "DBSubnetGroups": [ {% for subnet_group in subnet_groups %} - {{ subnet_group.to_json() }}{%- if not loop.last -%},{%- endif -%} + { {{ subnet_group.to_json() }} }{%- if not loop.last -%},{%- endif -%} {% endfor %} ], "Marker": null @@ -414,11 +414,7 @@ def modify_option_group(self): }""" -DELETE_SUBNET_GROUP_TEMPLATE = """ - - 6295e5ab-bbf3-11d3-f4c6-37db295f7674 - -""" +DELETE_SUBNET_GROUP_TEMPLATE = """{"DeleteDBSubnetGroupResponse": {"ResponseMetadata": {"RequestId": "13785dd5-a7fc-11e4-bb9c-7f371d0859b0"}}}""" CREATE_OPTION_GROUP_TEMPLATE = """{ "CreateOptionGroupResponse": { diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 1a191e492c4f..a6d930ea0be8 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -4,7 +4,7 @@ import boto.vpc from boto.exception import BotoServerError import sure # noqa - +import json from moto import mock_ec2, mock_rds2 from tests.helpers import disable_on_py3 @@ -435,35 +435,63 @@ def test_security_group_authorize(): result['DescribeDBSecurityGroupsResponse']['DescribeDBSecurityGroupsResult']['DBSecurityGroups'][0]['IPRanges'].should.equal(['10.3.2.45/32', '10.3.2.46/32']) -#@disable_on_py3() -#@mock_rds2 -#def test_add_security_group_to_database(): -# conn = boto.rds2.connect_to_region("us-west-2") -# -# database = conn.create_dbinstance("db-master-1", 10, 'db.m1.small', 'root', 'hunter2') -# security_group = conn.create_dbsecurity_group('db_sg', 'DB Security Group') -# database.modify(security_groups=[security_group]) -# -# database = conn.get_all_dbinstances()[0] -# list(database.security_groups).should.have.length_of(1) -# -# database.security_groups[0].name.should.equal("db_sg") -# -# -#@mock_ec2 -#@mock_rds2 -#def test_add_database_subnet_group(): -# vpc_conn = boto.vpc.connect_to_region("us-west-2") -# vpc = vpc_conn.create_vpc("10.0.0.0/16") -# subnet1 = vpc_conn.create_subnet(vpc.id, "10.1.0.0/24") -# subnet2 = vpc_conn.create_subnet(vpc.id, "10.2.0.0/24") -# -# subnet_ids = [subnet1.id, subnet2.id] -# conn = boto.rds2.connect_to_region("us-west-2") -# subnet_group = conn.create_db_subnet_group("db_subnet", "my db subnet", subnet_ids) -# subnet_group.name.should.equal('db_subnet') -# subnet_group.description.should.equal("my db subnet") -# list(subnet_group.subnet_ids).should.equal(subnet_ids) +@disable_on_py3() +@mock_rds2 +def test_add_security_group_to_database(): + conn = boto.rds2.connect_to_region("us-west-2") + + conn.create_db_instance(db_instance_identifier='db-master-1', + allocated_storage=10, + engine='postgres', + db_instance_class='db.m1.small', + master_username='root', + master_user_password='hunter2') + result = conn.describe_db_instances() + result['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances'][0]['DBSecurityGroups'].should.equal([]) + conn.create_db_security_group('db_sg', 'DB Security Group') + conn.modify_db_instance(db_instance_identifier='db-master-1', + db_security_groups=['db_sg']) + result = conn.describe_db_instances() + result['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances'][0]['DBSecurityGroups'][0]['DBSecurityGroup']['DBSecurityGroupName'].should.equal('db_sg') + + +@mock_ec2 +@mock_rds2 +def test_create_database_subnet_group(): + vpc_conn = boto.vpc.connect_to_region("us-west-2") + vpc = vpc_conn.create_vpc("10.0.0.0/16") + subnet1 = vpc_conn.create_subnet(vpc.id, "10.1.0.0/24") + subnet2 = vpc_conn.create_subnet(vpc.id, "10.2.0.0/24") + + subnet_ids = [subnet1.id, subnet2.id] + conn = boto.rds2.connect_to_region("us-west-2") + result = conn.create_db_subnet_group("db_subnet", "my db subnet", subnet_ids) + result['CreateDBSubnetGroupResponse']['CreateDBSubnetGroupResult']['DBSubnetGroup']['DBSubnetGroupName'].should.equal("db_subnet") + result['CreateDBSubnetGroupResponse']['CreateDBSubnetGroupResult']['DBSubnetGroup']['DBSubnetGroupDescription'].should.equal("my db subnet") + subnets = result['CreateDBSubnetGroupResponse']['CreateDBSubnetGroupResult']['DBSubnetGroup']['Subnets'] + subnet_group_ids = [subnets['Subnet'][0]['SubnetIdentifier'], subnets['Subnet'][1]['SubnetIdentifier']] + list(subnet_group_ids).should.equal(subnet_ids) + + +@disable_on_py3() +@mock_ec2 +@mock_rds2 +def test_create_database_in_subnet_group(): + vpc_conn = boto.vpc.connect_to_region("us-west-2") + vpc = vpc_conn.create_vpc("10.0.0.0/16") + subnet = vpc_conn.create_subnet(vpc.id, "10.1.0.0/24") + + conn = boto.rds2.connect_to_region("us-west-2") + conn.create_db_subnet_group("db_subnet1", "my db subnet", [subnet.id]) + conn.create_db_instance(db_instance_identifier='db-master-1', + allocated_storage=10, + engine='postgres', + db_instance_class='db.m1.small', + master_username='root', + master_user_password='hunter2', + db_subnet_group_name='db_subnet1') + result = conn.describe_db_instances("db-master-1") + result['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances'][0]['DBSubnetGroup']['DBSubnetGroupName'].should.equal("db_subnet1") @mock_ec2 @@ -493,41 +521,29 @@ def test_describe_database_subnet_group(): conn.describe_db_subnet_groups.when.called_with("not-a-subnet").should.throw(BotoServerError) -#@mock_ec2 -#@mock_rds2 -#def test_delete_database_subnet_group(): -# vpc_conn = boto.vpc.connect_to_region("us-west-2") -# vpc = vpc_conn.create_vpc("10.0.0.0/16") -# subnet = vpc_conn.create_subnet(vpc.id, "10.1.0.0/24") -# -# conn = boto.rds2.connect_to_region("us-west-2") -# conn.create_db_subnet_group("db_subnet1", "my db subnet", [subnet.id]) -# list(conn.get_all_db_subnet_groups()).should.have.length_of(1) -# -# conn.delete_db_subnet_group("db_subnet1") -# list(conn.get_all_db_subnet_groups()).should.have.length_of(0) -# -# conn.delete_db_subnet_group.when.called_with("db_subnet1").should.throw(BotoServerError) -# -# -#@disable_on_py3() -#@mock_ec2 -#@mock_rds2 -#def test_create_database_in_subnet_group(): -# vpc_conn = boto.vpc.connect_to_region("us-west-2") -# vpc = vpc_conn.create_vpc("10.0.0.0/16") -# subnet = vpc_conn.create_subnet(vpc.id, "10.1.0.0/24") -# -# conn = boto.rds2.connect_to_region("us-west-2") -# conn.create_db_subnet_group("db_subnet1", "my db subnet", [subnet.id]) -# -# database = conn.create_dbinstance("db-master-1", 10, 'db.m1.small', -# 'root', 'hunter2', db_subnet_group_name="db_subnet1") -# -# database = conn.get_all_dbinstances("db-master-1")[0] -# database.subnet_group.name.should.equal("db_subnet1") -# -# +@mock_ec2 +@mock_rds2 +def test_delete_database_subnet_group(): + vpc_conn = boto.vpc.connect_to_region("us-west-2") + vpc = vpc_conn.create_vpc("10.0.0.0/16") + subnet = vpc_conn.create_subnet(vpc.id, "10.1.0.0/24") + + conn = boto.rds2.connect_to_region("us-west-2") + result = conn.describe_db_subnet_groups() + result['DescribeDBSubnetGroupsResponse']['DescribeDBSubnetGroupsResult']['DBSubnetGroups'].should.have.length_of(0) + + conn.create_db_subnet_group("db_subnet1", "my db subnet", [subnet.id]) + result = conn.describe_db_subnet_groups() + result['DescribeDBSubnetGroupsResponse']['DescribeDBSubnetGroupsResult']['DBSubnetGroups'].should.have.length_of(1) + + conn.delete_db_subnet_group("db_subnet1") + result = conn.describe_db_subnet_groups() + result['DescribeDBSubnetGroupsResponse']['DescribeDBSubnetGroupsResult']['DBSubnetGroups'].should.have.length_of(0) + + conn.delete_db_subnet_group.when.called_with("db_subnet1").should.throw(BotoServerError) + + + #@disable_on_py3() #@mock_rds2 #def test_create_database_replica(): From 0d958e9b1c5ae7394b356ce488fd2c868984494f Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Fri, 30 Jan 2015 17:12:51 +1100 Subject: [PATCH 32/35] Added read replica support --- README.md | 3 -- moto/rds2/models.py | 6 ++-- moto/rds2/responses.py | 16 +++++----- tests/test_rds2/test_rds2.py | 57 +++++++++++++++--------------------- 4 files changed, 36 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index a20e3f379577..fb6b43593831 100644 --- a/README.md +++ b/README.md @@ -80,9 +80,6 @@ It gets even better! Moto isn't just S3. Here's the status of the other AWS serv | RDS | @mock_rds | core endpoints done | |------------------------------------------------------------------------------| | RDS2 | @mock_rds2 | core endpoints done | -| - Database | | core endpoints done | -| - Security Group | | not done | -| - Option Group | | core endpoints done | |------------------------------------------------------------------------------| | Route53 | @mock_route53 | core endpoints done | |------------------------------------------------------------------------------| diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 915386c00ed0..82349aa06674 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -88,14 +88,12 @@ def __init__(self, **kwargs): def address(self): return "{0}.aaaaaaaaaa.{1}.rds.amazonaws.com".format(self.db_instance_identifier, self.region) - # TODO: confirm how this should represent in the RESULT JSON def add_replica(self, replica): self.replicas.append(replica.db_instance_identifier) def remove_replica(self, replica): self.replicas.remove(replica.db_instance_identifier) - # TODO: confirm how this should represent in the RESULT JSON def set_as_replica(self): self.is_replica = True self.replicas = [] @@ -210,7 +208,11 @@ def to_json(self): "{{ replica }}" {%- endfor -%} ], + {%- if database.source_db_identifier -%} + "ReadReplicaSourceDBInstanceIdentifier": "{{ database.source_db_identifier }}", + {%- else -%} "ReadReplicaSourceDBInstanceIdentifier": null, + {%- endif -%} "SecondaryAvailabilityZone": null, "StatusInfos": null, "VpcSecurityGroups": [ diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index c4753e6034d7..8df22f467a12 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -293,14 +293,14 @@ def modify_option_group(self): } }""" -CREATE_DATABASE_REPLICA_TEMPLATE = """{ - "CreateDBInstanceResponse": { - "CreateDBInstanceResult": { - {{ database.to_json() }} - }, - "ResponseMetadata": { "RequestId": "523e3218-afc7-11c3-90f5-f90431260ab4" } +CREATE_DATABASE_REPLICA_TEMPLATE = """{"CreateDBInstanceReadReplicaResponse": { + "ResponseMetadata": { + "RequestId": "5e60c46d-a844-11e4-bb68-17f36418e58f" + }, + "CreateDBInstanceReadReplicaResult": { + "DBInstance": {{ database.to_json() }} } -}""" +}}""" DESCRIBE_DATABASES_TEMPLATE = """{ "DescribeDBInstancesResponse": { @@ -336,7 +336,7 @@ def modify_option_group(self): } }""" -# TODO: update delete DB TEMPLATE + DELETE_DATABASE_TEMPLATE = """{ "DeleteDBInstanceResponse": { "DeleteDBInstanceResult": { "DBInstance": {{ database.to_json() }} diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index a6d930ea0be8..81e8a54bf3fb 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -4,7 +4,6 @@ import boto.vpc from boto.exception import BotoServerError import sure # noqa -import json from moto import mock_ec2, mock_rds2 from tests.helpers import disable_on_py3 @@ -543,36 +542,28 @@ def test_delete_database_subnet_group(): conn.delete_db_subnet_group.when.called_with("db_subnet1").should.throw(BotoServerError) +@disable_on_py3() +@mock_rds2 +def test_create_database_replica(): + conn = boto.rds2.connect_to_region("us-west-2") + + conn.create_db_instance(db_instance_identifier='db-master-1', + allocated_storage=10, + engine='postgres', + db_instance_class='db.m1.small', + master_username='root', + master_user_password='hunter2', + db_security_groups=["my_sg"]) + + replica = conn.create_db_instance_read_replica("db-replica-1", "db-master-1", "db.m1.small") + replica['CreateDBInstanceReadReplicaResponse']['CreateDBInstanceReadReplicaResult']['DBInstance']['ReadReplicaSourceDBInstanceIdentifier'].should.equal('db-master-1') + replica['CreateDBInstanceReadReplicaResponse']['CreateDBInstanceReadReplicaResult']['DBInstance']['DBInstanceClass'].should.equal('db.m1.small') + replica['CreateDBInstanceReadReplicaResponse']['CreateDBInstanceReadReplicaResult']['DBInstance']['DBInstanceIdentifier'].should.equal('db-replica-1') + + master = conn.describe_db_instances("db-master-1") + master['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances'][0]['ReadReplicaDBInstanceIdentifiers'].should.equal(['db-replica-1']) + + conn.delete_db_instance("db-replica-1") -#@disable_on_py3() -#@mock_rds2 -#def test_create_database_replica(): -# conn = boto.rds2.connect_to_region("us-west-2") -# -# conn.create_db_instance(db_instance_identifier='db-master-1', -# allocated_storage=10, -# engine='postgres', -# db_instance_class='db.m1.small', -# master_username='root', -# master_user_password='hunter2', -# db_security_groups=["my_sg"]) -# -# # TODO: confirm the RESULT JSON -# replica = conn.create_db_instance_read_replica("replica", "db-master-1", "db.m1.small") -# print replica - #replica.id.should.equal("replica") - #replica.instance_class.should.equal("db.m1.small") - #status_info = replica.status_infos[0] - #status_info.normal.should.equal(True) - #status_info.status_type.should.equal('read replication') - #status_info.status.should.equal('replicating') - - # TODO: formulate checks on read replica status -# primary = conn.describe_db_instances("db-master-1") -# print primary - #primary.read_replica_dbinstance_identifiers[0].should.equal("replica") - - #conn.delete_dbinstance("replica") - - #primary = conn.get_all_dbinstances("db-master-1")[0] - #list(primary.read_replica_dbinstance_identifiers).should.have.length_of(0) + master = conn.describe_db_instances("db-master-1") + master['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances'][0]['ReadReplicaDBInstanceIdentifiers'].should.equal([]) From cff5238929ba9f2610b2b72898e784ad92da7c3b Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Fri, 30 Jan 2015 17:28:41 +1100 Subject: [PATCH 33/35] Fixed up use of format using {} instead of {0} which seams to break python 2.6.x --- moto/rds2/responses.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index 8df22f467a12..daa068aa6935 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -260,20 +260,20 @@ def modify_option_group(self): option_group_name = self._get_param('OptionGroupName') count = 1 options_to_include = [] - while self._get_param('OptionsToInclude.member.{}.OptionName'.format(count)): + while self._get_param('OptionsToInclude.member.{0}.OptionName'.format(count)): options_to_include.append({ - 'Port': self._get_param('OptionsToInclude.member.{}.Port'.format(count)), - 'OptionName': self._get_param('OptionsToInclude.member.{}.OptionName'.format(count)), - 'DBSecurityGroupMemberships': self._get_param('OptionsToInclude.member.{}.DBSecurityGroupMemberships'.format(count)), - 'OptionSettings': self._get_param('OptionsToInclude.member.{}.OptionSettings'.format(count)), - 'VpcSecurityGroupMemberships': self._get_param('OptionsToInclude.member.{}.VpcSecurityGroupMemberships'.format(count)) + 'Port': self._get_param('OptionsToInclude.member.{0}.Port'.format(count)), + 'OptionName': self._get_param('OptionsToInclude.member.{0}.OptionName'.format(count)), + 'DBSecurityGroupMemberships': self._get_param('OptionsToInclude.member.{0}.DBSecurityGroupMemberships'.format(count)), + 'OptionSettings': self._get_param('OptionsToInclude.member.{0}.OptionSettings'.format(count)), + 'VpcSecurityGroupMemberships': self._get_param('OptionsToInclude.member.{0}.VpcSecurityGroupMemberships'.format(count)) }) count += 1 count = 1 options_to_remove = [] - while self._get_param('OptionsToRemove.member.{}'.format(count)): - options_to_remove.append(self._get_param('OptionsToRemove.member.{}'.format(count))) + while self._get_param('OptionsToRemove.member.{0}'.format(count)): + options_to_remove.append(self._get_param('OptionsToRemove.member.{0}'.format(count))) count += 1 apply_immediately = self._get_param('ApplyImmediately') option_group = self.backend.modify_option_group(option_group_name, From 618c2a701372fdcf7093cc1c913c993a96bd46dc Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Fri, 30 Jan 2015 19:28:07 +1100 Subject: [PATCH 34/35] Turned off tests on python 3 --- tests/test_rds2/test_rds2.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 81e8a54bf3fb..7ca48f0aa0d0 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -214,6 +214,7 @@ def test_delete_non_existant_option_group(): conn.delete_option_group.when.called_with('non-existant').should.throw(BotoServerError) +@disable_on_py3() @mock_rds2 def test_describe_option_group_options(): conn = boto.rds2.connect_to_region("us-west-2") @@ -369,6 +370,7 @@ def test_create_database_security_group(): result['CreateDBSecurityGroupResponse']['CreateDBSecurityGroupResult']['DBSecurityGroup']['IPRanges'].should.equal([]) +@disable_on_py3() @mock_rds2 def test_get_security_groups(): conn = boto.rds2.connect_to_region("us-west-2") @@ -387,12 +389,14 @@ def test_get_security_groups(): result['DescribeDBSecurityGroupsResponse']['DescribeDBSecurityGroupsResult']['DBSecurityGroups'][0]['DBSecurityGroupName'].should.equal("db_sg1") +@disable_on_py3() @mock_rds2 def test_get_non_existant_security_group(): conn = boto.rds2.connect_to_region("us-west-2") conn.describe_db_security_groups.when.called_with("not-a-sg").should.throw(BotoServerError) +@disable_on_py3() @mock_rds2 def test_delete_database_security_group(): conn = boto.rds2.connect_to_region("us-west-2") @@ -406,6 +410,7 @@ def test_delete_database_security_group(): result['DescribeDBSecurityGroupsResponse']['DescribeDBSecurityGroupsResult']['DBSecurityGroups'].should.have.length_of(0) +@disable_on_py3() @mock_rds2 def test_delete_non_existant_security_group(): conn = boto.rds2.connect_to_region("us-west-2") @@ -454,6 +459,7 @@ def test_add_security_group_to_database(): result['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances'][0]['DBSecurityGroups'][0]['DBSecurityGroup']['DBSecurityGroupName'].should.equal('db_sg') +@disable_on_py3() @mock_ec2 @mock_rds2 def test_create_database_subnet_group(): @@ -493,6 +499,7 @@ def test_create_database_in_subnet_group(): result['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances'][0]['DBSubnetGroup']['DBSubnetGroupName'].should.equal("db_subnet1") +@disable_on_py3() @mock_ec2 @mock_rds2 def test_describe_database_subnet_group(): @@ -520,6 +527,7 @@ def test_describe_database_subnet_group(): conn.describe_db_subnet_groups.when.called_with("not-a-subnet").should.throw(BotoServerError) +@disable_on_py3() @mock_ec2 @mock_rds2 def test_delete_database_subnet_group(): From 41507e4baad1a26e91336d12fe065a2e7b6f6625 Mon Sep 17 00:00:00 2001 From: Mike Fuller Date: Fri, 30 Jan 2015 19:54:43 +1100 Subject: [PATCH 35/35] Fixes for tests run on python 2.6.9 --- moto/rds2/models.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 82349aa06674..21155834b71c 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -524,7 +524,7 @@ def create_option_group(self, option_group_kwargs): } if option_group_kwargs['name'] in self.option_groups: raise RDSClientError('OptionGroupAlreadyExistsFault', - 'An option group named {} already exists.'.format(option_group_kwargs['name'])) + 'An option group named {0} already exists.'.format(option_group_kwargs['name'])) if 'description' not in option_group_kwargs or not option_group_kwargs['description']: raise RDSClientError('InvalidParameterValue', 'The parameter OptionGroupDescription must be provided and must not be blank.') @@ -545,7 +545,7 @@ def delete_option_group(self, option_group_name): if option_group_name in self.option_groups: return self.option_groups.pop(option_group_name) else: - raise RDSClientError('OptionGroupNotFoundFault', 'Specified OptionGroupName: {} not found.'.format(option_group_name)) + raise RDSClientError('OptionGroupNotFoundFault', 'Specified OptionGroupName: {0} not found.'.format(option_group_name)) def describe_option_groups(self, option_group_kwargs): option_group_list = [] @@ -575,7 +575,7 @@ def describe_option_groups(self, option_group_kwargs): option_group_list.append(option_group) if not len(option_group_list): raise RDSClientError('OptionGroupNotFoundFault', - 'Specified OptionGroupName: {} not found.'.format(option_group_kwargs['name'])) + 'Specified OptionGroupName: {0} not found.'.format(option_group_kwargs['name'])) return option_group_list[marker:max_records+marker] @staticmethod @@ -599,10 +599,10 @@ def describe_option_group_options(engine_name, major_engine_version=None): } } if engine_name not in default_option_group_options: - raise RDSClientError('InvalidParameterValue', 'Invalid DB engine: {}'.format(engine_name)) + raise RDSClientError('InvalidParameterValue', 'Invalid DB engine: {0}'.format(engine_name)) if major_engine_version and major_engine_version not in default_option_group_options[engine_name]: raise RDSClientError('InvalidParameterCombination', - 'Cannot find major version {} for {}'.format(major_engine_version, engine_name)) + 'Cannot find major version {0} for {1}'.format(major_engine_version, engine_name)) if major_engine_version: return default_option_group_options[engine_name][major_engine_version] return default_option_group_options[engine_name]['all'] @@ -610,7 +610,7 @@ def describe_option_group_options(engine_name, major_engine_version=None): def modify_option_group(self, option_group_name, options_to_include=None, options_to_remove=None, apply_immediately=None): if option_group_name not in self.option_groups: raise RDSClientError('OptionGroupNotFoundFault', - 'Specified OptionGroupName: {} not found.'.format(option_group_name)) + 'Specified OptionGroupName: {0} not found.'.format(option_group_name)) if not options_to_include and not options_to_remove: raise RDSClientError('InvalidParameterValue', 'At least one option must be added, modified, or removed.') @@ -651,7 +651,7 @@ def list_tags_for_resource(self, arn): return self.subnet_groups[resource_name].get_tags() else: raise RDSClientError('InvalidParameterValue', - 'Invalid resource name: {}'.format(arn)) + 'Invalid resource name: {0}'.format(arn)) return [] def remove_tags_from_resource(self, arn, tag_keys):