diff --git a/moto/ec2/models/vpcs.py b/moto/ec2/models/vpcs.py index 4c4b67f5f..299f17d4b 100644 --- a/moto/ec2/models/vpcs.py +++ b/moto/ec2/models/vpcs.py @@ -263,6 +263,22 @@ class VPC(TaggedEC2Resource, CloudFormationModel): # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc.html return "AWS::EC2::VPC" + def get_cfn_attribute(self, attribute_name: str) -> Any: + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException + + if attribute_name == "CidrBlock": + return self.cidr_block + elif attribute_name == "CidrBlockAssociations": + return self.cidr_block_association_set + elif attribute_name == "DefaultSecurityGroup": + sec_group = self.ec2_backend.get_security_group_from_name( + "default", vpc_id=self.id + ) + return sec_group.id + elif attribute_name == "VpcId": + return self.id + raise UnformattedGetAttTemplateException() + @classmethod def create_from_cloudformation_json( # type: ignore[misc] cls, diff --git a/moto/efs/models.py b/moto/efs/models.py index 33df67491..ce1419243 100644 --- a/moto/efs/models.py +++ b/moto/efs/models.py @@ -10,7 +10,7 @@ from copy import deepcopy from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union from moto.core.base_backend import BackendDict, BaseBackend -from moto.core.common_models import BaseModel, CloudFormationModel +from moto.core.common_models import CloudFormationModel from moto.core.utils import camelcase_to_underscores, underscores_to_camelcase from moto.ec2 import ec2_backends from moto.ec2.exceptions import InvalidSubnetIdError @@ -43,7 +43,7 @@ def _lookup_az_id(account_id: str, az_name: str) -> Optional[str]: return None -class AccessPoint(BaseModel): +class AccessPoint(CloudFormationModel): def __init__( self, account_id: str, @@ -84,6 +84,38 @@ class AccessPoint(BaseModel): "LifeCycleState": "available", } + @staticmethod + def cloudformation_type() -> str: + return "AWS::EFS::AccessPoint" + + @classmethod + def create_from_cloudformation_json( # type: ignore[misc] + cls, + resource_name: str, + cloudformation_json: Any, + account_id: str, + region_name: str, + **kwargs: Any, + ) -> "AccessPoint": + props = cloudformation_json["Properties"] + file_system_id = props["FileSystemId"] + posix_user = props.get("PosixUser", {}) + root_directory = props.get("RootDirectory", {}) + tags = props.get("AccessPointTags", []) + + backend: EFSBackend = efs_backends[account_id][region_name] + return backend.create_access_point( + resource_name, + file_system_id=file_system_id, + posix_user=posix_user, + root_directory=root_directory, + tags=tags, + ) + + def delete(self, account_id: str, region_name: str) -> None: + backend: EFSBackend = efs_backends[account_id][region_name] + backend.delete_access_point(self.access_point_id) + class FileSystem(CloudFormationModel): """A model for an EFS File System Volume.""" @@ -223,40 +255,36 @@ class FileSystem(CloudFormationModel): region_name: str, **kwargs: Any, ) -> "FileSystem": - # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-efs-filesystem.html - props = deepcopy(cloudformation_json["Properties"]) - props = {camelcase_to_underscores(k): v for k, v in props.items()} - if "file_system_tags" in props: - props["tags"] = props.pop("file_system_tags") - if "backup_policy" in props: - if "status" not in props["backup_policy"]: - raise ValueError("BackupPolicy must be of type BackupPolicy.") - status = props.pop("backup_policy")["status"] - if status not in ["ENABLED", "DISABLED"]: - raise ValueError(f'Invalid status: "{status}".') - props["backup"] = status == "ENABLED" - if "bypass_policy_lockout_safety_check" in props: - raise ValueError( - "BypassPolicyLockoutSafetyCheck not currently " - "supported by AWS Cloudformation." + props = cloudformation_json["Properties"] + performance_mode = props.get("PerformanceMode", "generalPurpose") + encrypted = props.get("Encrypted", False) + kms_key_id = props.get("KmsKeyId") + throughput_mode = props.get("ThroughputMode", "bursting") + provisioned_throughput_in_mibps = props.get("ProvisionedThroughputInMibps", 0) + availability_zone_name = props.get("AvailabilityZoneName") + backup = props.get("BackupPolicy", {}).get("Status") == "ENABLED" + tags = props.get("FileSystemTags", []) + + lifecycle_policies = props.get("LifecyclePolicies", []) + + backend: EFSBackend = efs_backends[account_id][region_name] + fs = backend.create_file_system( + resource_name, + performance_mode=performance_mode, + encrypted=encrypted, + kms_key_id=kms_key_id, + throughput_mode=throughput_mode, + provisioned_throughput_in_mibps=provisioned_throughput_in_mibps, + availability_zone_name=availability_zone_name, + backup=backup, + tags=tags, + ) + + if lifecycle_policies: + backend.put_lifecycle_configuration( + file_system_id=fs.file_system_id, policies=lifecycle_policies ) - - return efs_backends[account_id][region_name].create_file_system( - resource_name, **props - ) - - @classmethod - def update_from_cloudformation_json( # type: ignore[misc] - cls, - original_resource: Any, - new_resource_name: str, - cloudformation_json: Any, - account_id: str, - region_name: str, - ) -> None: - raise NotImplementedError( - "Update of EFS File System via cloudformation is not yet implemented." - ) + return fs @classmethod def delete_from_cloudformation_json( # type: ignore[misc] @@ -357,7 +385,6 @@ class MountTarget(CloudFormationModel): region_name: str, **kwargs: Any, ) -> "MountTarget": - # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-efs-mounttarget.html props = deepcopy(cloudformation_json["Properties"]) props = {camelcase_to_underscores(k): v for k, v in props.items()} return efs_backends[account_id][region_name].create_mount_target(**props) diff --git a/tests/test_efs/test_efs_cloudformation.py b/tests/test_efs/test_efs_cloudformation.py new file mode 100644 index 000000000..98b443380 --- /dev/null +++ b/tests/test_efs/test_efs_cloudformation.py @@ -0,0 +1,189 @@ +import json + +import boto3 + +from moto import mock_aws + +template_fs_simple = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "FileSystemResource": {"Type": "AWS::EFS::FileSystem", "Properties": {}}, + }, +} + + +template_complete = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "MountTargetVPC": { + "Type": "AWS::EC2::VPC", + "Properties": {"CidrBlock": "172.31.0.0/16"}, + }, + "MountTargetSubnetOne": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.31.1.0/24", + "VpcId": {"Ref": "MountTargetVPC"}, + "AvailabilityZone": "us-east-1a", + }, + }, + "MountTargetSubnetTwo": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.31.2.0/24", + "VpcId": {"Ref": "MountTargetVPC"}, + "AvailabilityZone": "us-east-1b", + }, + }, + "MountTargetSubnetThree": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.31.3.0/24", + "VpcId": {"Ref": "MountTargetVPC"}, + "AvailabilityZone": "us-east-1c", + }, + }, + "FileSystemResource": { + "Type": "AWS::EFS::FileSystem", + "Properties": { + "PerformanceMode": "maxIO", + "LifecyclePolicies": [ + {"TransitionToIA": "AFTER_30_DAYS"}, + {"TransitionToPrimaryStorageClass": "AFTER_1_ACCESS"}, + ], + "Encrypted": True, + "FileSystemTags": [{"Key": "Name", "Value": "TestFileSystem"}], + "FileSystemPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["elasticfilesystem:ClientMount"], + "Principal": { + "AWS": "arn:aws:iam::111122223333:role/EfsReadOnly" + }, + } + ], + }, + "BackupPolicy": {"Status": "ENABLED"}, + "KmsKeyId": {"Fn::GetAtt": ["key", "Arn"]}, + }, + }, + "key": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Version": "2012-10-17", + "Id": "key-default-1", + "Statement": [ + { + "Sid": "Allow administration of the key", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:aws:iam::", + {"Ref": "AWS::AccountId"}, + ":root", + ], + ] + } + }, + "Action": ["kms:*"], + "Resource": "*", + } + ], + } + }, + }, + "MountTargetResource1": { + "Type": "AWS::EFS::MountTarget", + "Properties": { + "FileSystemId": {"Ref": "FileSystemResource"}, + "SubnetId": {"Ref": "MountTargetSubnetOne"}, + "SecurityGroups": [ + {"Fn::GetAtt": ["MountTargetVPC", "DefaultSecurityGroup"]} + ], + }, + }, + "MountTargetResource2": { + "Type": "AWS::EFS::MountTarget", + "Properties": { + "FileSystemId": {"Ref": "FileSystemResource"}, + "SubnetId": {"Ref": "MountTargetSubnetTwo"}, + "SecurityGroups": [ + {"Fn::GetAtt": ["MountTargetVPC", "DefaultSecurityGroup"]} + ], + }, + }, + "MountTargetResource3": { + "Type": "AWS::EFS::MountTarget", + "Properties": { + "FileSystemId": {"Ref": "FileSystemResource"}, + "SubnetId": {"Ref": "MountTargetSubnetThree"}, + "SecurityGroups": [ + {"Fn::GetAtt": ["MountTargetVPC", "DefaultSecurityGroup"]} + ], + }, + }, + "AccessPointResource": { + "Type": "AWS::EFS::AccessPoint", + "Properties": { + "FileSystemId": {"Ref": "FileSystemResource"}, + "PosixUser": { + "Uid": "13234", + "Gid": "1322", + "SecondaryGids": ["1344", "1452"], + }, + "RootDirectory": { + "CreationInfo": { + "OwnerGid": "708798", + "OwnerUid": "7987987", + "Permissions": "0755", + }, + "Path": "/testcfn/abc", + }, + }, + }, + }, +} + + +@mock_aws +def test_simple_template(): + region = "us-east-1" + cf = boto3.client("cloudformation", region_name=region) + cf.create_stack(StackName="teststack", TemplateBody=json.dumps(template_fs_simple)) + + efs = boto3.client("efs", region) + fs = efs.describe_file_systems()["FileSystems"][0] + assert fs["PerformanceMode"] == "generalPurpose" + assert fs["Encrypted"] is False + assert fs["ThroughputMode"] == "bursting" + + +@mock_aws +def test_full_template(): + region = "us-east-1" + cf = boto3.client("cloudformation", region_name=region) + cf.create_stack(StackName="teststack", TemplateBody=json.dumps(template_complete)) + + efs = boto3.client("efs", region) + fs = efs.describe_file_systems()["FileSystems"][0] + fs_id = fs["FileSystemId"] + assert fs["Name"] == "TestFileSystem" + assert fs["KmsKeyId"] + + lc = efs.describe_lifecycle_configuration(FileSystemId=fs_id)["LifecyclePolicies"] + assert {"TransitionToIA": "AFTER_30_DAYS"} in lc + assert {"TransitionToPrimaryStorageClass": "AFTER_1_ACCESS"} in lc + + aps = efs.describe_access_points()["AccessPoints"][0] + assert aps["FileSystemId"] == fs_id + + cf.delete_stack(StackName="teststack") + + assert efs.describe_file_systems()["FileSystems"] == [] + assert efs.describe_access_points()["AccessPoints"] == []