EFS: CF support for FileSystems/AccessPoints (#7433)
This commit is contained in:
parent
4c65c32d40
commit
74ea84edb4
@ -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,
|
||||
|
@ -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)
|
||||
|
189
tests/test_efs/test_efs_cloudformation.py
Normal file
189
tests/test_efs/test_efs_cloudformation.py
Normal 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"] == []
|
Loading…
Reference in New Issue
Block a user