378 lines
16 KiB
Python
378 lines
16 KiB
Python
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 load_resource
|
|
|
|
from .exceptions import DBClusterNotFoundError
|
|
|
|
|
|
class GlobalCluster(BaseModel):
|
|
def __init__(
|
|
self,
|
|
account_id: 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:aws: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(
|
|
"""
|
|
<GlobalClusterIdentifier>{{ cluster.global_cluster_identifier }}</GlobalClusterIdentifier>
|
|
<GlobalClusterResourceId>{{ cluster.global_cluster_resource_id }}</GlobalClusterResourceId>
|
|
<GlobalClusterArn>{{ cluster.global_cluster_arn }}</GlobalClusterArn>
|
|
<Engine>{{ cluster.engine }}</Engine>
|
|
<Status>available</Status>
|
|
<EngineVersion>{{ cluster.engine_version }}</EngineVersion>
|
|
<StorageEncrypted>{{ 'true' if cluster.storage_encrypted else 'false' }}</StorageEncrypted>
|
|
<DeletionProtection>{{ 'true' if cluster.deletion_protection else 'false' }}</DeletionProtection>"""
|
|
)
|
|
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:aws: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(
|
|
"""<DBCluster>
|
|
{% if cluster.allocated_storage %}
|
|
<AllocatedStorage>{{ cluster.allocated_storage }}</AllocatedStorage>
|
|
{% endif %}
|
|
<AvailabilityZones>
|
|
{% for zone in cluster.availability_zones %}
|
|
<AvailabilityZone>{{ zone }}</AvailabilityZone>
|
|
{% endfor %}
|
|
</AvailabilityZones>
|
|
{% if cluster.backup_retention_period %}
|
|
<BackupRetentionPeriod>{{ cluster.backup_retention_period }}</BackupRetentionPeriod>
|
|
{% endif %}
|
|
{% if cluster.character_set_name %}
|
|
<CharacterSetName>{{ cluster.character_set_name }}</CharacterSetName>
|
|
{% endif %}
|
|
{% if cluster.database_name %}
|
|
<DatabaseName>{{ cluster.database_name }}</DatabaseName>
|
|
{% endif %}
|
|
<DBClusterIdentifier>{{ cluster.db_cluster_identifier }}</DBClusterIdentifier>
|
|
<DBClusterParameterGroup>{{ cluster.db_cluster_parameter_group_name }}</DBClusterParameterGroup>
|
|
<DBSubnetGroup>{{ cluster.db_subnet_group }}</DBSubnetGroup>
|
|
<Status>{{ cluster.status }}</Status>
|
|
<PercentProgress>{{ cluster.percent_progress }}</PercentProgress>
|
|
{% if cluster.earliest_restorable_time %}
|
|
<EarliestRestorableTime>{{ cluster.earliest_restorable_time }}</EarliestRestorableTime>
|
|
{% endif %}
|
|
<Endpoint>{{ cluster.endpoint }}</Endpoint>
|
|
<ReaderEndpoint>{{ cluster.reader_endpoint }}</ReaderEndpoint>
|
|
<MultiAZ>false</MultiAZ>
|
|
<Engine>{{ cluster.engine }}</Engine>
|
|
<EngineVersion>{{ cluster.engine_version }}</EngineVersion>
|
|
{% if cluster.latest_restorable_time %}
|
|
<LatestRestorableTime>{{ cluster.latest_restorable_time }}</LatestRestorableTime>
|
|
{% endif %}
|
|
{% if cluster.port %}
|
|
<Port>{{ cluster.port }}</Port>
|
|
{% endif %}
|
|
<MasterUsername>{{ cluster.master_username }}</MasterUsername>
|
|
<DBClusterOptionGroupMemberships>
|
|
{% for dbclusteroptiongroupmembership in cluster.dbclusteroptiongroupmemberships %}
|
|
<member>
|
|
<DBClusterOptionGroupName>{{ dbclusteroptiongroupmembership.db_cluster_option_group_name }}</DBClusterOptionGroupName>
|
|
<Status>{{ dbclusteroptiongroupmembership.status }}</Status>
|
|
</member>
|
|
{% endfor %}
|
|
</DBClusterOptionGroupMemberships>
|
|
<PreferredBackupWindow>{{ cluster.preferred_backup_window }}</PreferredBackupWindow>
|
|
<PreferredMaintenanceWindow>{{ cluster.preferred_maintenance_window }}</PreferredMaintenanceWindow>
|
|
<ReplicationSourceIdentifier>{{ cluster.replication_source_identifier }}</ReplicationSourceIdentifier>
|
|
<ReadReplicaIdentifiers>
|
|
{% for readreplicaidentifier in cluster.readreplicaidentifiers %}
|
|
<member/>
|
|
{% endfor %}
|
|
</ReadReplicaIdentifiers>
|
|
<DBClusterMembers>
|
|
{% for dbclustermember in cluster.dbclustermembers %}
|
|
<member>
|
|
<DBInstanceIdentifier>{{ dbclustermember.db_instance_identifier }}</DBInstanceIdentifier>
|
|
<IsClusterWriter>{{ dbclustermember.is_cluster_writer }}</IsClusterWriter>
|
|
<DBClusterParameterGroupStatus>{{ dbclustermember.db_cluster_parameter_group_status }}</DBClusterParameterGroupStatus>
|
|
<PromotionTier>{{ dbclustermember.promotion_tier }}</PromotionTier>
|
|
</member>
|
|
{% endfor %}
|
|
</DBClusterMembers>
|
|
<VpcSecurityGroups>
|
|
{% for vpcsecuritygroup in cluster.vpcsecuritygroups %}
|
|
<member>
|
|
<VpcSecurityGroupId>{{ vpcsecuritygroup.vpc_security_group_id }}</VpcSecurityGroupId>
|
|
<Status>{{ vpcsecuritygroup.status }}</Status>
|
|
</member>
|
|
{% endfor %}
|
|
</VpcSecurityGroups>
|
|
<HostedZoneId>{{ cluster.hosted_zone_id }}</HostedZoneId>
|
|
<StorageEncrypted>{{ 'true' if cluster.storage_encrypted else 'false'}}</StorageEncrypted>
|
|
<KmsKeyId>{{ cluster.kms_key_id }}</KmsKeyId>
|
|
<DbClusterResourceId>{{ cluster.resource_id }}</DbClusterResourceId>
|
|
<DBClusterArn>{{ cluster.db_cluster_arn }}</DBClusterArn>
|
|
<AssociatedRoles>
|
|
{% for associatedrole in cluster.associatedroles %}
|
|
<member>
|
|
<RoleArn>{{ associatedrole.role_arn }}</RoleArn>
|
|
<Status>{{ associatedrole.status }}</Status>
|
|
<FeatureName>{{ associatedrole.feature_name }}</FeatureName>
|
|
</member>
|
|
{% endfor %}
|
|
</AssociatedRoles>
|
|
<IAMDatabaseAuthenticationEnabled>false</IAMDatabaseAuthenticationEnabled>
|
|
<CloneGroupId>{{ cluster.clone_group_id }}</CloneGroupId>
|
|
<ClusterCreateTime>{{ cluster.cluster_create_time }}</ClusterCreateTime>
|
|
<CopyTagsToSnapshot>false</CopyTagsToSnapshot>
|
|
<EnabledCloudwatchLogsExports>
|
|
{% for enabledcloudwatchlogsexport in cluster.enabledcloudwatchlogsexports %}
|
|
<member/>db_cluster_arn
|
|
{% endfor %}
|
|
</EnabledCloudwatchLogsExports>
|
|
<DeletionProtection>false</DeletionProtection>
|
|
<CrossAccountClone>false</CrossAccountClone>
|
|
{% if cluster.automatic_restart_time %}
|
|
<AutomaticRestartTime>{{ cluster.automatic_restart_time }}</AutomaticRestartTime>
|
|
{% endif %}
|
|
{% if cluster.serverless_v2_scaling_configuration %}
|
|
<ServerlessV2ScalingConfiguration>
|
|
<MinCapacity>{{ cluster.serverless_v2_scaling_configuration["MinCapacity"] }}</MinCapacity>
|
|
<MaxCapacity>{{ cluster.serverless_v2_scaling_configuration["MaxCapacity"] }}</MaxCapacity>
|
|
</ServerlessV2ScalingConfiguration>
|
|
{% endif %}
|
|
</DBCluster>"""
|
|
)
|
|
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,
|
|
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")
|