diff --git a/moto/backends.py b/moto/backends.py index 1f8fa4a507b9..b05d30a79df6 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -94,7 +94,6 @@ from moto.meteringmarketplace.models import MeteringMarketplaceBackend from moto.moto_api._internal.models import MotoAPIBackend from moto.mq.models import MQBackend - from moto.neptune.models import NeptuneBackend from moto.networkmanager.models import NetworkManagerBackend from moto.opensearch.models import OpenSearchServiceBackend from moto.opensearchserverless.models import OpenSearchServiceServerlessBackend @@ -147,13 +146,18 @@ from moto.xray.models import XRayBackend -ALT_SERVICE_NAMES = {"lambda": "awslambda", "moto_api": "moto_api._internal"} +ALT_SERVICE_NAMES = { + "lambda": "awslambda", + "moto_api": "moto_api._internal", + "neptune": "rds", +} ALT_BACKEND_NAMES = { "moto_api._internal": "moto_api", "awslambda": "lambda", "awslambda_simple": "lambda_simple", "dynamodb_v20111205": "dynamodb", "elasticbeanstalk": "eb", + "neptune": "rds", } @@ -556,7 +560,7 @@ def get_backend(name: "Literal['moto_api']") -> "BackendDict[MotoAPIBackend]": . @overload def get_backend(name: "Literal['mq']") -> "BackendDict[MQBackend]": ... @overload -def get_backend(name: "Literal['neptune']") -> "BackendDict[NeptuneBackend]": ... +def get_backend(name: "Literal['neptune']") -> "BackendDict[RDSBackend]": ... @overload def get_backend( name: "Literal['networkmanager']", diff --git a/moto/neptune/README.md b/moto/neptune/README.md new file mode 100644 index 000000000000..abedabe61565 --- /dev/null +++ b/moto/neptune/README.md @@ -0,0 +1,3 @@ +Neptune service is handled by the RDS service backend located [here](../rds). + +Neptune service has a dedicated test suite located [here](../../tests/test_neptune). \ No newline at end of file diff --git a/moto/neptune/__init__.py b/moto/neptune/__init__.py deleted file mode 100644 index e7353bb834b8..000000000000 --- a/moto/neptune/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -""" -Neptune is a bit of an odd duck. -It shares almost everything with RDS: the endpoint URL, and the features. Only the parameters to these features can be different. - -Because the endpoint URL is the same (rds.amazonaws.com), every request is intercepted by the RDS service. -RDS then has to determine whether any incoming call was meant for RDS, or for neptune. -""" - -from .models import neptune_backends # noqa: F401 diff --git a/moto/neptune/exceptions.py b/moto/neptune/exceptions.py deleted file mode 100644 index c2958b0e8106..000000000000 --- a/moto/neptune/exceptions.py +++ /dev/null @@ -1,27 +0,0 @@ -from jinja2 import Template - -from moto.core.exceptions import RESTError - - -class NeptuneClientError(RESTError): - def __init__(self, code: str, message: str): - super().__init__(error_type=code, message=message) - template = Template( - """ - - - {{ code }} - {{ message }} - Sender - - 6876f774-7273-11e4-85dc-39e55ca848d1 - """ - ) - self.description = template.render(code=code, message=message) - - -class DBClusterNotFoundError(NeptuneClientError): - def __init__(self, cluster_identifier: str): - super().__init__( - "DBClusterNotFoundFault", f"DBCluster {cluster_identifier} not found." - ) diff --git a/moto/neptune/models.py b/moto/neptune/models.py deleted file mode 100644 index ee11a5686ffe..000000000000 --- a/moto/neptune/models.py +++ /dev/null @@ -1,377 +0,0 @@ -import copy -import string -from typing import Any, Dict, List, Optional - -from jinja2 import Template - -from moto.core.base_backend import BackendDict, BaseBackend -from moto.core.common_models import BaseModel -from moto.core.utils import iso_8601_datetime_with_milliseconds -from moto.moto_api._internal import mock_random as random -from moto.utilities.utils import get_partition, load_resource - -from .exceptions import DBClusterNotFoundError - - -class GlobalCluster(BaseModel): - def __init__( - self, - account_id: str, - region_name: str, - global_cluster_identifier: str, - engine: Optional[str], - engine_version: Optional[str], - storage_encrypted: Optional[str], - deletion_protection: Optional[str], - ): - self.global_cluster_identifier = global_cluster_identifier - self.global_cluster_resource_id = "cluster-" + random.get_random_hex(8) - self.global_cluster_arn = f"arn:{get_partition(region_name)}:rds::{account_id}:global-cluster:{global_cluster_identifier}" - self.engine = engine or "neptune" - self.engine_version = engine_version or "1.2.0.0" - self.storage_encrypted = ( - storage_encrypted and storage_encrypted.lower() == "true" - ) - self.deletion_protection = ( - deletion_protection and deletion_protection.lower() == "true" - ) - - def to_xml(self) -> str: - template = Template( - """ - {{ cluster.global_cluster_identifier }} - {{ cluster.global_cluster_resource_id }} - {{ cluster.global_cluster_arn }} - {{ cluster.engine }} - available - {{ cluster.engine_version }} - {{ 'true' if cluster.storage_encrypted else 'false' }} - {{ 'true' if cluster.deletion_protection else 'false' }}""" - ) - return template.render(cluster=self) - - -class DBCluster(BaseModel): - def __init__( - self, - account_id: str, - region_name: str, - db_cluster_identifier: str, - database_name: Optional[str], - tags: List[Dict[str, str]], - storage_encrypted: str, - parameter_group_name: str, - engine: str, - engine_version: str, - kms_key_id: Optional[str], - preferred_maintenance_window: Optional[str], - preferred_backup_window: Optional[str], - backup_retention_period: Optional[int], - port: Optional[int], - serverless_v2_scaling_configuration: Optional[Dict[str, int]], - ): - self.account_id = account_id - self.region_name = region_name - self.db_cluster_identifier = db_cluster_identifier - self.resource_id = "cluster-" + random.get_random_hex(8) - self.tags = tags - self.storage_encrypted = storage_encrypted.lower() != "false" - self.db_cluster_parameter_group_name = parameter_group_name - self.engine = engine - self.engine_version = engine_version - self.database_name = database_name - self.db_subnet_group = "default" - self.status = "available" - self.backup_retention_period = backup_retention_period - self.cluster_create_time = iso_8601_datetime_with_milliseconds() - self.url_identifier = "".join( - random.choice(string.ascii_lowercase + string.digits) for _ in range(12) - ) - self.endpoint = f"{self.db_cluster_identifier}.cluster-{self.url_identifier}.{self.region_name}.neptune.amazonaws.com" - self.reader_endpoint = f"{self.db_cluster_identifier}.cluster-ro-{self.url_identifier}.{self.region_name}.neptune.amazonaws.com" - self.resource_id = "cluster-" + "".join( - random.choice(string.ascii_uppercase + string.digits) for _ in range(26) - ) - self.hosted_zone_id = "".join( - random.choice(string.ascii_uppercase + string.digits) for _ in range(14) - ) - self.kms_key_id = kms_key_id or ( - "default_kms_key_id" if self.storage_encrypted else None - ) - self.preferred_maintenance_window = preferred_maintenance_window - self.preferred_backup_window = preferred_backup_window - self.port = port - self.availability_zones = [ - f"{self.region_name}a", - f"{self.region_name}b", - f"{self.region_name}c", - ] - self.serverless_v2_scaling_configuration = serverless_v2_scaling_configuration - - @property - def db_cluster_arn(self) -> str: - return f"arn:{get_partition(self.region_name)}:rds:{self.region_name}:{self.account_id}:cluster:{self.db_cluster_identifier}" - - def get_tags(self) -> List[Dict[str, str]]: - return self.tags - - def add_tags(self, tags: List[Dict[str, str]]) -> List[Dict[str, str]]: - 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: List[str]) -> None: - self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in tag_keys] - - def to_xml(self) -> str: - template = Template( - """ - {% if cluster.allocated_storage %} - {{ cluster.allocated_storage }} - {% endif %} - - {% for zone in cluster.availability_zones %} - {{ zone }} - {% endfor %} - - {% if cluster.backup_retention_period %} - {{ cluster.backup_retention_period }} - {% endif %} - {% if cluster.character_set_name %} - {{ cluster.character_set_name }} - {% endif %} - {% if cluster.database_name %} - {{ cluster.database_name }} - {% endif %} - {{ cluster.db_cluster_identifier }} - {{ cluster.db_cluster_parameter_group_name }} - {{ cluster.db_subnet_group }} - {{ cluster.status }} - {{ cluster.percent_progress }} - {% if cluster.earliest_restorable_time %} - {{ cluster.earliest_restorable_time }} - {% endif %} - {{ cluster.endpoint }} - {{ cluster.reader_endpoint }} - false - {{ cluster.engine }} - {{ cluster.engine_version }} - {% if cluster.latest_restorable_time %} - {{ cluster.latest_restorable_time }} - {% endif %} - {% if cluster.port %} - {{ cluster.port }} - {% endif %} - {{ cluster.master_username }} - -{% for dbclusteroptiongroupmembership in cluster.dbclusteroptiongroupmemberships %} - - {{ dbclusteroptiongroupmembership.db_cluster_option_group_name }} - {{ dbclusteroptiongroupmembership.status }} - -{% endfor %} - - {{ cluster.preferred_backup_window }} - {{ cluster.preferred_maintenance_window }} - {{ cluster.replication_source_identifier }} - -{% for readreplicaidentifier in cluster.readreplicaidentifiers %} - -{% endfor %} - - -{% for dbclustermember in cluster.dbclustermembers %} - - {{ dbclustermember.db_instance_identifier }} - {{ dbclustermember.is_cluster_writer }} - {{ dbclustermember.db_cluster_parameter_group_status }} - {{ dbclustermember.promotion_tier }} - -{% endfor %} - - -{% for vpcsecuritygroup in cluster.vpcsecuritygroups %} - - {{ vpcsecuritygroup.vpc_security_group_id }} - {{ vpcsecuritygroup.status }} - -{% endfor %} - - {{ cluster.hosted_zone_id }} - {{ 'true' if cluster.storage_encrypted else 'false'}} - {{ cluster.kms_key_id }} - {{ cluster.resource_id }} - {{ cluster.db_cluster_arn }} - -{% for associatedrole in cluster.associatedroles %} - - {{ associatedrole.role_arn }} - {{ associatedrole.status }} - {{ associatedrole.feature_name }} - -{% endfor %} - - false - {{ cluster.clone_group_id }} - {{ cluster.cluster_create_time }} - false - -{% for enabledcloudwatchlogsexport in cluster.enabledcloudwatchlogsexports %} - db_cluster_arn -{% endfor %} - - false - false - {% if cluster.automatic_restart_time %} - {{ cluster.automatic_restart_time }} - {% endif %} - {% if cluster.serverless_v2_scaling_configuration %} - - {{ cluster.serverless_v2_scaling_configuration["MinCapacity"] }} - {{ cluster.serverless_v2_scaling_configuration["MaxCapacity"] }} - - {% endif %} - """ - ) - return template.render(cluster=self) - - -class NeptuneBackend(BaseBackend): - """Implementation of Neptune APIs.""" - - def __init__(self, region_name: str, account_id: str): - super().__init__(region_name, account_id) - self.clusters: Dict[str, DBCluster] = dict() - self.global_clusters: Dict[str, GlobalCluster] = dict() - self._db_cluster_options: Optional[List[Dict[str, Any]]] = None - - @property - def global_backend(self) -> "NeptuneBackend": - return neptune_backends[self.account_id]["us-east-1"] - - @property - def db_cluster_options(self) -> List[Dict[str, Any]]: # type: ignore[misc] - if self._db_cluster_options is None: - from moto.rds.utils import decode_orderable_db_instance - - decoded_options: List[Dict[str, Any]] = load_resource( - __name__, "../rds/resources/cluster_options/neptune.json" - ) - self._db_cluster_options = [ - decode_orderable_db_instance(option) for option in decoded_options - ] - return self._db_cluster_options - - def create_db_cluster(self, **kwargs: Any) -> DBCluster: - cluster = DBCluster( - account_id=self.account_id, - region_name=self.region_name, - db_cluster_identifier=kwargs["db_cluster_identifier"], - database_name=kwargs.get("database_name"), - storage_encrypted=kwargs.get("storage_encrypted", True), - parameter_group_name=kwargs.get("db_cluster_parameter_group_name") or "", - tags=kwargs.get("tags", []), - engine=kwargs.get("engine", "neptune"), - engine_version=kwargs.get("engine_version") or "1.2.0.2", - kms_key_id=kwargs.get("kms_key_id"), - preferred_maintenance_window=kwargs.get("preferred_maintenance_window") - or "none", - preferred_backup_window=kwargs.get("preferred_backup_window"), - backup_retention_period=kwargs.get("backup_retention_period") or 1, - port=kwargs.get("port") or 8192, - serverless_v2_scaling_configuration=kwargs.get( - "serverless_v2_scaling_configuration" - ), - ) - self.clusters[cluster.db_cluster_identifier] = cluster - return cluster - - def create_global_cluster( - self, - global_cluster_identifier: str, - engine: Optional[str], - engine_version: Optional[str], - storage_encrypted: Optional[str], - deletion_protection: Optional[str], - ) -> GlobalCluster: - cluster = GlobalCluster( - account_id=self.account_id, - region_name=self.region_name, - global_cluster_identifier=global_cluster_identifier, - engine=engine, - engine_version=engine_version, - storage_encrypted=storage_encrypted, - deletion_protection=deletion_protection, - ) - self.global_backend.global_clusters[global_cluster_identifier] = cluster - return cluster - - def delete_global_cluster(self, global_cluster_identifier: str) -> GlobalCluster: - return self.global_backend.global_clusters.pop(global_cluster_identifier) - - def describe_global_clusters(self) -> List[GlobalCluster]: - return list(self.global_backend.global_clusters.values()) - - def describe_db_clusters(self, db_cluster_identifier: str) -> List[DBCluster]: - """ - Pagination and the Filters-argument is not yet implemented - """ - if db_cluster_identifier: - if db_cluster_identifier not in self.clusters: - raise DBClusterNotFoundError(db_cluster_identifier) - return [self.clusters[db_cluster_identifier]] - return list(self.clusters.values()) - - def delete_db_cluster(self, cluster_identifier: str) -> DBCluster: - """ - The parameters SkipFinalSnapshot and FinalDBSnapshotIdentifier are not yet implemented. - The DeletionProtection-attribute is not yet enforced - """ - if cluster_identifier in self.clusters: - return self.clusters.pop(cluster_identifier) - raise DBClusterNotFoundError(cluster_identifier) - - def modify_db_cluster(self, kwargs: Any) -> DBCluster: - cluster_id = kwargs["db_cluster_identifier"] - - cluster = self.clusters[cluster_id] - del self.clusters[cluster_id] - - kwargs["db_cluster_identifier"] = kwargs.pop("new_db_cluster_identifier") - for k, v in kwargs.items(): - if v is not None: - setattr(cluster, k, v) - - cluster_id = kwargs.get("new_db_cluster_identifier", cluster_id) - self.clusters[cluster_id] = cluster - - initial_state = copy.deepcopy(cluster) # Return status=creating - cluster.status = "available" # Already set the final status in the background - return initial_state - - def start_db_cluster(self, cluster_identifier: str) -> DBCluster: - if cluster_identifier not in self.clusters: - raise DBClusterNotFoundError(cluster_identifier) - cluster = self.clusters[cluster_identifier] - temp_state = copy.deepcopy(cluster) - temp_state.status = "started" - cluster.status = "available" # This is the final status - already setting it in the background - return temp_state - - def describe_orderable_db_instance_options( - self, engine_version: Optional[str] - ) -> List[Dict[str, Any]]: - """ - Only the EngineVersion-parameter is currently implemented. - """ - if engine_version: - return [ - option - for option in self.db_cluster_options - if option["EngineVersion"] == engine_version - ] - return self.db_cluster_options - - -neptune_backends = BackendDict(NeptuneBackend, "neptune") diff --git a/moto/neptune/responses.py b/moto/neptune/responses.py deleted file mode 100644 index d9b57532a819..000000000000 --- a/moto/neptune/responses.py +++ /dev/null @@ -1,194 +0,0 @@ -from moto.core.responses import BaseResponse - -from .models import NeptuneBackend, neptune_backends - - -class NeptuneResponse(BaseResponse): - """Handler for Neptune requests and responses.""" - - def __init__(self) -> None: - super().__init__(service_name="neptune") - - @property - def neptune_backend(self) -> NeptuneBackend: - """Return backend instance specific for this region.""" - return neptune_backends[self.current_account][self.region] - - @property - def global_backend(self) -> NeptuneBackend: - """Return backend instance of the region that stores Global Clusters""" - return neptune_backends[self.current_account]["us-east-1"] - - def create_db_cluster(self) -> str: - params = self._get_params() - availability_zones = params.get("AvailabilityZones") - backup_retention_period = params.get("BackupRetentionPeriod") - character_set_name = params.get("CharacterSetName") - copy_tags_to_snapshot = params.get("CopyTagsToSnapshot") - database_name = params.get("DatabaseName") - db_cluster_identifier = params.get("DBClusterIdentifier") - db_cluster_parameter_group_name = params.get("DBClusterParameterGroupName") - vpc_security_group_ids = params.get("VpcSecurityGroupIds") - db_subnet_group_name = params.get("DBSubnetGroupName") - engine = params.get("Engine") - engine_version = params.get("EngineVersion") - port = params.get("Port") - master_username = params.get("MasterUsername") - master_user_password = params.get("MasterUserPassword") - option_group_name = params.get("OptionGroupName") - preferred_backup_window = params.get("PreferredBackupWindow") - preferred_maintenance_window = params.get("PreferredMaintenanceWindow") - replication_source_identifier = params.get("ReplicationSourceIdentifier") - tags = (self._get_multi_param_dict("Tags") or {}).get("Tag", []) - storage_encrypted = params.get("StorageEncrypted", "") - kms_key_id = params.get("KmsKeyId") - pre_signed_url = params.get("PreSignedUrl") - enable_iam_database_authentication = params.get( - "EnableIAMDatabaseAuthentication" - ) - enable_cloudwatch_logs_exports = params.get("EnableCloudwatchLogsExports") - deletion_protection = params.get("DeletionProtection") - serverless_v2_scaling_configuration = params.get( - "ServerlessV2ScalingConfiguration" - ) - global_cluster_identifier = params.get("GlobalClusterIdentifier") - source_region = params.get("SourceRegion") - db_cluster = self.neptune_backend.create_db_cluster( - availability_zones=availability_zones, - backup_retention_period=backup_retention_period, - character_set_name=character_set_name, - copy_tags_to_snapshot=copy_tags_to_snapshot, - database_name=database_name, - db_cluster_identifier=db_cluster_identifier, - db_cluster_parameter_group_name=db_cluster_parameter_group_name, - vpc_security_group_ids=vpc_security_group_ids, - db_subnet_group_name=db_subnet_group_name, - engine=engine, - engine_version=engine_version, - port=port, - master_username=master_username, - master_user_password=master_user_password, - option_group_name=option_group_name, - preferred_backup_window=preferred_backup_window, - preferred_maintenance_window=preferred_maintenance_window, - replication_source_identifier=replication_source_identifier, - tags=tags, - storage_encrypted=storage_encrypted, - kms_key_id=kms_key_id, - pre_signed_url=pre_signed_url, - enable_iam_database_authentication=enable_iam_database_authentication, - enable_cloudwatch_logs_exports=enable_cloudwatch_logs_exports, - deletion_protection=deletion_protection, - serverless_v2_scaling_configuration=serverless_v2_scaling_configuration, - global_cluster_identifier=global_cluster_identifier, - source_region=source_region, - ) - template = self.response_template(CREATE_DB_CLUSTER_TEMPLATE) - return template.render(cluster=db_cluster) - - def describe_db_clusters(self) -> str: - params = self._get_params() - db_cluster_identifier = params["DBClusterIdentifier"] - db_clusters = self.neptune_backend.describe_db_clusters( - db_cluster_identifier=db_cluster_identifier - ) - template = self.response_template(DESCRIBE_DB_CLUSTERS_TEMPLATE) - return template.render(db_clusters=db_clusters) - - def describe_global_clusters(self) -> str: - clusters = self.global_backend.describe_global_clusters() - template = self.response_template(DESCRIBE_GLOBAL_CLUSTERS_TEMPLATE) - return template.render(clusters=clusters) - - def create_global_cluster(self) -> str: - params = self._get_params() - cluster = self.global_backend.create_global_cluster( - global_cluster_identifier=params["GlobalClusterIdentifier"], - engine=params.get("Engine"), - engine_version=params.get("EngineVersion"), - storage_encrypted=params.get("StorageEncrypted"), - deletion_protection=params.get("DeletionProtection"), - ) - template = self.response_template(CREATE_GLOBAL_CLUSTER_TEMPLATE) - return template.render(cluster=cluster) - - def delete_global_cluster(self) -> str: - params = self._get_params() - cluster = self.global_backend.delete_global_cluster( - global_cluster_identifier=params["GlobalClusterIdentifier"], - ) - template = self.response_template(DELETE_GLOBAL_CLUSTER_TEMPLATE) - return template.render(cluster=cluster) - - -CREATE_DB_CLUSTER_TEMPLATE = """ - - 1549581b-12b7-11e3-895e-1334aEXAMPLE - - - {{ cluster.to_xml() }} - -""" - -DESCRIBE_DB_CLUSTERS_TEMPLATE = """ - - 1549581b-12b7-11e3-895e-1334aEXAMPLE - - - -{% for cluster in db_clusters %} - {{ cluster.to_xml() }} -{% endfor %} - - -""" - -CREATE_GLOBAL_CLUSTER_TEMPLATE = """ - - 1549581b-12b7-11e3-895e-1334aEXAMPLE - - - - {{ cluster.to_xml() }} - - -""" - -DELETE_GLOBAL_CLUSTER_TEMPLATE = """ - - 1549581b-12b7-11e3-895e-1334aEXAMPLE - - - - {{ cluster.to_xml() }} - - -""" - -DESCRIBE_GLOBAL_CLUSTERS_TEMPLATE = """ - - 1549581b-12b7-11e3-895e-1334aEXAMPLE - - - -{% for cluster in clusters %} - - {{ cluster.to_xml() }} - -{% endfor %} - - -""" - -REMOVE_FROM_GLOBAL_CLUSTER_TEMPLATE = """ - - 1549581b-12b7-11e3-895e-1334aEXAMPLE - - - {% if cluster %} - - {{ cluster.to_xml() }} - - {% endif %} - -""" diff --git a/moto/neptune/urls.py b/moto/neptune/urls.py deleted file mode 100644 index bcdbcb2942b1..000000000000 --- a/moto/neptune/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -All calls to this service are intercepted by RDS -""" - -url_bases = [] # type: ignore[var-annotated] - - -url_paths = {} # type: ignore[var-annotated] diff --git a/moto/rds/models.py b/moto/rds/models.py index d22b188c946d..4e21129a9226 100644 --- a/moto/rds/models.py +++ b/moto/rds/models.py @@ -3,6 +3,7 @@ import re import string from collections import OrderedDict, defaultdict +from functools import lru_cache from re import compile as re_compile from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple, Union @@ -13,7 +14,6 @@ from moto.core.utils import iso_8601_datetime_with_milliseconds from moto.ec2.models import ec2_backends from moto.moto_api._internal import mock_random as random -from moto.neptune.models import NeptuneBackend, neptune_backends from moto.utilities.utils import ARN_PARTITION_REGEX, load_resource from .exceptions import ( @@ -137,7 +137,9 @@ def __init__( self.global_cluster_identifier = global_cluster_identifier self.global_cluster_resource_id = "cluster-" + random.get_random_hex(8) self.engine = engine - self.engine_version = engine_version or "5.7.mysql_aurora.2.11.2" + self.engine_version = engine_version or DBCluster.default_engine_version( + self.engine + ) self.storage_encrypted = ( storage_encrypted and storage_encrypted.lower() == "true" ) @@ -249,7 +251,11 @@ def __init__( ) self.master_username = kwargs.get("master_username") self.global_cluster_identifier = kwargs.get("global_cluster_identifier") - if not self.master_username and self.global_cluster_identifier: + if ( + not self.master_username + and self.global_cluster_identifier + or self.engine == "neptune" + ): pass elif not self.master_username: raise InvalidParameterValue( @@ -265,7 +271,10 @@ def __init__( f"{self.region}b", f"{self.region}c", ] - self.parameter_group = kwargs.get("parameter_group") or "default.aurora8.0" + default_pg = ( + "default.neptune1.3" if self.engine == "neptune" else "default.aurora8.0" + ) + self.parameter_group = kwargs.get("parameter_group") or default_pg self.subnet_group = kwargs.get("db_subnet_group_name") or "default" self.url_identifier = "".join( random.choice(string.ascii_lowercase + string.digits) for _ in range(12) @@ -539,6 +548,7 @@ def default_engine_version(engine: str) -> str: "aurora-mysql": "5.7.mysql_aurora.2.07.2", "aurora-postgresql": "12.7", "mysql": "8.0.23", + "neptune": "1.3.2.1", "postgres": "13.4", }[engine] @@ -549,6 +559,7 @@ def default_port(engine: str) -> int: "aurora-mysql": 3306, "aurora-postgresql": 5432, "mysql": 3306, + "neptune": 8182, "postgres": 5432, }[engine] @@ -566,6 +577,7 @@ def default_allocated_storage(engine: str, storage_type: str) -> int: "aurora-mysql": {"gp2": 20, "io1": 100, "standard": 10}, "aurora-postgresql": {"gp2": 20, "io1": 100, "standard": 10}, "mysql": {"gp2": 20, "io1": 100, "standard": 5}, + "neptune": {"gp2": 0, "io1": 0, "standard": 0}, "postgres": {"gp2": 20, "io1": 100, "standard": 5}, }[engine][storage_type] @@ -1651,24 +1663,18 @@ def __init__(self, region_name: str, account_id: str): self.db_proxies: Dict[str, DBProxy] = OrderedDict() def reset(self) -> None: - self.neptune.reset() super().reset() - @property - def neptune(self) -> NeptuneBackend: - return neptune_backends[self.account_id][self.region_name] + @lru_cache() + def db_cluster_options(self, engine) -> List[Dict[str, Any]]: # type: ignore + from moto.rds.utils import decode_orderable_db_instance - @property - def db_cluster_options(self) -> List[Dict[str, Any]]: # type: ignore - if self._db_cluster_options is None: - from moto.rds.utils import decode_orderable_db_instance - - decoded_options = load_resource( - __name__, "resources/cluster_options/aurora-postgresql.json" - ) - self._db_cluster_options = [ - decode_orderable_db_instance(option) for option in decoded_options - ] + decoded_options = load_resource( + __name__, f"resources/cluster_options/{engine}.json" + ) + self._db_cluster_options = [ + decode_orderable_db_instance(option) for option in decoded_options + ] return self._db_cluster_options def create_db_instance(self, db_kwargs: Dict[str, Any]) -> DBInstance: @@ -2290,23 +2296,7 @@ def create_db_cluster(self, kwargs: Dict[str, Any]) -> DBCluster: cluster = DBCluster(self, **kwargs) self.clusters[cluster_id] = cluster - if ( - cluster.global_cluster_identifier - and cluster.global_cluster_identifier in self.global_clusters - ): - global_cluster = self.global_clusters[cluster.global_cluster_identifier] - - # Main DB cluster, does RW on global cluster - setattr(cluster, "is_writer", True) - # self.clusters[cluster_id] = cluster - global_cluster.members.append(cluster) - - # search all backend to check if global cluster named global_cluster_identifier exists - # anywhere else - if ( - cluster.global_cluster_identifier - and cluster.global_cluster_identifier not in self.global_clusters - ): + if cluster.global_cluster_identifier: for regional_backend in rds_backends[self.account_id]: if ( cluster.global_cluster_identifier @@ -2316,6 +2306,12 @@ def create_db_cluster(self, kwargs: Dict[str, Any]) -> DBCluster: regional_backend ].global_clusters[cluster.global_cluster_identifier] global_cluster.members.append(cluster) + if len(global_cluster.members) == 1: + # primary cluster + setattr(cluster, "is_writer", True) + else: + # secondary cluster(s) + setattr(cluster, "is_writer", False) if cluster.replication_source_identifier: cluster_identifier = cluster.replication_source_identifier @@ -2329,14 +2325,13 @@ def create_db_cluster(self, kwargs: Dict[str, Any]) -> DBCluster: def modify_db_cluster(self, kwargs: Dict[str, Any]) -> DBCluster: cluster_id = kwargs["db_cluster_identifier"] - if cluster_id in self.neptune.clusters: - return self.neptune.modify_db_cluster(kwargs) # type: ignore - cluster = self.clusters[cluster_id] del self.clusters[cluster_id] kwargs["db_cluster_identifier"] = kwargs.pop("new_db_cluster_identifier") for k, v in kwargs.items(): + if k == "db_cluster_parameter_group_name": + k = "parameter_group" if v is not None: setattr(cluster, k, v) @@ -2426,17 +2421,13 @@ def describe_db_clusters( self, cluster_identifier: Optional[str] = None, filters: Any = None ) -> List[DBCluster]: clusters = self.clusters - clusters_neptune = self.neptune.clusters if cluster_identifier: filters = merge_filters(filters, {"db-cluster-id": [cluster_identifier]}) if filters: clusters = self._filter_resources(clusters, filters, DBCluster) - clusters_neptune = self._filter_resources( - clusters_neptune, filters, DBCluster - ) - if cluster_identifier and not (clusters or clusters_neptune): + if cluster_identifier and not clusters: raise DBClusterNotFoundError(cluster_identifier) - return list(clusters.values()) + list(clusters_neptune.values()) # type: ignore + return list(clusters.values()) # type: ignore def describe_db_cluster_snapshots( self, @@ -2475,13 +2466,10 @@ def delete_db_cluster( if snapshot_name: self.create_auto_cluster_snapshot(cluster_identifier, snapshot_name) return self.clusters.pop(cluster_identifier) - if cluster_identifier in self.neptune.clusters: - return self.neptune.delete_db_cluster(cluster_identifier) # type: ignore raise DBClusterNotFoundError(cluster_identifier) def start_db_cluster(self, cluster_identifier: str) -> DBCluster: if cluster_identifier not in self.clusters: - return self.neptune.start_db_cluster(cluster_identifier) # type: ignore raise DBClusterNotFoundError(cluster_identifier) cluster = self.clusters[cluster_identifier] if cluster.status != "stopped": @@ -2605,8 +2593,6 @@ def list_tags_for_resource(self, arn: str) -> List[Dict[str, str]]: elif resource_type == "cluster": # Cluster if resource_name in self.clusters: return self.clusters[resource_name].get_tags() - if resource_name in self.neptune.clusters: - return self.neptune.clusters[resource_name].get_tags() elif resource_type == "es": # Event Subscription if resource_name in self.event_subscriptions: return self.event_subscriptions[resource_name].get_tags() @@ -2669,8 +2655,6 @@ def remove_tags_from_resource(self, arn: str, tag_keys: List[str]) -> None: elif resource_type == "cluster": if resource_name in self.clusters: self.clusters[resource_name].remove_tags(tag_keys) - if resource_name in self.neptune.clusters: - self.neptune.clusters[resource_name].remove_tags(tag_keys) elif resource_type == "cluster-snapshot": # DB Cluster Snapshot if resource_name in self.cluster_snapshots: self.cluster_snapshots[resource_name].remove_tags(tag_keys) @@ -2715,8 +2699,6 @@ def add_tags_to_resource( # type: ignore[return] elif resource_type == "cluster": if resource_name in self.clusters: return self.clusters[resource_name].add_tags(tags) - if resource_name in self.neptune.clusters: - return self.neptune.clusters[resource_name].add_tags(tags) elif resource_type == "cluster-snapshot": # DB Cluster Snapshot if resource_name in self.cluster_snapshots: return self.cluster_snapshots[resource_name].add_tags(tags) @@ -2771,16 +2753,14 @@ def describe_orderable_db_instance_options( """ Only the Aurora-Postgresql and Neptune-engine is currently implemented """ - if engine == "neptune": - return self.neptune.describe_orderable_db_instance_options(engine_version) - if engine == "aurora-postgresql": + if engine in ["aurora-postgresql", "neptune"]: if engine_version: return [ option - for option in self.db_cluster_options + for option in self.db_cluster_options(engine) if option["EngineVersion"] == engine_version ] - return self.db_cluster_options + return self.db_cluster_options(engine) return [] def create_db_cluster_parameter_group( @@ -2853,16 +2833,9 @@ def create_global_cluster( return global_cluster def describe_global_clusters(self) -> List[GlobalCluster]: - return ( - list(self.global_clusters.values()) - + self.neptune.describe_global_clusters() # type: ignore - ) + return list(self.global_clusters.values()) def delete_global_cluster(self, global_cluster_identifier: str) -> GlobalCluster: - try: - return self.neptune.delete_global_cluster(global_cluster_identifier) # type: ignore - except: # noqa: E722 Do not use bare except - pass # It's not a Neptune Global Cluster - assume it's an RDS cluster instead global_cluster = self.global_clusters[global_cluster_identifier] if global_cluster.members: raise InvalidGlobalClusterStateFault(global_cluster.global_cluster_arn) diff --git a/moto/rds/responses.py b/moto/rds/responses.py index 635851c89250..b39bd61cd0c0 100644 --- a/moto/rds/responses.py +++ b/moto/rds/responses.py @@ -1,42 +1,22 @@ from collections import defaultdict from typing import Any, Dict, Iterable, List -from moto.core.common_types import TYPE_RESPONSE from moto.core.responses import BaseResponse from moto.ec2.models import ec2_backends -from moto.neptune.responses import ( - CREATE_GLOBAL_CLUSTER_TEMPLATE, - DELETE_GLOBAL_CLUSTER_TEMPLATE, - DESCRIBE_GLOBAL_CLUSTERS_TEMPLATE, - REMOVE_FROM_GLOBAL_CLUSTER_TEMPLATE, - NeptuneResponse, -) from .exceptions import DBParameterGroupNotFoundError from .models import RDSBackend, rds_backends class RDSResponse(BaseResponse): - def __init__(self) -> None: - super().__init__(service_name="rds") - # Neptune and RDS share a HTTP endpoint RDS is the lucky guy that catches all requests - # So we have to determine whether we can handle an incoming request here, or whether it needs redirecting to Neptune - self.neptune = NeptuneResponse() - @property def backend(self) -> RDSBackend: return rds_backends[self.current_account][self.region] - def _dispatch(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE: - # Because some requests are send through to Neptune, we have to prepare the NeptuneResponse-class - self.neptune.setup_class(request, full_url, headers) - return super()._dispatch(request, full_url, headers) - - def __getattribute__(self, name: str) -> Any: - if name in ["create_db_cluster", "create_global_cluster"]: - if self._get_param("Engine") == "neptune": - return object.__getattribute__(self.neptune, name) - return object.__getattribute__(self, name) + @property + def global_backend(self) -> RDSBackend: + """Return backend instance of the region that stores Global Clusters""" + return rds_backends[self.current_account]["us-east-1"] def _get_db_kwargs(self) -> Dict[str, Any]: args = { @@ -756,13 +736,13 @@ def describe_orderable_db_instance_options(self) -> str: return template.render(options=options, marker=None) def describe_global_clusters(self) -> str: - clusters = self.backend.describe_global_clusters() + clusters = self.global_backend.describe_global_clusters() template = self.response_template(DESCRIBE_GLOBAL_CLUSTERS_TEMPLATE) return template.render(clusters=clusters) def create_global_cluster(self) -> str: params = self._get_params() - cluster = self.backend.create_global_cluster( + cluster = self.global_backend.create_global_cluster( global_cluster_identifier=params["GlobalClusterIdentifier"], source_db_cluster_identifier=params.get("SourceDBClusterIdentifier"), engine=params.get("Engine"), @@ -775,7 +755,7 @@ def create_global_cluster(self) -> str: def delete_global_cluster(self) -> str: params = self._get_params() - cluster = self.backend.delete_global_cluster( + cluster = self.global_backend.delete_global_cluster( global_cluster_identifier=params["GlobalClusterIdentifier"], ) template = self.response_template(DELETE_GLOBAL_CLUSTER_TEMPLATE) @@ -1699,3 +1679,53 @@ def create_db_proxy(self) -> str: """ + +CREATE_GLOBAL_CLUSTER_TEMPLATE = """ + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + + + + {{ cluster.to_xml() }} + + +""" + +DELETE_GLOBAL_CLUSTER_TEMPLATE = """ + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + + + + {{ cluster.to_xml() }} + + +""" + +DESCRIBE_GLOBAL_CLUSTERS_TEMPLATE = """ + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + + + +{% for cluster in clusters %} + + {{ cluster.to_xml() }} + +{% endfor %} + + +""" + +REMOVE_FROM_GLOBAL_CLUSTER_TEMPLATE = """ + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + + + {% if cluster %} + + {{ cluster.to_xml() }} + + {% endif %} + +""" diff --git a/moto/rds/utils.py b/moto/rds/utils.py index 41dabc435e66..ebb75d898edf 100644 --- a/moto/rds/utils.py +++ b/moto/rds/utils.py @@ -51,6 +51,7 @@ def valid_db_instance_engine(self) -> List[str]: class ClusterEngine(str, Enum): AURORA_POSTGRESQL = "aurora-postgresql" AURORA_MYSQL = "aurora-mysql" + NEPTUNE = "neptune" RDS_POSTGRESQL = "postgres" RDS_MYSQL = "mysql" diff --git a/tests/test_neptune/test_clusters.py b/tests/test_neptune/test_clusters.py index c1dfdd34377a..2d9376de1068 100644 --- a/tests/test_neptune/test_clusters.py +++ b/tests/test_neptune/test_clusters.py @@ -18,9 +18,9 @@ def test_create_db_cluster(): assert "DbClusterResourceId" in resp assert "DBClusterArn" in resp assert resp["Engine"] == "neptune" - assert resp["EngineVersion"] == "1.2.0.2" - assert resp["StorageEncrypted"] is True - assert resp["DBClusterParameterGroup"] == "" + assert "EngineVersion" in resp + assert resp["StorageEncrypted"] is False + assert resp["DBClusterParameterGroup"].startswith("default.neptune") assert "Endpoint" in resp assert "cluster-" in resp["DbClusterResourceId"] assert resp["AvailabilityZones"] == ["us-east-2a", "us-east-2b", "us-east-2c"] @@ -108,9 +108,7 @@ def test_modify_db_cluster(): @mock_aws def test_start_db_cluster(): client = boto3.client("neptune", region_name="us-east-2") - client.create_db_cluster(DBClusterIdentifier="cluster-id", Engine="neptune")[ - "DBCluster" - ] - + client.create_db_cluster(DBClusterIdentifier="cluster-id", Engine="neptune") + client.stop_db_cluster(DBClusterIdentifier="cluster-id") cluster = client.start_db_cluster(DBClusterIdentifier="cluster-id")["DBCluster"] assert cluster["Status"] == "started" diff --git a/tests/test_neptune/test_global_clusters.py b/tests/test_neptune/test_global_clusters.py index edf207ad124e..f8c8aa4688d7 100644 --- a/tests/test_neptune/test_global_clusters.py +++ b/tests/test_neptune/test_global_clusters.py @@ -19,7 +19,7 @@ def test_create_global_cluster(): assert "GlobalClusterResourceId" in resp assert "GlobalClusterArn" in resp assert resp["Engine"] == "neptune" - assert resp["EngineVersion"] == "1.2.0.0" + assert "EngineVersion" in resp assert resp["StorageEncrypted"] is False assert resp["DeletionProtection"] is False diff --git a/tests/test_rds/test_global_clusters.py b/tests/test_rds/test_global_clusters.py index be84baa71b97..d61208b5ef07 100644 --- a/tests/test_rds/test_global_clusters.py +++ b/tests/test_rds/test_global_clusters.py @@ -39,7 +39,7 @@ def test_global_cluster_members(): ) assert global_cluster["Status"] == "available" assert global_cluster["Engine"] == "aurora-mysql" - assert global_cluster["EngineVersion"] == "5.7.mysql_aurora.2.11.2" + assert "mysql_aurora" in global_cluster["EngineVersion"] assert global_cluster["StorageEncrypted"] is False assert global_cluster["DeletionProtection"] is False assert global_cluster["GlobalClusterMembers"] == []