EFS: CF support for FileSystems/AccessPoints (#7433)

This commit is contained in:
Bert Blommers 2024-03-06 22:23:06 +00:00 committed by GitHub
parent 4c65c32d40
commit 74ea84edb4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 268 additions and 36 deletions

View File

@ -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,

View File

@ -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)

View File

@ -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"] == []