Merge pull request #54 from spulec/master

Merge upstream
This commit is contained in:
Bert Blommers 2020-08-01 19:49:04 +01:00 committed by GitHub
commit a3cd699af8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1947 additions and 372 deletions

View File

@ -6,7 +6,7 @@ from boto.ec2.blockdevicemapping import BlockDeviceType, BlockDeviceMapping
from moto.ec2.exceptions import InvalidInstanceIdError
from moto.compat import OrderedDict
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, BaseModel, CloudFormationModel
from moto.ec2 import ec2_backends
from moto.elb import elb_backends
from moto.elbv2 import elbv2_backends
@ -74,7 +74,7 @@ class FakeScalingPolicy(BaseModel):
)
class FakeLaunchConfiguration(BaseModel):
class FakeLaunchConfiguration(CloudFormationModel):
def __init__(
self,
name,
@ -127,6 +127,15 @@ class FakeLaunchConfiguration(BaseModel):
)
return config
@staticmethod
def cloudformation_name_type():
return "LaunchConfigurationName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html
return "AWS::AutoScaling::LaunchConfiguration"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -215,7 +224,7 @@ class FakeLaunchConfiguration(BaseModel):
return block_device_map
class FakeAutoScalingGroup(BaseModel):
class FakeAutoScalingGroup(CloudFormationModel):
def __init__(
self,
name,
@ -309,6 +318,15 @@ class FakeAutoScalingGroup(BaseModel):
tag["PropagateAtLaunch"] = bool_to_string[tag["PropagateAtLaunch"]]
return tags
@staticmethod
def cloudformation_name_type():
return "AutoScalingGroupName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-autoscalinggroup.html
return "AWS::AutoScaling::AutoScalingGroup"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -28,7 +28,7 @@ import requests.adapters
from boto3 import Session
from moto.awslambda.policy import Policy
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, CloudFormationModel
from moto.core.exceptions import RESTError
from moto.iam.models import iam_backend
from moto.iam.exceptions import IAMNotFoundException
@ -151,7 +151,7 @@ class _DockerDataVolumeContext:
raise # multiple processes trying to use same volume?
class LambdaFunction(BaseModel):
class LambdaFunction(CloudFormationModel):
def __init__(self, spec, region, validate_s3=True, version=1):
# required
self.region = region
@ -492,6 +492,15 @@ class LambdaFunction(BaseModel):
return result
@staticmethod
def cloudformation_name_type():
return "FunctionName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html
return "AWS::Lambda::Function"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -556,7 +565,7 @@ class LambdaFunction(BaseModel):
lambda_backends[region].delete_function(self.function_name)
class EventSourceMapping(BaseModel):
class EventSourceMapping(CloudFormationModel):
def __init__(self, spec):
# required
self.function_name = spec["FunctionName"]
@ -633,6 +642,15 @@ class EventSourceMapping(BaseModel):
lambda_backend = lambda_backends[region_name]
lambda_backend.delete_event_source_mapping(self.uuid)
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html
return "AWS::Lambda::EventSourceMapping"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -667,13 +685,22 @@ class EventSourceMapping(BaseModel):
esm.delete(region_name)
class LambdaVersion(BaseModel):
class LambdaVersion(CloudFormationModel):
def __init__(self, spec):
self.version = spec["Version"]
def __repr__(self):
return str(self.logical_resource_id)
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-version.html
return "AWS::Lambda::Version"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -13,7 +13,7 @@ import threading
import dateutil.parser
from boto3 import Session
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, BaseModel, CloudFormationModel
from moto.iam import iam_backends
from moto.ec2 import ec2_backends
from moto.ecs import ecs_backends
@ -42,7 +42,7 @@ def datetime2int(date):
return int(time.mktime(date.timetuple()))
class ComputeEnvironment(BaseModel):
class ComputeEnvironment(CloudFormationModel):
def __init__(
self,
compute_environment_name,
@ -76,6 +76,15 @@ class ComputeEnvironment(BaseModel):
def physical_resource_id(self):
return self.arn
@staticmethod
def cloudformation_name_type():
return "ComputeEnvironmentName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-batch-computeenvironment.html
return "AWS::Batch::ComputeEnvironment"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -95,7 +104,7 @@ class ComputeEnvironment(BaseModel):
return backend.get_compute_environment_by_arn(arn)
class JobQueue(BaseModel):
class JobQueue(CloudFormationModel):
def __init__(
self, name, priority, state, environments, env_order_json, region_name
):
@ -139,6 +148,15 @@ class JobQueue(BaseModel):
def physical_resource_id(self):
return self.arn
@staticmethod
def cloudformation_name_type():
return "JobQueueName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-batch-jobqueue.html
return "AWS::Batch::JobQueue"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -164,7 +182,7 @@ class JobQueue(BaseModel):
return backend.get_job_queue_by_arn(arn)
class JobDefinition(BaseModel):
class JobDefinition(CloudFormationModel):
def __init__(
self,
name,
@ -264,6 +282,15 @@ class JobDefinition(BaseModel):
def physical_resource_id(self):
return self.arn
@staticmethod
def cloudformation_name_type():
return "JobDefinitionName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-batch-jobdefinition.html
return "AWS::Batch::JobDefinition"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -6,31 +6,43 @@ import copy
import warnings
import re
from moto.autoscaling import models as autoscaling_models
from moto.awslambda import models as lambda_models
from moto.batch import models as batch_models
from moto.cloudwatch import models as cloudwatch_models
from moto.cognitoidentity import models as cognitoidentity_models
from moto.compat import collections_abc
from moto.datapipeline import models as datapipeline_models
from moto.dynamodb2 import models as dynamodb2_models
# This ugly section of imports is necessary because we
# build the list of CloudFormationModel subclasses using
# CloudFormationModel.__subclasses__(). However, if the class
# definition of a subclass hasn't been executed yet - for example, if
# the subclass's module hasn't been imported yet - then that subclass
# doesn't exist yet, and __subclasses__ won't find it.
# So we import here to populate the list of subclasses.
from moto.autoscaling import models as autoscaling_models # noqa
from moto.awslambda import models as awslambda_models # noqa
from moto.batch import models as batch_models # noqa
from moto.cloudwatch import models as cloudwatch_models # noqa
from moto.datapipeline import models as datapipeline_models # noqa
from moto.dynamodb2 import models as dynamodb2_models # noqa
from moto.ecr import models as ecr_models # noqa
from moto.ecs import models as ecs_models # noqa
from moto.elb import models as elb_models # noqa
from moto.elbv2 import models as elbv2_models # noqa
from moto.events import models as events_models # noqa
from moto.iam import models as iam_models # noqa
from moto.kinesis import models as kinesis_models # noqa
from moto.kms import models as kms_models # noqa
from moto.rds import models as rds_models # noqa
from moto.rds2 import models as rds2_models # noqa
from moto.redshift import models as redshift_models # noqa
from moto.route53 import models as route53_models # noqa
from moto.s3 import models as s3_models # noqa
from moto.sns import models as sns_models # noqa
from moto.sqs import models as sqs_models # noqa
# End ugly list of imports
from moto.ec2 import models as ec2_models
from moto.ecs import models as ecs_models
from moto.elb import models as elb_models
from moto.elbv2 import models as elbv2_models
from moto.events import models as events_models
from moto.iam import models as iam_models
from moto.kinesis import models as kinesis_models
from moto.kms import models as kms_models
from moto.rds import models as rds_models
from moto.rds2 import models as rds2_models
from moto.redshift import models as redshift_models
from moto.route53 import models as route53_models
from moto.s3 import models as s3_models, s3_backend
from moto.s3 import models as _, s3_backend # noqa
from moto.s3.utils import bucket_and_name_from_url
from moto.sns import models as sns_models
from moto.sqs import models as sqs_models
from moto.core import ACCOUNT_ID
from moto.core import ACCOUNT_ID, CloudFormationModel
from .utils import random_suffix
from .exceptions import (
ExportNotFound,
@ -40,105 +52,13 @@ from .exceptions import (
)
from boto.cloudformation.stack import Output
MODEL_MAP = {
"AWS::AutoScaling::AutoScalingGroup": autoscaling_models.FakeAutoScalingGroup,
"AWS::AutoScaling::LaunchConfiguration": autoscaling_models.FakeLaunchConfiguration,
"AWS::Batch::JobDefinition": batch_models.JobDefinition,
"AWS::Batch::JobQueue": batch_models.JobQueue,
"AWS::Batch::ComputeEnvironment": batch_models.ComputeEnvironment,
"AWS::DynamoDB::Table": dynamodb2_models.Table,
"AWS::Kinesis::Stream": kinesis_models.Stream,
"AWS::Lambda::EventSourceMapping": lambda_models.EventSourceMapping,
"AWS::Lambda::Function": lambda_models.LambdaFunction,
"AWS::Lambda::Version": lambda_models.LambdaVersion,
"AWS::EC2::EIP": ec2_models.ElasticAddress,
"AWS::EC2::Instance": ec2_models.Instance,
"AWS::EC2::InternetGateway": ec2_models.InternetGateway,
"AWS::EC2::NatGateway": ec2_models.NatGateway,
"AWS::EC2::NetworkInterface": ec2_models.NetworkInterface,
"AWS::EC2::Route": ec2_models.Route,
"AWS::EC2::RouteTable": ec2_models.RouteTable,
"AWS::EC2::SecurityGroup": ec2_models.SecurityGroup,
"AWS::EC2::SecurityGroupIngress": ec2_models.SecurityGroupIngress,
"AWS::EC2::SpotFleet": ec2_models.SpotFleetRequest,
"AWS::EC2::Subnet": ec2_models.Subnet,
"AWS::EC2::SubnetRouteTableAssociation": ec2_models.SubnetRouteTableAssociation,
"AWS::EC2::Volume": ec2_models.Volume,
"AWS::EC2::VolumeAttachment": ec2_models.VolumeAttachment,
"AWS::EC2::VPC": ec2_models.VPC,
"AWS::EC2::VPCGatewayAttachment": ec2_models.VPCGatewayAttachment,
"AWS::EC2::VPCPeeringConnection": ec2_models.VPCPeeringConnection,
"AWS::ECS::Cluster": ecs_models.Cluster,
"AWS::ECS::TaskDefinition": ecs_models.TaskDefinition,
"AWS::ECS::Service": ecs_models.Service,
"AWS::ElasticLoadBalancing::LoadBalancer": elb_models.FakeLoadBalancer,
"AWS::ElasticLoadBalancingV2::LoadBalancer": elbv2_models.FakeLoadBalancer,
"AWS::ElasticLoadBalancingV2::TargetGroup": elbv2_models.FakeTargetGroup,
"AWS::ElasticLoadBalancingV2::Listener": elbv2_models.FakeListener,
"AWS::Cognito::IdentityPool": cognitoidentity_models.CognitoIdentity,
"AWS::DataPipeline::Pipeline": datapipeline_models.Pipeline,
"AWS::IAM::InstanceProfile": iam_models.InstanceProfile,
"AWS::IAM::Role": iam_models.Role,
"AWS::KMS::Key": kms_models.Key,
"AWS::Logs::LogGroup": cloudwatch_models.LogGroup,
"AWS::RDS::DBInstance": rds_models.Database,
"AWS::RDS::DBSecurityGroup": rds_models.SecurityGroup,
"AWS::RDS::DBSubnetGroup": rds_models.SubnetGroup,
"AWS::RDS::DBParameterGroup": rds2_models.DBParameterGroup,
"AWS::Redshift::Cluster": redshift_models.Cluster,
"AWS::Redshift::ClusterParameterGroup": redshift_models.ParameterGroup,
"AWS::Redshift::ClusterSubnetGroup": redshift_models.SubnetGroup,
"AWS::Route53::HealthCheck": route53_models.HealthCheck,
"AWS::Route53::HostedZone": route53_models.FakeZone,
"AWS::Route53::RecordSet": route53_models.RecordSet,
"AWS::Route53::RecordSetGroup": route53_models.RecordSetGroup,
"AWS::SNS::Topic": sns_models.Topic,
"AWS::S3::Bucket": s3_models.FakeBucket,
"AWS::SQS::Queue": sqs_models.Queue,
"AWS::Events::Rule": events_models.Rule,
"AWS::Events::EventBus": events_models.EventBus,
}
UNDOCUMENTED_NAME_TYPE_MAP = {
"AWS::AutoScaling::AutoScalingGroup": "AutoScalingGroupName",
"AWS::AutoScaling::LaunchConfiguration": "LaunchConfigurationName",
"AWS::IAM::InstanceProfile": "InstanceProfileName",
}
# http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-name.html
# List of supported CloudFormation models
MODEL_LIST = CloudFormationModel.__subclasses__()
MODEL_MAP = {model.cloudformation_type(): model for model in MODEL_LIST}
NAME_TYPE_MAP = {
"AWS::ApiGateway::ApiKey": "Name",
"AWS::ApiGateway::Model": "Name",
"AWS::CloudWatch::Alarm": "AlarmName",
"AWS::DynamoDB::Table": "TableName",
"AWS::ElasticBeanstalk::Application": "ApplicationName",
"AWS::ElasticBeanstalk::Environment": "EnvironmentName",
"AWS::CodeDeploy::Application": "ApplicationName",
"AWS::CodeDeploy::DeploymentConfig": "DeploymentConfigName",
"AWS::CodeDeploy::DeploymentGroup": "DeploymentGroupName",
"AWS::Config::ConfigRule": "ConfigRuleName",
"AWS::Config::DeliveryChannel": "Name",
"AWS::Config::ConfigurationRecorder": "Name",
"AWS::ElasticLoadBalancing::LoadBalancer": "LoadBalancerName",
"AWS::ElasticLoadBalancingV2::LoadBalancer": "Name",
"AWS::ElasticLoadBalancingV2::TargetGroup": "Name",
"AWS::EC2::SecurityGroup": "GroupName",
"AWS::ElastiCache::CacheCluster": "ClusterName",
"AWS::ECR::Repository": "RepositoryName",
"AWS::ECS::Cluster": "ClusterName",
"AWS::Elasticsearch::Domain": "DomainName",
"AWS::Events::Rule": "Name",
"AWS::IAM::Group": "GroupName",
"AWS::IAM::ManagedPolicy": "ManagedPolicyName",
"AWS::IAM::Role": "RoleName",
"AWS::IAM::User": "UserName",
"AWS::Lambda::Function": "FunctionName",
"AWS::RDS::DBInstance": "DBInstanceIdentifier",
"AWS::S3::Bucket": "BucketName",
"AWS::SNS::Topic": "TopicName",
"AWS::SQS::Queue": "QueueName",
model.cloudformation_type(): model.cloudformation_name_type()
for model in MODEL_LIST
}
NAME_TYPE_MAP.update(UNDOCUMENTED_NAME_TYPE_MAP)
# Just ignore these models types for now
NULL_MODELS = [
@ -292,9 +212,11 @@ def clean_json(resource_json, resources_map):
def resource_class_from_type(resource_type):
if resource_type in NULL_MODELS:
return None
if resource_type not in MODEL_MAP:
logger.warning("No Moto CloudFormation support for %s", resource_type)
return None
return MODEL_MAP.get(resource_type)

View File

@ -3,7 +3,7 @@ import json
from boto3 import Session
from moto.core.utils import iso_8601_datetime_without_milliseconds
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, BaseModel, CloudFormationModel
from moto.core.exceptions import RESTError
from moto.logs import logs_backends
from datetime import datetime, timedelta
@ -490,13 +490,22 @@ class CloudWatchBackend(BaseBackend):
return None, metrics
class LogGroup(BaseModel):
class LogGroup(CloudFormationModel):
def __init__(self, spec):
# required
self.name = spec["LogGroupName"]
# optional
self.tags = spec.get("Tags", [])
@staticmethod
def cloudformation_name_type():
return "LogGroupName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-loggroup.html
return "AWS::Logs::LogGroup"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -1,6 +1,7 @@
from __future__ import unicode_literals
from .models import BaseModel, BaseBackend, moto_api_backend, ACCOUNT_ID # noqa
from .models import CloudFormationModel # noqa
from .responses import ActionAuthenticatorMixin
moto_api_backends = {"global": moto_api_backend}

View File

@ -8,6 +8,7 @@ import os
import re
import six
import types
from abc import abstractmethod
from io import BytesIO
from collections import defaultdict
from botocore.config import Config
@ -534,6 +535,47 @@ class BaseModel(object):
return instance
# Parent class for every Model that can be instantiated by CloudFormation
# On subclasses, implement the two methods as @staticmethod to ensure correct behaviour of the CF parser
class CloudFormationModel(BaseModel):
@abstractmethod
def cloudformation_name_type(self):
# This must be implemented as a staticmethod with no parameters
# Return None for resources that do not have a name property
pass
@abstractmethod
def cloudformation_type(self):
# This must be implemented as a staticmethod with no parameters
# See for example https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html
return "AWS::SERVICE::RESOURCE"
@abstractmethod
def create_from_cloudformation_json(self):
# This must be implemented as a classmethod with parameters:
# cls, resource_name, cloudformation_json, region_name
# Extract the resource parameters from the cloudformation json
# and return an instance of the resource class
pass
@abstractmethod
def update_from_cloudformation_json(self):
# This must be implemented as a classmethod with parameters:
# cls, original_resource, new_resource_name, cloudformation_json, region_name
# Extract the resource parameters from the cloudformation json,
# delete the old resource and return the new one. Optionally inspect
# the change in parameters and no-op when nothing has changed.
pass
@abstractmethod
def delete_from_cloudformation_json(self):
# This must be implemented as a classmethod with parameters:
# cls, resource_name, cloudformation_json, region_name
# Extract the resource parameters from the cloudformation json
# and delete the resource. Do not include a return statement.
pass
class BaseBackend(object):
def _reset_model_refs(self):
# Remove all references to the models stored

View File

@ -4,7 +4,7 @@ import datetime
from boto3 import Session
from moto.compat import OrderedDict
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, BaseModel, CloudFormationModel
from .utils import get_random_pipeline_id, remove_capitalization_of_dict_keys
@ -18,7 +18,7 @@ class PipelineObject(BaseModel):
return {"fields": self.fields, "id": self.object_id, "name": self.name}
class Pipeline(BaseModel):
class Pipeline(CloudFormationModel):
def __init__(self, name, unique_id, **kwargs):
self.name = name
self.unique_id = unique_id
@ -74,6 +74,15 @@ class Pipeline(BaseModel):
def activate(self):
self.status = "SCHEDULED"
@staticmethod
def cloudformation_name_type():
return "Name"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-datapipeline-pipeline.html
return "AWS::DataPipeline::Pipeline"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -4,7 +4,7 @@ import datetime
import json
from moto.compat import OrderedDict
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, BaseModel, CloudFormationModel
from moto.core.utils import unix_time
from moto.core import ACCOUNT_ID
from .comparisons import get_comparison_func
@ -82,7 +82,7 @@ class Item(BaseModel):
return {"Item": included}
class Table(BaseModel):
class Table(CloudFormationModel):
def __init__(
self,
name,
@ -135,6 +135,15 @@ class Table(BaseModel):
}
return results
@staticmethod
def cloudformation_name_type():
return "TableName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html
return "AWS::DynamoDB::Table"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -9,7 +9,7 @@ import uuid
from boto3 import Session
from moto.compat import OrderedDict
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, BaseModel, CloudFormationModel
from moto.core.utils import unix_time
from moto.core.exceptions import JsonRESTError
from moto.dynamodb2.comparisons import get_filter_expression
@ -359,7 +359,7 @@ class GlobalSecondaryIndex(SecondaryIndex):
self.throughput = u.get("ProvisionedThroughput", self.throughput)
class Table(BaseModel):
class Table(CloudFormationModel):
def __init__(
self,
table_name,
@ -431,6 +431,15 @@ class Table(BaseModel):
def physical_resource_id(self):
return self.name
@staticmethod
def cloudformation_name_type():
return "TableName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html
return "AWS::DynamoDB::Table"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -109,7 +109,7 @@ class ExpressionTokenizer(object):
@classmethod
def is_expression_attribute(cls, input_string):
return re.compile("^[a-zA-Z][a-zA-Z0-9_]*$").match(input_string) is not None
return re.compile("^[a-zA-Z0-9][a-zA-Z0-9_]*$").match(input_string) is not None
@classmethod
def is_expression_attribute_name(cls, input_string):

View File

@ -22,7 +22,7 @@ from boto.ec2.launchspecification import LaunchSpecification
from moto.compat import OrderedDict
from moto.core import BaseBackend
from moto.core.models import Model, BaseModel
from moto.core.models import Model, BaseModel, CloudFormationModel
from moto.core.utils import (
iso_8601_datetime_with_milliseconds,
camelcase_to_underscores,
@ -219,7 +219,7 @@ class TaggedEC2Resource(BaseModel):
raise FilterNotImplementedError(filter_name, method_name)
class NetworkInterface(TaggedEC2Resource):
class NetworkInterface(TaggedEC2Resource, CloudFormationModel):
def __init__(
self,
ec2_backend,
@ -268,6 +268,15 @@ class NetworkInterface(TaggedEC2Resource):
if group:
self._group_set.append(group)
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html
return "AWS::EC2::NetworkInterface"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -454,7 +463,7 @@ class NetworkInterfaceBackend(object):
return generic_filter(filters, enis)
class Instance(TaggedEC2Resource, BotoInstance):
class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel):
VALID_ATTRIBUTES = {
"instanceType",
"kernel",
@ -621,6 +630,15 @@ class Instance(TaggedEC2Resource, BotoInstance):
formatted_ip, self.region_name
)
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-instance.html
return "AWS::EC2::Instance"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -1843,7 +1861,7 @@ class SecurityRule(object):
return True
class SecurityGroup(TaggedEC2Resource):
class SecurityGroup(TaggedEC2Resource, CloudFormationModel):
def __init__(self, ec2_backend, group_id, name, description, vpc_id=None):
self.ec2_backend = ec2_backend
self.id = group_id
@ -1861,6 +1879,15 @@ class SecurityGroup(TaggedEC2Resource):
if vpc and len(vpc.get_cidr_block_association_set(ipv6=True)) > 0:
self.egress_rules.append(SecurityRule("-1", None, None, [], []))
@staticmethod
def cloudformation_name_type():
return "GroupName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-securitygroup.html
return "AWS::EC2::SecurityGroup"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -2260,11 +2287,20 @@ class SecurityGroupBackend(object):
raise RulesPerSecurityGroupLimitExceededError
class SecurityGroupIngress(object):
class SecurityGroupIngress(CloudFormationModel):
def __init__(self, security_group, properties):
self.security_group = security_group
self.properties = properties
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-securitygroupingress.html
return "AWS::EC2::SecurityGroupIngress"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -2328,7 +2364,7 @@ class SecurityGroupIngress(object):
return cls(security_group, properties)
class VolumeAttachment(object):
class VolumeAttachment(CloudFormationModel):
def __init__(self, volume, instance, device, status):
self.volume = volume
self.attach_time = utc_date_and_time()
@ -2336,6 +2372,15 @@ class VolumeAttachment(object):
self.device = device
self.status = status
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-volumeattachment.html
return "AWS::EC2::VolumeAttachment"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -2354,7 +2399,7 @@ class VolumeAttachment(object):
return attachment
class Volume(TaggedEC2Resource):
class Volume(TaggedEC2Resource, CloudFormationModel):
def __init__(
self, ec2_backend, volume_id, size, zone, snapshot_id=None, encrypted=False
):
@ -2367,6 +2412,15 @@ class Volume(TaggedEC2Resource):
self.ec2_backend = ec2_backend
self.encrypted = encrypted
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-volume.html
return "AWS::EC2::Volume"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -2434,6 +2488,7 @@ class Snapshot(TaggedEC2Resource):
self.description = description
self.start_time = utc_date_and_time()
self.create_volume_permission_groups = set()
self.create_volume_permission_userids = set()
self.ec2_backend = ec2_backend
self.status = "completed"
self.encrypted = encrypted
@ -2598,32 +2653,36 @@ class EBSBackend(object):
snapshot = self.get_snapshot(snapshot_id)
return snapshot.create_volume_permission_groups
def add_create_volume_permission(self, snapshot_id, user_id=None, group=None):
if user_id:
self.raise_not_implemented_error(
"The UserId parameter for ModifySnapshotAttribute"
)
if group != "all":
raise InvalidAMIAttributeItemValueError("UserGroup", group)
def get_create_volume_permission_userids(self, snapshot_id):
snapshot = self.get_snapshot(snapshot_id)
snapshot.create_volume_permission_groups.add(group)
return snapshot.create_volume_permission_userids
def add_create_volume_permission(self, snapshot_id, user_ids=None, groups=None):
snapshot = self.get_snapshot(snapshot_id)
if user_ids:
snapshot.create_volume_permission_userids.update(user_ids)
if groups and groups != ["all"]:
raise InvalidAMIAttributeItemValueError("UserGroup", groups)
else:
snapshot.create_volume_permission_groups.update(groups)
return True
def remove_create_volume_permission(self, snapshot_id, user_id=None, group=None):
if user_id:
self.raise_not_implemented_error(
"The UserId parameter for ModifySnapshotAttribute"
)
if group != "all":
raise InvalidAMIAttributeItemValueError("UserGroup", group)
def remove_create_volume_permission(self, snapshot_id, user_ids=None, groups=None):
snapshot = self.get_snapshot(snapshot_id)
snapshot.create_volume_permission_groups.discard(group)
if user_ids:
snapshot.create_volume_permission_userids.difference_update(user_ids)
if groups and groups != ["all"]:
raise InvalidAMIAttributeItemValueError("UserGroup", groups)
else:
snapshot.create_volume_permission_groups.difference_update(groups)
return True
class VPC(TaggedEC2Resource):
class VPC(TaggedEC2Resource, CloudFormationModel):
def __init__(
self,
ec2_backend,
@ -2656,6 +2715,15 @@ class VPC(TaggedEC2Resource):
amazon_provided_ipv6_cidr_block=amazon_provided_ipv6_cidr_block,
)
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc.html
return "AWS::EC2::VPC"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -3022,13 +3090,22 @@ class VPCPeeringConnectionStatus(object):
self.message = "Inactive"
class VPCPeeringConnection(TaggedEC2Resource):
class VPCPeeringConnection(TaggedEC2Resource, CloudFormationModel):
def __init__(self, vpc_pcx_id, vpc, peer_vpc):
self.id = vpc_pcx_id
self.vpc = vpc
self.peer_vpc = peer_vpc
self._status = VPCPeeringConnectionStatus()
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpcpeeringconnection.html
return "AWS::EC2::VPCPeeringConnection"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -3114,7 +3191,7 @@ class VPCPeeringConnectionBackend(object):
return vpc_pcx
class Subnet(TaggedEC2Resource):
class Subnet(TaggedEC2Resource, CloudFormationModel):
def __init__(
self,
ec2_backend,
@ -3150,6 +3227,15 @@ class Subnet(TaggedEC2Resource):
self._unused_ips = set() # if instance is destroyed hold IP here for reuse
self._subnet_ips = {} # has IP: instance
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html
return "AWS::EC2::Subnet"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -3377,11 +3463,20 @@ class SubnetBackend(object):
raise InvalidParameterValueError(attr_name)
class SubnetRouteTableAssociation(object):
class SubnetRouteTableAssociation(CloudFormationModel):
def __init__(self, route_table_id, subnet_id):
self.route_table_id = route_table_id
self.subnet_id = subnet_id
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnetroutetableassociation.html
return "AWS::EC2::SubnetRouteTableAssociation"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -3411,7 +3506,7 @@ class SubnetRouteTableAssociationBackend(object):
return subnet_association
class RouteTable(TaggedEC2Resource):
class RouteTable(TaggedEC2Resource, CloudFormationModel):
def __init__(self, ec2_backend, route_table_id, vpc_id, main=False):
self.ec2_backend = ec2_backend
self.id = route_table_id
@ -3420,6 +3515,15 @@ class RouteTable(TaggedEC2Resource):
self.associations = {}
self.routes = {}
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-routetable.html
return "AWS::EC2::RouteTable"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -3555,7 +3659,7 @@ class RouteTableBackend(object):
return self.associate_route_table(route_table_id, subnet_id)
class Route(object):
class Route(CloudFormationModel):
def __init__(
self,
route_table,
@ -3581,6 +3685,15 @@ class Route(object):
self.interface = interface
self.vpc_pcx = vpc_pcx
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-route.html
return "AWS::EC2::Route"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -3748,12 +3861,21 @@ class RouteBackend(object):
return deleted
class InternetGateway(TaggedEC2Resource):
class InternetGateway(TaggedEC2Resource, CloudFormationModel):
def __init__(self, ec2_backend):
self.ec2_backend = ec2_backend
self.id = random_internet_gateway_id()
self.vpc = None
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-internetgateway.html
return "AWS::EC2::InternetGateway"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -3826,11 +3948,20 @@ class InternetGatewayBackend(object):
return self.describe_internet_gateways(internet_gateway_ids=igw_ids)[0]
class VPCGatewayAttachment(BaseModel):
class VPCGatewayAttachment(CloudFormationModel):
def __init__(self, gateway_id, vpc_id):
self.gateway_id = gateway_id
self.vpc_id = vpc_id
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpcgatewayattachment.html
return "AWS::EC2::VPCGatewayAttachment"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -4051,7 +4182,7 @@ class SpotFleetLaunchSpec(object):
self.weighted_capacity = float(weighted_capacity)
class SpotFleetRequest(TaggedEC2Resource):
class SpotFleetRequest(TaggedEC2Resource, CloudFormationModel):
def __init__(
self,
ec2_backend,
@ -4100,6 +4231,15 @@ class SpotFleetRequest(TaggedEC2Resource):
def physical_resource_id(self):
return self.id
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-spotfleet.html
return "AWS::EC2::SpotFleet"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -4323,7 +4463,7 @@ class SpotFleetBackend(object):
return True
class ElasticAddress(object):
class ElasticAddress(CloudFormationModel):
def __init__(self, domain, address=None):
if address:
self.public_ip = address
@ -4335,6 +4475,15 @@ class ElasticAddress(object):
self.eni = None
self.association_id = None
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-eip.html
return "AWS::EC2::EIP"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -5095,7 +5244,7 @@ class CustomerGatewayBackend(object):
return deleted
class NatGateway(object):
class NatGateway(CloudFormationModel):
def __init__(self, backend, subnet_id, allocation_id):
# public properties
self.id = random_nat_gateway_id()
@ -5133,6 +5282,15 @@ class NatGateway(object):
eips = self._backend.address_by_allocation([self.allocation_id])
return eips[0].public_ip
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-natgateway.html
return "AWS::EC2::NatGateway"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -116,22 +116,23 @@ class ElasticBlockStore(BaseResponse):
def describe_snapshot_attribute(self):
snapshot_id = self._get_param("SnapshotId")
groups = self.ec2_backend.get_create_volume_permission_groups(snapshot_id)
user_ids = self.ec2_backend.get_create_volume_permission_userids(snapshot_id)
template = self.response_template(DESCRIBE_SNAPSHOT_ATTRIBUTES_RESPONSE)
return template.render(snapshot_id=snapshot_id, groups=groups)
return template.render(snapshot_id=snapshot_id, groups=groups, userIds=user_ids)
def modify_snapshot_attribute(self):
snapshot_id = self._get_param("SnapshotId")
operation_type = self._get_param("OperationType")
group = self._get_param("UserGroup.1")
user_id = self._get_param("UserId.1")
groups = self._get_multi_param("UserGroup")
user_ids = self._get_multi_param("UserId")
if self.is_not_dryrun("ModifySnapshotAttribute"):
if operation_type == "add":
self.ec2_backend.add_create_volume_permission(
snapshot_id, user_id=user_id, group=group
snapshot_id, user_ids=user_ids, groups=groups
)
elif operation_type == "remove":
self.ec2_backend.remove_create_volume_permission(
snapshot_id, user_id=user_id, group=group
snapshot_id, user_ids=user_ids, groups=groups
)
return MODIFY_SNAPSHOT_ATTRIBUTE_RESPONSE
@ -311,18 +312,18 @@ DESCRIBE_SNAPSHOT_ATTRIBUTES_RESPONSE = """
<DescribeSnapshotAttributeResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
<requestId>a9540c9f-161a-45d8-9cc1-1182b89ad69f</requestId>
<snapshotId>snap-a0332ee0</snapshotId>
{% if not groups %}
<createVolumePermission/>
{% endif %}
{% if groups %}
<createVolumePermission>
{% for group in groups %}
<item>
<group>{{ group }}</group>
</item>
{% endfor %}
</createVolumePermission>
{% endif %}
<createVolumePermission>
{% for group in groups %}
<item>
<group>{{ group }}</group>
</item>
{% endfor %}
{% for userId in userIds %}
<item>
<userId>{{ userId }}</userId>
</item>
{% endfor %}
</createVolumePermission>
</DescribeSnapshotAttributeResponse>
"""

View File

@ -7,7 +7,7 @@ from random import random
from botocore.exceptions import ParamValidationError
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, BaseModel, CloudFormationModel
from moto.ec2 import ec2_backends
from moto.ecr.exceptions import ImageNotFoundException, RepositoryNotFoundException
@ -38,7 +38,7 @@ class BaseObject(BaseModel):
return self.gen_response_object()
class Repository(BaseObject):
class Repository(BaseObject, CloudFormationModel):
def __init__(self, repository_name):
self.registry_id = DEFAULT_REGISTRY_ID
self.arn = "arn:aws:ecr:us-east-1:{0}:repository/{1}".format(
@ -67,6 +67,15 @@ class Repository(BaseObject):
del response_object["arn"], response_object["name"], response_object["images"]
return response_object
@staticmethod
def cloudformation_name_type():
return "RepositoryName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecr-repository.html
return "AWS::ECR::Repository"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -8,7 +8,7 @@ import pytz
from boto3 import Session
from moto.core.exceptions import JsonRESTError
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, BaseModel, CloudFormationModel
from moto.core.utils import unix_time
from moto.ec2 import ec2_backends
from copy import copy
@ -44,7 +44,7 @@ class BaseObject(BaseModel):
return self.gen_response_object()
class Cluster(BaseObject):
class Cluster(BaseObject, CloudFormationModel):
def __init__(self, cluster_name, region_name):
self.active_services_count = 0
self.arn = "arn:aws:ecs:{0}:012345678910:cluster/{1}".format(
@ -69,6 +69,15 @@ class Cluster(BaseObject):
del response_object["arn"], response_object["name"]
return response_object
@staticmethod
def cloudformation_name_type():
return "ClusterName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-cluster.html
return "AWS::ECS::Cluster"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -116,7 +125,7 @@ class Cluster(BaseObject):
raise UnformattedGetAttTemplateException()
class TaskDefinition(BaseObject):
class TaskDefinition(BaseObject, CloudFormationModel):
def __init__(
self,
family,
@ -159,6 +168,15 @@ class TaskDefinition(BaseObject):
def physical_resource_id(self):
return self.arn
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html
return "AWS::ECS::TaskDefinition"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -235,7 +253,7 @@ class Task(BaseObject):
return response_object
class Service(BaseObject):
class Service(BaseObject, CloudFormationModel):
def __init__(
self,
cluster,
@ -315,6 +333,15 @@ class Service(BaseObject):
return response_object
@staticmethod
def cloudformation_name_type():
return "ServiceName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-service.html
return "AWS::ECS::Service"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -13,7 +13,7 @@ from boto.ec2.elb.attributes import (
)
from boto.ec2.elb.policies import Policies, OtherPolicy
from moto.compat import OrderedDict
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, BaseModel, CloudFormationModel
from moto.ec2.models import ec2_backends
from .exceptions import (
BadHealthCheckDefinition,
@ -69,7 +69,7 @@ class FakeBackend(BaseModel):
)
class FakeLoadBalancer(BaseModel):
class FakeLoadBalancer(CloudFormationModel):
def __init__(
self,
name,
@ -119,6 +119,15 @@ class FakeLoadBalancer(BaseModel):
)
self.backends.append(backend)
@staticmethod
def cloudformation_name_type():
return "LoadBalancerName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancing-loadbalancer.html
return "AWS::ElasticLoadBalancing::LoadBalancer"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -6,7 +6,7 @@ from jinja2 import Template
from botocore.exceptions import ParamValidationError
from moto.compat import OrderedDict
from moto.core.exceptions import RESTError
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, BaseModel, CloudFormationModel
from moto.core.utils import camelcase_to_underscores, underscores_to_camelcase
from moto.ec2.models import ec2_backends
from moto.acm.models import acm_backends
@ -50,7 +50,7 @@ class FakeHealthStatus(BaseModel):
self.description = description
class FakeTargetGroup(BaseModel):
class FakeTargetGroup(CloudFormationModel):
HTTP_CODE_REGEX = re.compile(r"(?:(?:\d+-\d+|\d+),?)+")
def __init__(
@ -143,6 +143,15 @@ class FakeTargetGroup(BaseModel):
)
return FakeHealthStatus(t["id"], t["port"], self.healthcheck_port, "healthy")
@staticmethod
def cloudformation_name_type():
return "Name"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-targetgroup.html
return "AWS::ElasticLoadBalancingV2::TargetGroup"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -183,7 +192,7 @@ class FakeTargetGroup(BaseModel):
return target_group
class FakeListener(BaseModel):
class FakeListener(CloudFormationModel):
def __init__(
self,
load_balancer_arn,
@ -228,6 +237,15 @@ class FakeListener(BaseModel):
self._non_default_rules, key=lambda x: x.priority
)
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-listener.html
return "AWS::ElasticLoadBalancingV2::Listener"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -343,7 +361,7 @@ class FakeBackend(BaseModel):
)
class FakeLoadBalancer(BaseModel):
class FakeLoadBalancer(CloudFormationModel):
VALID_ATTRS = {
"access_logs.s3.enabled",
"access_logs.s3.bucket",
@ -402,6 +420,15 @@ class FakeLoadBalancer(BaseModel):
""" Not exposed as part of the ELB API - used for CloudFormation. """
elbv2_backends[region].delete_load_balancer(self.arn)
@staticmethod
def cloudformation_name_type():
return "Name"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-loadbalancer.html
return "AWS::ElasticLoadBalancingV2::LoadBalancer"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -4,14 +4,14 @@ import json
from boto3 import Session
from moto.core.exceptions import JsonRESTError
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, CloudFormationModel
from moto.sts.models import ACCOUNT_ID
from moto.utilities.tagging_service import TaggingService
from uuid import uuid4
class Rule(BaseModel):
class Rule(CloudFormationModel):
def _generate_arn(self, name):
return "arn:aws:events:{region_name}:111111111111:rule/{name}".format(
region_name=self.region_name, name=name
@ -73,6 +73,15 @@ class Rule(BaseModel):
raise UnformattedGetAttTemplateException()
@staticmethod
def cloudformation_name_type():
return "Name"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-rule.html
return "AWS::Events::Rule"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -101,7 +110,7 @@ class Rule(BaseModel):
event_backend.delete_rule(name=event_name)
class EventBus(BaseModel):
class EventBus(CloudFormationModel):
def __init__(self, region_name, name):
self.region = region_name
self.name = name
@ -152,6 +161,15 @@ class EventBus(BaseModel):
raise UnformattedGetAttTemplateException()
@staticmethod
def cloudformation_name_type():
return "Name"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-eventbus.html
return "AWS::Events::EventBus"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -15,7 +15,7 @@ from six.moves.urllib.parse import urlparse
from uuid import uuid4
from moto.core.exceptions import RESTError
from moto.core import BaseBackend, BaseModel, ACCOUNT_ID
from moto.core import BaseBackend, BaseModel, ACCOUNT_ID, CloudFormationModel
from moto.core.utils import (
iso_8601_datetime_without_milliseconds,
iso_8601_datetime_with_milliseconds,
@ -299,7 +299,7 @@ class InlinePolicy(Policy):
"""TODO: is this needed?"""
class Role(BaseModel):
class Role(CloudFormationModel):
def __init__(
self,
role_id,
@ -327,6 +327,15 @@ class Role(BaseModel):
def created_iso_8601(self):
return iso_8601_datetime_with_milliseconds(self.create_date)
@staticmethod
def cloudformation_name_type():
return "RoleName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html
return "AWS::IAM::Role"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -384,7 +393,7 @@ class Role(BaseModel):
return [self.tags[tag] for tag in self.tags]
class InstanceProfile(BaseModel):
class InstanceProfile(CloudFormationModel):
def __init__(self, instance_profile_id, name, path, roles):
self.id = instance_profile_id
self.name = name
@ -396,6 +405,15 @@ class InstanceProfile(BaseModel):
def created_iso_8601(self):
return iso_8601_datetime_with_milliseconds(self.create_date)
@staticmethod
def cloudformation_name_type():
return "InstanceProfileName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-instanceprofile.html
return "AWS::IAM::InstanceProfile"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -12,7 +12,7 @@ from hashlib import md5
from boto3 import Session
from moto.compat import OrderedDict
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, BaseModel, CloudFormationModel
from moto.core.utils import unix_time
from moto.core import ACCOUNT_ID
from .exceptions import (
@ -129,7 +129,7 @@ class Shard(BaseModel):
}
class Stream(BaseModel):
class Stream(CloudFormationModel):
def __init__(self, stream_name, shard_count, region):
self.stream_name = stream_name
self.shard_count = shard_count
@ -216,6 +216,15 @@ class Stream(BaseModel):
}
}
@staticmethod
def cloudformation_name_type():
return "Name"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesis-stream.html
return "AWS::Kinesis::Stream"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -6,7 +6,7 @@ from datetime import datetime, timedelta
from boto3 import Session
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, CloudFormationModel
from moto.core.utils import unix_time
from moto.utilities.tagging_service import TaggingService
from moto.core.exceptions import JsonRESTError
@ -15,7 +15,7 @@ from moto.iam.models import ACCOUNT_ID
from .utils import decrypt, encrypt, generate_key_id, generate_master_key
class Key(BaseModel):
class Key(CloudFormationModel):
def __init__(
self, policy, key_usage, customer_master_key_spec, description, region
):
@ -99,6 +99,15 @@ class Key(BaseModel):
def delete(self, region_name):
kms_backends[region_name].delete_key(self.id)
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kms-key.html
return "AWS::KMS::Key"
@classmethod
def create_from_cloudformation_json(
self, resource_name, cloudformation_json, region_name

View File

@ -2,6 +2,54 @@ from __future__ import unicode_literals
from moto.core.exceptions import JsonRESTError
class AccountAlreadyRegisteredException(JsonRESTError):
code = 400
def __init__(self):
super(AccountAlreadyRegisteredException, self).__init__(
"AccountAlreadyRegisteredException",
"The provided account is already a delegated administrator for your organization.",
)
class AccountNotRegisteredException(JsonRESTError):
code = 400
def __init__(self):
super(AccountNotRegisteredException, self).__init__(
"AccountNotRegisteredException",
"The provided account is not a registered delegated administrator for your organization.",
)
class AccountNotFoundException(JsonRESTError):
code = 400
def __init__(self):
super(AccountNotFoundException, self).__init__(
"AccountNotFoundException", "You specified an account that doesn't exist."
)
class AWSOrganizationsNotInUseException(JsonRESTError):
code = 400
def __init__(self):
super(AWSOrganizationsNotInUseException, self).__init__(
"AWSOrganizationsNotInUseException",
"Your account is not a member of an organization.",
)
class ConstraintViolationException(JsonRESTError):
code = 400
def __init__(self, message):
super(ConstraintViolationException, self).__init__(
"ConstraintViolationException", message
)
class InvalidInputException(JsonRESTError):
code = 400

View File

@ -4,7 +4,7 @@ import datetime
import re
import json
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, BaseModel, ACCOUNT_ID
from moto.core.exceptions import RESTError
from moto.core.utils import unix_time
from moto.organizations import utils
@ -12,6 +12,11 @@ from moto.organizations.exceptions import (
InvalidInputException,
DuplicateOrganizationalUnitException,
DuplicatePolicyException,
AccountNotFoundException,
ConstraintViolationException,
AccountAlreadyRegisteredException,
AWSOrganizationsNotInUseException,
AccountNotRegisteredException,
)
@ -85,15 +90,13 @@ class FakeAccount(BaseModel):
def describe(self):
return {
"Account": {
"Id": self.id,
"Arn": self.arn,
"Email": self.email,
"Name": self.name,
"Status": self.status,
"JoinedMethod": self.joined_method,
"JoinedTimestamp": unix_time(self.create_time),
}
"Id": self.id,
"Arn": self.arn,
"Email": self.email,
"Name": self.name,
"Status": self.status,
"JoinedMethod": self.joined_method,
"JoinedTimestamp": unix_time(self.create_time),
}
@ -221,6 +224,56 @@ class FakeServiceAccess(BaseModel):
return service_principal in FakeServiceAccess.TRUSTED_SERVICES
class FakeDelegatedAdministrator(BaseModel):
# List of services, which support a different Account to ba a delegated administrator
# https://docs.aws.amazon.com/organizations/latest/userguide/orgs_integrated-services-list.html
SUPPORTED_SERVICES = [
"config-multiaccountsetup.amazonaws.com",
"guardduty.amazonaws.com",
"access-analyzer.amazonaws.com",
"macie.amazonaws.com",
"servicecatalog.amazonaws.com",
"ssm.amazonaws.com",
]
def __init__(self, account):
self.account = account
self.enabled_date = datetime.datetime.utcnow()
self.services = {}
def add_service_principal(self, service_principal):
if service_principal in self.services:
raise AccountAlreadyRegisteredException
if not self.supported_service(service_principal):
raise InvalidInputException(
"You specified an unrecognized service principal."
)
self.services[service_principal] = {
"ServicePrincipal": service_principal,
"DelegationEnabledDate": unix_time(datetime.datetime.utcnow()),
}
def remove_service_principal(self, service_principal):
if service_principal not in self.services:
raise InvalidInputException(
"You specified an unrecognized service principal."
)
self.services.pop(service_principal)
def describe(self):
admin = self.account.describe()
admin["DelegationEnabledDate"] = unix_time(self.enabled_date)
return admin
@staticmethod
def supported_service(service_principal):
return service_principal in FakeDelegatedAdministrator.SUPPORTED_SERVICES
class OrganizationsBackend(BaseBackend):
def __init__(self):
self.org = None
@ -228,6 +281,7 @@ class OrganizationsBackend(BaseBackend):
self.ou = []
self.policies = []
self.services = []
self.admins = []
def create_organization(self, **kwargs):
self.org = FakeOrganization(kwargs["FeatureSet"])
@ -259,10 +313,7 @@ class OrganizationsBackend(BaseBackend):
def describe_organization(self):
if not self.org:
raise RESTError(
"AWSOrganizationsNotInUseException",
"Your account is not a member of an organization.",
)
raise AWSOrganizationsNotInUseException
return self.org.describe()
def list_roots(self):
@ -325,10 +376,7 @@ class OrganizationsBackend(BaseBackend):
(account for account in self.accounts if account.id == account_id), None
)
if account is None:
raise RESTError(
"AccountNotFoundException",
"You specified an account that doesn't exist.",
)
raise AccountNotFoundException
return account
def get_account_by_attr(self, attr, value):
@ -341,15 +389,12 @@ class OrganizationsBackend(BaseBackend):
None,
)
if account is None:
raise RESTError(
"AccountNotFoundException",
"You specified an account that doesn't exist.",
)
raise AccountNotFoundException
return account
def describe_account(self, **kwargs):
account = self.get_account_by_id(kwargs["AccountId"])
return account.describe()
return dict(Account=account.describe())
def describe_create_account_status(self, **kwargs):
account = self.get_account_by_attr(
@ -358,15 +403,13 @@ class OrganizationsBackend(BaseBackend):
return account.create_account_status
def list_accounts(self):
return dict(
Accounts=[account.describe()["Account"] for account in self.accounts]
)
return dict(Accounts=[account.describe() for account in self.accounts])
def list_accounts_for_parent(self, **kwargs):
parent_id = self.validate_parent_id(kwargs["ParentId"])
return dict(
Accounts=[
account.describe()["Account"]
account.describe()
for account in self.accounts
if account.parent_id == parent_id
]
@ -399,7 +442,7 @@ class OrganizationsBackend(BaseBackend):
elif kwargs["ChildType"] == "ORGANIZATIONAL_UNIT":
obj_list = self.ou
else:
raise RESTError("InvalidInputException", "You specified an invalid value.")
raise InvalidInputException("You specified an invalid value.")
return dict(
Children=[
{"Id": obj.id, "Type": kwargs["ChildType"]}
@ -427,7 +470,7 @@ class OrganizationsBackend(BaseBackend):
"You specified a policy that doesn't exist.",
)
else:
raise RESTError("InvalidInputException", "You specified an invalid value.")
raise InvalidInputException("You specified an invalid value.")
return policy.describe()
def get_policy_by_id(self, policy_id):
@ -472,12 +515,9 @@ class OrganizationsBackend(BaseBackend):
account.attached_policies.append(policy)
policy.attachments.append(account)
else:
raise RESTError(
"AccountNotFoundException",
"You specified an account that doesn't exist.",
)
raise AccountNotFoundException
else:
raise RESTError("InvalidInputException", "You specified an invalid value.")
raise InvalidInputException("You specified an invalid value.")
def list_policies(self, **kwargs):
return dict(
@ -510,12 +550,9 @@ class OrganizationsBackend(BaseBackend):
elif re.compile(utils.ACCOUNT_ID_REGEX).match(kwargs["TargetId"]):
obj = next((a for a in self.accounts if a.id == kwargs["TargetId"]), None)
if obj is None:
raise RESTError(
"AccountNotFoundException",
"You specified an account that doesn't exist.",
)
raise AccountNotFoundException
else:
raise RESTError("InvalidInputException", "You specified an invalid value.")
raise InvalidInputException("You specified an invalid value.")
return dict(
Policies=[
p.describe()["Policy"]["PolicySummary"] for p in obj.attached_policies
@ -533,7 +570,7 @@ class OrganizationsBackend(BaseBackend):
"You specified a policy that doesn't exist.",
)
else:
raise RESTError("InvalidInputException", "You specified an invalid value.")
raise InvalidInputException("You specified an invalid value.")
objects = [
{"TargetId": obj.id, "Arn": obj.arn, "Name": obj.name, "Type": obj.type}
for obj in policy.attachments
@ -606,5 +643,95 @@ class OrganizationsBackend(BaseBackend):
if service_principal:
self.services.remove(service_principal)
def register_delegated_administrator(self, **kwargs):
account_id = kwargs["AccountId"]
if account_id == ACCOUNT_ID:
raise ConstraintViolationException(
"You cannot register master account/yourself as delegated administrator for your organization."
)
account = self.get_account_by_id(account_id)
admin = next(
(admin for admin in self.admins if admin.account.id == account_id), None
)
if admin is None:
admin = FakeDelegatedAdministrator(account)
self.admins.append(admin)
admin.add_service_principal(kwargs["ServicePrincipal"])
def list_delegated_administrators(self, **kwargs):
admins = self.admins
service = kwargs.get("ServicePrincipal")
if service:
if not FakeDelegatedAdministrator.supported_service(service):
raise InvalidInputException(
"You specified an unrecognized service principal."
)
admins = [admin for admin in admins if service in admin.services]
delegated_admins = [admin.describe() for admin in admins]
return dict(DelegatedAdministrators=delegated_admins)
def list_delegated_services_for_account(self, **kwargs):
admin = next(
(admin for admin in self.admins if admin.account.id == kwargs["AccountId"]),
None,
)
if admin is None:
account = next(
(
account
for account in self.accounts
if account.id == kwargs["AccountId"]
),
None,
)
if account:
raise AccountNotRegisteredException
raise AWSOrganizationsNotInUseException
services = [service for service in admin.services.values()]
return dict(DelegatedServices=services)
def deregister_delegated_administrator(self, **kwargs):
account_id = kwargs["AccountId"]
service = kwargs["ServicePrincipal"]
if account_id == ACCOUNT_ID:
raise ConstraintViolationException(
"You cannot register master account/yourself as delegated administrator for your organization."
)
admin = next(
(admin for admin in self.admins if admin.account.id == account_id), None,
)
if admin is None:
account = next(
(
account
for account in self.accounts
if account.id == kwargs["AccountId"]
),
None,
)
if account:
raise AccountNotRegisteredException
raise AccountNotFoundException
admin.remove_service_principal(service)
# remove account, when no services attached
if not admin.services:
self.admins.remove(admin)
organizations_backend = OrganizationsBackend()

View File

@ -163,3 +163,31 @@ class OrganizationsResponse(BaseResponse):
return json.dumps(
self.organizations_backend.disable_aws_service_access(**self.request_params)
)
def register_delegated_administrator(self):
return json.dumps(
self.organizations_backend.register_delegated_administrator(
**self.request_params
)
)
def list_delegated_administrators(self):
return json.dumps(
self.organizations_backend.list_delegated_administrators(
**self.request_params
)
)
def list_delegated_services_for_account(self):
return json.dumps(
self.organizations_backend.list_delegated_services_for_account(
**self.request_params
)
)
def deregister_delegated_administrator(self):
return json.dumps(
self.organizations_backend.deregister_delegated_administrator(
**self.request_params
)
)

View File

@ -3,14 +3,14 @@ from __future__ import unicode_literals
import boto.rds
from jinja2 import Template
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, CloudFormationModel
from moto.core.utils import get_random_hex
from moto.ec2.models import ec2_backends
from moto.rds.exceptions import UnformattedGetAttTemplateException
from moto.rds2.models import rds2_backends
class Database(BaseModel):
class Database(CloudFormationModel):
def get_cfn_attribute(self, attribute_name):
if attribute_name == "Endpoint.Address":
return self.address
@ -18,13 +18,22 @@ class Database(BaseModel):
return self.port
raise UnformattedGetAttTemplateException()
@staticmethod
def cloudformation_name_type():
return "DBInstanceIdentifier"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbinstance.html
return "AWS::RDS::DBInstance"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
):
properties = cloudformation_json["Properties"]
db_instance_identifier = properties.get("DBInstanceIdentifier")
db_instance_identifier = properties.get(cls.cloudformation_name_type())
if not db_instance_identifier:
db_instance_identifier = resource_name.lower() + get_random_hex(12)
db_security_groups = properties.get("DBSecurityGroups")
@ -163,7 +172,7 @@ class Database(BaseModel):
backend.delete_database(self.db_instance_identifier)
class SecurityGroup(BaseModel):
class SecurityGroup(CloudFormationModel):
def __init__(self, group_name, description):
self.group_name = group_name
self.description = description
@ -206,6 +215,15 @@ class SecurityGroup(BaseModel):
def authorize_security_group(self, security_group):
self.ec2_security_groups.append(security_group)
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbsecuritygroup.html
return "AWS::RDS::DBSecurityGroup"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -239,7 +257,7 @@ class SecurityGroup(BaseModel):
backend.delete_security_group(self.group_name)
class SubnetGroup(BaseModel):
class SubnetGroup(CloudFormationModel):
def __init__(self, subnet_name, description, subnets):
self.subnet_name = subnet_name
self.description = description
@ -271,13 +289,23 @@ class SubnetGroup(BaseModel):
)
return template.render(subnet_group=self)
@staticmethod
def cloudformation_name_type():
return "DBSubnetGroupName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbsubnetgroup.html
return "AWS::RDS::DBSubnetGroup"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
):
properties = cloudformation_json["Properties"]
subnet_name = resource_name.lower() + get_random_hex(12)
subnet_name = properties.get(cls.cloudformation_name_type())
if not subnet_name:
subnet_name = resource_name.lower() + get_random_hex(12)
description = properties["DBSubnetGroupDescription"]
subnet_ids = properties["SubnetIds"]
tags = properties.get("Tags")

View File

@ -9,7 +9,7 @@ from boto3 import Session
from jinja2 import Template
from re import compile as re_compile
from moto.compat import OrderedDict
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, BaseModel, CloudFormationModel
from moto.core.utils import get_random_hex
from moto.core.utils import iso_8601_datetime_with_milliseconds
from moto.ec2.models import ec2_backends
@ -28,7 +28,7 @@ from .exceptions import (
)
class Database(BaseModel):
class Database(CloudFormationModel):
def __init__(self, **kwargs):
self.status = "available"
self.is_replica = False
@ -356,13 +356,22 @@ class Database(BaseModel):
"sqlserver-web": {"gp2": 20, "io1": 100, "standard": 20},
}[engine][storage_type]
@staticmethod
def cloudformation_name_type():
return "DBInstanceIdentifier"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbinstance.html
return "AWS::RDS::DBInstance"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
):
properties = cloudformation_json["Properties"]
db_instance_identifier = properties.get("DBInstanceIdentifier")
db_instance_identifier = properties.get(cls.cloudformation_name_type())
if not db_instance_identifier:
db_instance_identifier = resource_name.lower() + get_random_hex(12)
db_security_groups = properties.get("DBSecurityGroups")
@ -564,7 +573,7 @@ class Snapshot(BaseModel):
self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in tag_keys]
class SecurityGroup(BaseModel):
class SecurityGroup(CloudFormationModel):
def __init__(self, group_name, description, tags):
self.group_name = group_name
self.description = description
@ -627,6 +636,15 @@ class SecurityGroup(BaseModel):
def authorize_security_group(self, security_group):
self.ec2_security_groups.append(security_group)
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbsecuritygroup.html
return "AWS::RDS::DBSecurityGroup"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -671,7 +689,7 @@ class SecurityGroup(BaseModel):
backend.delete_security_group(self.group_name)
class SubnetGroup(BaseModel):
class SubnetGroup(CloudFormationModel):
def __init__(self, subnet_name, description, subnets, tags):
self.subnet_name = subnet_name
self.description = description
@ -726,13 +744,24 @@ class SubnetGroup(BaseModel):
)
return template.render(subnet_group=self)
@staticmethod
def cloudformation_name_type():
return "DBSubnetGroupName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbsubnetgroup.html
return "AWS::RDS::DBSubnetGroup"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
):
properties = cloudformation_json["Properties"]
subnet_name = resource_name.lower() + get_random_hex(12)
subnet_name = properties.get(cls.cloudformation_name_type())
if not subnet_name:
subnet_name = resource_name.lower() + get_random_hex(12)
description = properties["DBSubnetGroupDescription"]
subnet_ids = properties["SubnetIds"]
tags = properties.get("Tags")
@ -1441,7 +1470,7 @@ class OptionGroupOptionSetting(object):
return template.render(option_group_option_setting=self)
class DBParameterGroup(object):
class DBParameterGroup(CloudFormationModel):
def __init__(self, name, description, family, tags):
self.name = name
self.description = description
@ -1480,6 +1509,15 @@ class DBParameterGroup(object):
backend = rds2_backends[region_name]
backend.delete_db_parameter_group(self.name)
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbparametergroup.html
return "AWS::RDS::DBParameterGroup"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -6,7 +6,7 @@ import datetime
from boto3 import Session
from botocore.exceptions import ClientError
from moto.compat import OrderedDict
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, BaseModel, CloudFormationModel
from moto.core.utils import iso_8601_datetime_with_milliseconds
from moto.ec2 import ec2_backends
from .exceptions import (
@ -63,7 +63,7 @@ class TaggableResourceMixin(object):
return self.tags
class Cluster(TaggableResourceMixin, BaseModel):
class Cluster(TaggableResourceMixin, CloudFormationModel):
resource_type = "cluster"
@ -157,6 +157,15 @@ class Cluster(TaggableResourceMixin, BaseModel):
self.iam_roles_arn = iam_roles_arn or []
self.restored_from_snapshot = restored_from_snapshot
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html
return "AWS::Redshift::Cluster"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -170,6 +179,7 @@ class Cluster(TaggableResourceMixin, BaseModel):
].cluster_subnet_group_name
else:
subnet_group_name = None
cluster = redshift_backend.create_cluster(
cluster_identifier=resource_name,
node_type=properties.get("NodeType"),
@ -321,7 +331,7 @@ class SnapshotCopyGrant(TaggableResourceMixin, BaseModel):
}
class SubnetGroup(TaggableResourceMixin, BaseModel):
class SubnetGroup(TaggableResourceMixin, CloudFormationModel):
resource_type = "subnetgroup"
@ -342,6 +352,15 @@ class SubnetGroup(TaggableResourceMixin, BaseModel):
if not self.subnets:
raise InvalidSubnetError(subnet_ids)
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-clustersubnetgroup.html
return "AWS::Redshift::ClusterSubnetGroup"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -412,7 +431,7 @@ class SecurityGroup(TaggableResourceMixin, BaseModel):
}
class ParameterGroup(TaggableResourceMixin, BaseModel):
class ParameterGroup(TaggableResourceMixin, CloudFormationModel):
resource_type = "parametergroup"
@ -429,6 +448,15 @@ class ParameterGroup(TaggableResourceMixin, BaseModel):
self.group_family = group_family
self.description = description
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-clusterparametergroup.html
return "AWS::Redshift::ClusterParameterGroup"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -7,7 +7,7 @@ import random
import uuid
from jinja2 import Template
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, CloudFormationModel
ROUTE53_ID_CHOICE = string.ascii_uppercase + string.digits
@ -18,7 +18,7 @@ def create_route53_zone_id():
return "".join([random.choice(ROUTE53_ID_CHOICE) for _ in range(0, 15)])
class HealthCheck(BaseModel):
class HealthCheck(CloudFormationModel):
def __init__(self, health_check_id, health_check_args):
self.id = health_check_id
self.ip_address = health_check_args.get("ip_address")
@ -34,6 +34,15 @@ class HealthCheck(BaseModel):
def physical_resource_id(self):
return self.id
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53-healthcheck.html
return "AWS::Route53::HealthCheck"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -75,7 +84,7 @@ class HealthCheck(BaseModel):
return template.render(health_check=self)
class RecordSet(BaseModel):
class RecordSet(CloudFormationModel):
def __init__(self, kwargs):
self.name = kwargs.get("Name")
self.type_ = kwargs.get("Type")
@ -91,6 +100,15 @@ class RecordSet(BaseModel):
self.failover = kwargs.get("Failover")
self.geo_location = kwargs.get("GeoLocation")
@staticmethod
def cloudformation_name_type():
return "Name"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53-recordset.html
return "AWS::Route53::RecordSet"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -202,7 +220,7 @@ def reverse_domain_name(domain_name):
return ".".join(reversed(domain_name.split(".")))
class FakeZone(BaseModel):
class FakeZone(CloudFormationModel):
def __init__(self, name, id_, private_zone, comment=None):
self.name = name
self.id = id_
@ -267,6 +285,15 @@ class FakeZone(BaseModel):
def physical_resource_id(self):
return self.id
@staticmethod
def cloudformation_name_type():
return "Name"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53-hostedzone.html
return "AWS::Route53::HostedZone"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -278,7 +305,7 @@ class FakeZone(BaseModel):
return hosted_zone
class RecordSetGroup(BaseModel):
class RecordSetGroup(CloudFormationModel):
def __init__(self, hosted_zone_id, record_sets):
self.hosted_zone_id = hosted_zone_id
self.record_sets = record_sets
@ -287,6 +314,15 @@ class RecordSetGroup(BaseModel):
def physical_resource_id(self):
return "arn:aws:route53:::hostedzone/{0}".format(self.hosted_zone_id)
@staticmethod
def cloudformation_name_type():
return None
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53-recordsetgroup.html
return "AWS::Route53::RecordSetGroup"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -21,7 +21,7 @@ import uuid
import six
from bisect import insort
from moto.core import ACCOUNT_ID, BaseBackend, BaseModel
from moto.core import ACCOUNT_ID, BaseBackend, BaseModel, CloudFormationModel
from moto.core.utils import iso_8601_datetime_without_milliseconds_s3, rfc_1123_datetime
from moto.cloudwatch.models import MetricDatum
from moto.utilities.tagging_service import TaggingService
@ -763,7 +763,7 @@ class PublicAccessBlock(BaseModel):
}
class FakeBucket(BaseModel):
class FakeBucket(CloudFormationModel):
def __init__(self, name, region_name):
self.name = name
self.region_name = region_name
@ -1070,6 +1070,15 @@ class FakeBucket(BaseModel):
def physical_resource_id(self):
return self.name
@staticmethod
def cloudformation_name_type():
return "BucketName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-bucket.html
return "AWS::S3::Bucket"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -57,3 +57,8 @@ class InvalidRequestException(SecretsManagerClientError):
super(InvalidRequestException, self).__init__(
"InvalidRequestException", message
)
class ValidationException(SecretsManagerClientError):
def __init__(self, message):
super(ValidationException, self).__init__("ValidationException", message)

View File

@ -0,0 +1,44 @@
def _matcher(pattern, str):
for word in pattern.split(" "):
if word not in str:
return False
return True
def name(secret, names):
for n in names:
if _matcher(n, secret["name"]):
return True
return False
def description(secret, descriptions):
for d in descriptions:
if _matcher(d, secret["description"]):
return True
return False
def tag_key(secret, tag_keys):
for k in tag_keys:
for tag in secret["tags"]:
if _matcher(k, tag["Key"]):
return True
return False
def tag_value(secret, tag_values):
for v in tag_values:
for tag in secret["tags"]:
if _matcher(v, tag["Value"]):
return True
return False
def all(secret, values):
return (
name(secret, values)
or description(secret, values)
or tag_key(secret, values)
or tag_value(secret, values)
)

View File

@ -18,6 +18,31 @@ from .exceptions import (
ClientError,
)
from .utils import random_password, secret_arn, get_secret_name_from_arn
from .list_secrets.filters import all, tag_key, tag_value, description, name
_filter_functions = {
"all": all,
"name": name,
"description": description,
"tag-key": tag_key,
"tag-value": tag_value,
}
def filter_keys():
return list(_filter_functions.keys())
def _matches(secret, filters):
is_match = True
for f in filters:
# Filter names are pre-validated in the resource layer
filter_function = _filter_functions.get(f["Key"])
is_match = is_match and filter_function(secret, f["Values"])
return is_match
class SecretsManager(BaseModel):
@ -442,35 +467,35 @@ class SecretsManagerBackend(BaseBackend):
return response
def list_secrets(self, max_results, next_token):
def list_secrets(self, filters, max_results, next_token):
# TODO implement pagination and limits
secret_list = []
for secret in self.secrets.values():
if _matches(secret, filters):
versions_to_stages = {}
for version_id, version in secret["versions"].items():
versions_to_stages[version_id] = version["version_stages"]
versions_to_stages = {}
for version_id, version in secret["versions"].items():
versions_to_stages[version_id] = version["version_stages"]
secret_list.append(
{
"ARN": secret_arn(self.region, secret["secret_id"]),
"DeletedDate": secret.get("deleted_date", None),
"Description": secret.get("description", ""),
"KmsKeyId": "",
"LastAccessedDate": None,
"LastChangedDate": None,
"LastRotatedDate": None,
"Name": secret["name"],
"RotationEnabled": secret["rotation_enabled"],
"RotationLambdaARN": secret["rotation_lambda_arn"],
"RotationRules": {
"AutomaticallyAfterDays": secret["auto_rotate_after_days"]
},
"SecretVersionsToStages": versions_to_stages,
"Tags": secret["tags"],
}
)
secret_list.append(
{
"ARN": secret_arn(self.region, secret["secret_id"]),
"DeletedDate": secret.get("deleted_date", None),
"Description": secret.get("description", ""),
"KmsKeyId": "",
"LastAccessedDate": None,
"LastChangedDate": None,
"LastRotatedDate": None,
"Name": secret["name"],
"RotationEnabled": secret["rotation_enabled"],
"RotationLambdaARN": secret["rotation_lambda_arn"],
"RotationRules": {
"AutomaticallyAfterDays": secret["auto_rotate_after_days"]
},
"SecretVersionsToStages": versions_to_stages,
"Tags": secret["tags"],
}
)
return secret_list, None

View File

@ -1,13 +1,36 @@
from __future__ import unicode_literals
from moto.core.responses import BaseResponse
from moto.secretsmanager.exceptions import InvalidRequestException
from moto.secretsmanager.exceptions import (
InvalidRequestException,
InvalidParameterException,
ValidationException,
)
from .models import secretsmanager_backends
from .models import secretsmanager_backends, filter_keys
import json
def _validate_filters(filters):
for idx, f in enumerate(filters):
filter_key = f.get("Key", None)
filter_values = f.get("Values", None)
if filter_key is None:
raise InvalidParameterException("Invalid filter key")
if filter_key not in filter_keys():
raise ValidationException(
"1 validation error detected: Value '{}' at 'filters.{}.member.key' failed to satisfy constraint: "
"Member must satisfy enum value set: [all, name, tag-key, description, tag-value]".format(
filter_key, idx + 1
)
)
if filter_values is None:
raise InvalidParameterException(
"Invalid filter values for key: {}".format(filter_key)
)
class SecretsManagerResponse(BaseResponse):
def get_secret_value(self):
secret_id = self._get_param("SecretId")
@ -102,10 +125,12 @@ class SecretsManagerResponse(BaseResponse):
)
def list_secrets(self):
filters = self._get_param("Filters", if_none=[])
_validate_filters(filters)
max_results = self._get_int_param("MaxResults")
next_token = self._get_param("NextToken")
secret_list, next_token = secretsmanager_backends[self.region].list_secrets(
max_results=max_results, next_token=next_token
filters=filters, max_results=max_results, next_token=next_token
)
return json.dumps(dict(SecretList=secret_list, NextToken=next_token))

View File

@ -11,7 +11,7 @@ import re
from boto3 import Session
from moto.compat import OrderedDict
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, BaseModel, CloudFormationModel
from moto.core.utils import (
iso_8601_datetime_with_milliseconds,
camelcase_to_underscores,
@ -37,7 +37,7 @@ DEFAULT_PAGE_SIZE = 100
MAXIMUM_MESSAGE_LENGTH = 262144 # 256 KiB
class Topic(BaseModel):
class Topic(CloudFormationModel):
def __init__(self, name, sns_backend):
self.name = name
self.sns_backend = sns_backend
@ -87,6 +87,15 @@ class Topic(BaseModel):
def policy(self, policy):
self._policy_json = json.loads(policy)
@staticmethod
def cloudformation_name_type():
return "TopicName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sns-topic.html
return "AWS::SNS::Topic"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
@ -94,7 +103,7 @@ class Topic(BaseModel):
sns_backend = sns_backends[region_name]
properties = cloudformation_json["Properties"]
topic = sns_backend.create_topic(properties.get("TopicName"))
topic = sns_backend.create_topic(properties.get(cls.cloudformation_name_type()))
for subscription in properties.get("Subscription", []):
sns_backend.subscribe(
topic.arn, subscription["Endpoint"], subscription["Protocol"]

View File

@ -12,7 +12,7 @@ from xml.sax.saxutils import escape
from boto3 import Session
from moto.core.exceptions import RESTError
from moto.core import BaseBackend, BaseModel
from moto.core import BaseBackend, BaseModel, CloudFormationModel
from moto.core.utils import (
camelcase_to_underscores,
get_random_message_id,
@ -188,7 +188,7 @@ class Message(BaseModel):
return False
class Queue(BaseModel):
class Queue(CloudFormationModel):
BASE_ATTRIBUTES = [
"ApproximateNumberOfMessages",
"ApproximateNumberOfMessagesDelayed",
@ -354,6 +354,15 @@ class Queue(BaseModel):
),
)
@staticmethod
def cloudformation_name_type():
return "QueueName"
@staticmethod
def cloudformation_type():
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html
return "AWS::SQS::Queue"
@classmethod
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name

View File

@ -219,6 +219,18 @@ def test_expression_tokenizer_single_set_action_attribute_name_valid_key():
]
def test_expression_tokenizer_single_set_action_attribute_name_leading_number():
set_action = "SET attr=#0"
token_list = ExpressionTokenizer.make_list(set_action)
assert token_list == [
Token(Token.ATTRIBUTE, "SET"),
Token(Token.WHITESPACE, " "),
Token(Token.ATTRIBUTE, "attr"),
Token(Token.EQUAL_SIGN, "="),
Token(Token.ATTRIBUTE_NAME, "#0"),
]
def test_expression_tokenizer_just_a_pipe():
set_action = "|"
try:

View File

@ -783,6 +783,16 @@ def test_ami_registration():
assert images[0]["State"] == "available", "State should be available."
@mock_ec2
def test_ami_registration():
ec2 = boto3.client("ec2", region_name="us-east-1")
image_id = ec2.register_image(Name="test-register-image").get("ImageId", "")
images = ec2.describe_images(ImageIds=[image_id]).get("Images", [])
assert images[0]["Name"] == "test-register-image", "No image was registered."
assert images[0]["RootDeviceName"] == "/dev/sda1", "Wrong root device name."
assert images[0]["State"] == "available", "State should be available."
@mock_ec2
def test_ami_filter_wildcard():
ec2_resource = boto3.resource("ec2", region_name="us-west-1")

View File

@ -562,19 +562,176 @@ def test_snapshot_attribute():
cm.exception.status.should.equal(400)
cm.exception.request_id.should_not.be.none
# Error: Add or remove with user ID instead of group
conn.modify_snapshot_attribute.when.called_with(
snapshot.id,
attribute="createVolumePermission",
operation="add",
user_ids=["user"],
).should.throw(NotImplementedError)
conn.modify_snapshot_attribute.when.called_with(
snapshot.id,
attribute="createVolumePermission",
operation="remove",
user_ids=["user"],
).should.throw(NotImplementedError)
@mock_ec2
def test_modify_snapshot_attribute():
import copy
ec2_client = boto3.client("ec2", region_name="us-east-1")
response = ec2_client.create_volume(Size=80, AvailabilityZone="us-east-1a")
volume = boto3.resource("ec2", region_name="us-east-1").Volume(response["VolumeId"])
snapshot = volume.create_snapshot()
# Baseline
attributes = ec2_client.describe_snapshot_attribute(
SnapshotId=snapshot.id, Attribute="createVolumePermission"
)
assert not attributes[
"CreateVolumePermissions"
], "Snapshot should have no permissions."
ADD_GROUP_ARGS = {
"SnapshotId": snapshot.id,
"Attribute": "createVolumePermission",
"OperationType": "add",
"GroupNames": ["all"],
}
REMOVE_GROUP_ARGS = {
"SnapshotId": snapshot.id,
"Attribute": "createVolumePermission",
"OperationType": "remove",
"GroupNames": ["all"],
}
# Add 'all' group and confirm
with assert_raises(ClientError) as cm:
ec2_client.modify_snapshot_attribute(**dict(ADD_GROUP_ARGS, **{"DryRun": True}))
cm.exception.response["Error"]["Code"].should.equal("DryRunOperation")
cm.exception.response["ResponseMetadata"]["RequestId"].should_not.be.none
cm.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ec2_client.modify_snapshot_attribute(**ADD_GROUP_ARGS)
attributes = ec2_client.describe_snapshot_attribute(
SnapshotId=snapshot.id, Attribute="createVolumePermission"
)
assert attributes["CreateVolumePermissions"] == [
{"Group": "all"}
], "This snapshot should have public group permissions."
# Add is idempotent
ec2_client.modify_snapshot_attribute.when.called_with(
**ADD_GROUP_ARGS
).should_not.throw(ClientError)
assert attributes["CreateVolumePermissions"] == [
{"Group": "all"}
], "This snapshot should have public group permissions."
# Remove 'all' group and confirm
with assert_raises(ClientError) as ex:
ec2_client.modify_snapshot_attribute(
**dict(REMOVE_GROUP_ARGS, **{"DryRun": True})
)
cm.exception.response["Error"]["Code"].should.equal("DryRunOperation")
cm.exception.response["ResponseMetadata"]["RequestId"].should_not.be.none
cm.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ec2_client.modify_snapshot_attribute(**REMOVE_GROUP_ARGS)
attributes = ec2_client.describe_snapshot_attribute(
SnapshotId=snapshot.id, Attribute="createVolumePermission"
)
assert not attributes[
"CreateVolumePermissions"
], "This snapshot should have no permissions."
# Remove is idempotent
ec2_client.modify_snapshot_attribute.when.called_with(
**REMOVE_GROUP_ARGS
).should_not.throw(ClientError)
assert not attributes[
"CreateVolumePermissions"
], "This snapshot should have no permissions."
# Error: Add with group != 'all'
with assert_raises(ClientError) as cm:
ec2_client.modify_snapshot_attribute(
SnapshotId=snapshot.id,
Attribute="createVolumePermission",
OperationType="add",
GroupNames=["everyone"],
)
cm.exception.response["Error"]["Code"].should.equal("InvalidAMIAttributeItemValue")
cm.exception.response["ResponseMetadata"]["RequestId"].should_not.be.none
cm.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
# Error: Add with invalid snapshot ID
with assert_raises(ClientError) as cm:
ec2_client.modify_snapshot_attribute(
SnapshotId="snapshot-abcd1234",
Attribute="createVolumePermission",
OperationType="add",
GroupNames=["all"],
)
cm.exception.response["Error"]["Code"].should.equal("InvalidSnapshot.NotFound")
cm.exception.response["ResponseMetadata"]["RequestId"].should_not.be.none
cm.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
# Error: Remove with invalid snapshot ID
with assert_raises(ClientError) as cm:
ec2_client.modify_snapshot_attribute(
SnapshotId="snapshot-abcd1234",
Attribute="createVolumePermission",
OperationType="remove",
GroupNames=["all"],
)
cm.exception.response["Error"]["Code"].should.equal("InvalidSnapshot.NotFound")
cm.exception.response["ResponseMetadata"]["RequestId"].should_not.be.none
cm.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
# Test adding user id
ec2_client.modify_snapshot_attribute(
SnapshotId=snapshot.id,
Attribute="createVolumePermission",
OperationType="add",
UserIds=["1234567891"],
)
attributes = ec2_client.describe_snapshot_attribute(
SnapshotId=snapshot.id, Attribute="createVolumePermission"
)
assert len(attributes["CreateVolumePermissions"]) == 1
# Test adding user id again along with additional.
ec2_client.modify_snapshot_attribute(
SnapshotId=snapshot.id,
Attribute="createVolumePermission",
OperationType="add",
UserIds=["1234567891", "2345678912"],
)
attributes = ec2_client.describe_snapshot_attribute(
SnapshotId=snapshot.id, Attribute="createVolumePermission"
)
assert len(attributes["CreateVolumePermissions"]) == 2
# Test removing both user IDs.
ec2_client.modify_snapshot_attribute(
SnapshotId=snapshot.id,
Attribute="createVolumePermission",
OperationType="remove",
UserIds=["1234567891", "2345678912"],
)
attributes = ec2_client.describe_snapshot_attribute(
SnapshotId=snapshot.id, Attribute="createVolumePermission"
)
assert len(attributes["CreateVolumePermissions"]) == 0
# Idempotency when removing users.
ec2_client.modify_snapshot_attribute(
SnapshotId=snapshot.id,
Attribute="createVolumePermission",
OperationType="remove",
UserIds=["1234567891"],
)
attributes = ec2_client.describe_snapshot_attribute(
SnapshotId=snapshot.id, Attribute="createVolumePermission"
)
assert len(attributes["CreateVolumePermissions"]) == 0
@mock_ec2_deprecated

View File

@ -10,6 +10,7 @@ from botocore.exceptions import ClientError
from nose.tools import assert_raises
from moto import mock_organizations
from moto.core import ACCOUNT_ID
from moto.organizations import utils
from .organizations_test_utils import (
validate_organization,
@ -64,8 +65,11 @@ def test_describe_organization_exception():
response = client.describe_organization()
ex = e.exception
ex.operation_name.should.equal("DescribeOrganization")
ex.response["Error"]["Code"].should.equal("400")
ex.response["Error"]["Message"].should.contain("AWSOrganizationsNotInUseException")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("AWSOrganizationsNotInUseException")
ex.response["Error"]["Message"].should.equal(
"Your account is not a member of an organization."
)
# Organizational Units
@ -193,8 +197,11 @@ def test_describe_account_exception():
response = client.describe_account(AccountId=utils.make_random_account_id())
ex = e.exception
ex.operation_name.should.equal("DescribeAccount")
ex.response["Error"]["Code"].should.equal("400")
ex.response["Error"]["Message"].should.contain("AccountNotFoundException")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("AccountNotFoundException")
ex.response["Error"]["Message"].should.equal(
"You specified an account that doesn't exist."
)
@mock_organizations
@ -340,8 +347,9 @@ def test_list_children_exception():
response = client.list_children(ParentId=root_id, ChildType="BLEE")
ex = e.exception
ex.operation_name.should.equal("ListChildren")
ex.response["Error"]["Code"].should.equal("400")
ex.response["Error"]["Message"].should.contain("InvalidInputException")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("InvalidInputException")
ex.response["Error"]["Message"].should.equal("You specified an invalid value.")
# Service Control Policies
@ -405,8 +413,9 @@ def test_describe_policy_exception():
response = client.describe_policy(PolicyId="meaninglessstring")
ex = e.exception
ex.operation_name.should.equal("DescribePolicy")
ex.response["Error"]["Code"].should.equal("400")
ex.response["Error"]["Message"].should.contain("InvalidInputException")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("InvalidInputException")
ex.response["Error"]["Message"].should.equal("You specified an invalid value.")
@mock_organizations
@ -517,16 +526,20 @@ def test_attach_policy_exception():
response = client.attach_policy(PolicyId=policy_id, TargetId=account_id)
ex = e.exception
ex.operation_name.should.equal("AttachPolicy")
ex.response["Error"]["Code"].should.equal("400")
ex.response["Error"]["Message"].should.contain("AccountNotFoundException")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("AccountNotFoundException")
ex.response["Error"]["Message"].should.equal(
"You specified an account that doesn't exist."
)
with assert_raises(ClientError) as e:
response = client.attach_policy(
PolicyId=policy_id, TargetId="meaninglessstring"
)
ex = e.exception
ex.operation_name.should.equal("AttachPolicy")
ex.response["Error"]["Code"].should.equal("400")
ex.response["Error"]["Message"].should.contain("InvalidInputException")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("InvalidInputException")
ex.response["Error"]["Message"].should.equal("You specified an invalid value.")
@mock_organizations
@ -636,16 +649,20 @@ def test_list_policies_for_target_exception():
)
ex = e.exception
ex.operation_name.should.equal("ListPoliciesForTarget")
ex.response["Error"]["Code"].should.equal("400")
ex.response["Error"]["Message"].should.contain("AccountNotFoundException")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("AccountNotFoundException")
ex.response["Error"]["Message"].should.equal(
"You specified an account that doesn't exist."
)
with assert_raises(ClientError) as e:
response = client.list_policies_for_target(
TargetId="meaninglessstring", Filter="SERVICE_CONTROL_POLICY"
)
ex = e.exception
ex.operation_name.should.equal("ListPoliciesForTarget")
ex.response["Error"]["Code"].should.equal("400")
ex.response["Error"]["Message"].should.contain("InvalidInputException")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("InvalidInputException")
ex.response["Error"]["Message"].should.equal("You specified an invalid value.")
@mock_organizations
@ -694,8 +711,9 @@ def test_list_targets_for_policy_exception():
response = client.list_targets_for_policy(PolicyId="meaninglessstring")
ex = e.exception
ex.operation_name.should.equal("ListTargetsForPolicy")
ex.response["Error"]["Code"].should.equal("400")
ex.response["Error"]["Message"].should.contain("InvalidInputException")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("InvalidInputException")
ex.response["Error"]["Message"].should.equal("You specified an invalid value.")
@mock_organizations
@ -947,3 +965,343 @@ def test_disable_aws_service_access_errors():
ex.response["Error"]["Message"].should.equal(
"You specified an unrecognized service principal."
)
@mock_organizations
def test_register_delegated_administrator():
# given
client = boto3.client("organizations", region_name="us-east-1")
org_id = client.create_organization(FeatureSet="ALL")["Organization"]["Id"]
account_id = client.create_account(AccountName=mockname, Email=mockemail)[
"CreateAccountStatus"
]["AccountId"]
# when
client.register_delegated_administrator(
AccountId=account_id, ServicePrincipal="ssm.amazonaws.com"
)
# then
response = client.list_delegated_administrators()
response["DelegatedAdministrators"].should.have.length_of(1)
admin = response["DelegatedAdministrators"][0]
admin["Id"].should.equal(account_id)
admin["Arn"].should.equal(
"arn:aws:organizations::{0}:account/{1}/{2}".format(
ACCOUNT_ID, org_id, account_id
)
)
admin["Email"].should.equal(mockemail)
admin["Name"].should.equal(mockname)
admin["Status"].should.equal("ACTIVE")
admin["JoinedMethod"].should.equal("CREATED")
admin["JoinedTimestamp"].should.be.a(datetime)
admin["DelegationEnabledDate"].should.be.a(datetime)
@mock_organizations
def test_register_delegated_administrator_errors():
# given
client = boto3.client("organizations", region_name="us-east-1")
client.create_organization(FeatureSet="ALL")
account_id = client.create_account(AccountName=mockname, Email=mockemail)[
"CreateAccountStatus"
]["AccountId"]
client.register_delegated_administrator(
AccountId=account_id, ServicePrincipal="ssm.amazonaws.com"
)
# register master Account
# when
with assert_raises(ClientError) as e:
client.register_delegated_administrator(
AccountId=ACCOUNT_ID, ServicePrincipal="ssm.amazonaws.com"
)
# then
ex = e.exception
ex.operation_name.should.equal("RegisterDelegatedAdministrator")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("ConstraintViolationException")
ex.response["Error"]["Message"].should.equal(
"You cannot register master account/yourself as delegated administrator for your organization."
)
# register not existing Account
# when
with assert_raises(ClientError) as e:
client.register_delegated_administrator(
AccountId="000000000000", ServicePrincipal="ssm.amazonaws.com"
)
# then
ex = e.exception
ex.operation_name.should.equal("RegisterDelegatedAdministrator")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("AccountNotFoundException")
ex.response["Error"]["Message"].should.equal(
"You specified an account that doesn't exist."
)
# register not supported service
# when
with assert_raises(ClientError) as e:
client.register_delegated_administrator(
AccountId=account_id, ServicePrincipal="moto.amazonaws.com"
)
# then
ex = e.exception
ex.operation_name.should.equal("RegisterDelegatedAdministrator")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("InvalidInputException")
ex.response["Error"]["Message"].should.equal(
"You specified an unrecognized service principal."
)
# register service again
# when
with assert_raises(ClientError) as e:
client.register_delegated_administrator(
AccountId=account_id, ServicePrincipal="ssm.amazonaws.com"
)
# then
ex = e.exception
ex.operation_name.should.equal("RegisterDelegatedAdministrator")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("AccountAlreadyRegisteredException")
ex.response["Error"]["Message"].should.equal(
"The provided account is already a delegated administrator for your organization."
)
@mock_organizations
def test_list_delegated_administrators():
# given
client = boto3.client("organizations", region_name="us-east-1")
org_id = client.create_organization(FeatureSet="ALL")["Organization"]["Id"]
account_id_1 = client.create_account(AccountName=mockname, Email=mockemail)[
"CreateAccountStatus"
]["AccountId"]
account_id_2 = client.create_account(AccountName=mockname, Email=mockemail)[
"CreateAccountStatus"
]["AccountId"]
client.register_delegated_administrator(
AccountId=account_id_1, ServicePrincipal="ssm.amazonaws.com"
)
client.register_delegated_administrator(
AccountId=account_id_2, ServicePrincipal="guardduty.amazonaws.com"
)
# when
response = client.list_delegated_administrators()
# then
response["DelegatedAdministrators"].should.have.length_of(2)
sorted([admin["Id"] for admin in response["DelegatedAdministrators"]]).should.equal(
sorted([account_id_1, account_id_2])
)
# when
response = client.list_delegated_administrators(
ServicePrincipal="ssm.amazonaws.com"
)
# then
response["DelegatedAdministrators"].should.have.length_of(1)
admin = response["DelegatedAdministrators"][0]
admin["Id"].should.equal(account_id_1)
admin["Arn"].should.equal(
"arn:aws:organizations::{0}:account/{1}/{2}".format(
ACCOUNT_ID, org_id, account_id_1
)
)
admin["Email"].should.equal(mockemail)
admin["Name"].should.equal(mockname)
admin["Status"].should.equal("ACTIVE")
admin["JoinedMethod"].should.equal("CREATED")
admin["JoinedTimestamp"].should.be.a(datetime)
admin["DelegationEnabledDate"].should.be.a(datetime)
@mock_organizations
def test_list_delegated_administrators_erros():
# given
client = boto3.client("organizations", region_name="us-east-1")
client.create_organization(FeatureSet="ALL")
# list not supported service
# when
with assert_raises(ClientError) as e:
client.list_delegated_administrators(ServicePrincipal="moto.amazonaws.com")
# then
ex = e.exception
ex.operation_name.should.equal("ListDelegatedAdministrators")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("InvalidInputException")
ex.response["Error"]["Message"].should.equal(
"You specified an unrecognized service principal."
)
@mock_organizations
def test_list_delegated_services_for_account():
# given
client = boto3.client("organizations", region_name="us-east-1")
client.create_organization(FeatureSet="ALL")
account_id = client.create_account(AccountName=mockname, Email=mockemail)[
"CreateAccountStatus"
]["AccountId"]
client.register_delegated_administrator(
AccountId=account_id, ServicePrincipal="ssm.amazonaws.com"
)
client.register_delegated_administrator(
AccountId=account_id, ServicePrincipal="guardduty.amazonaws.com"
)
# when
response = client.list_delegated_services_for_account(AccountId=account_id)
# then
response["DelegatedServices"].should.have.length_of(2)
sorted(
[service["ServicePrincipal"] for service in response["DelegatedServices"]]
).should.equal(["guardduty.amazonaws.com", "ssm.amazonaws.com"])
@mock_organizations
def test_list_delegated_services_for_account_erros():
# given
client = boto3.client("organizations", region_name="us-east-1")
client.create_organization(FeatureSet="ALL")
# list services for not existing Account
# when
with assert_raises(ClientError) as e:
client.list_delegated_services_for_account(AccountId="000000000000")
# then
ex = e.exception
ex.operation_name.should.equal("ListDelegatedServicesForAccount")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("AWSOrganizationsNotInUseException")
ex.response["Error"]["Message"].should.equal(
"Your account is not a member of an organization."
)
# list services for not registered Account
# when
with assert_raises(ClientError) as e:
client.list_delegated_services_for_account(AccountId=ACCOUNT_ID)
# then
ex = e.exception
ex.operation_name.should.equal("ListDelegatedServicesForAccount")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("AccountNotRegisteredException")
ex.response["Error"]["Message"].should.equal(
"The provided account is not a registered delegated administrator for your organization."
)
@mock_organizations
def test_deregister_delegated_administrator():
# given
client = boto3.client("organizations", region_name="us-east-1")
client.create_organization(FeatureSet="ALL")
account_id = client.create_account(AccountName=mockname, Email=mockemail)[
"CreateAccountStatus"
]["AccountId"]
client.register_delegated_administrator(
AccountId=account_id, ServicePrincipal="ssm.amazonaws.com"
)
# when
client.deregister_delegated_administrator(
AccountId=account_id, ServicePrincipal="ssm.amazonaws.com"
)
# then
response = client.list_delegated_administrators()
response["DelegatedAdministrators"].should.have.length_of(0)
@mock_organizations
def test_deregister_delegated_administrator_erros():
# given
client = boto3.client("organizations", region_name="us-east-1")
client.create_organization(FeatureSet="ALL")
account_id = client.create_account(AccountName=mockname, Email=mockemail)[
"CreateAccountStatus"
]["AccountId"]
# deregister master Account
# when
with assert_raises(ClientError) as e:
client.deregister_delegated_administrator(
AccountId=ACCOUNT_ID, ServicePrincipal="ssm.amazonaws.com"
)
# then
ex = e.exception
ex.operation_name.should.equal("DeregisterDelegatedAdministrator")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("ConstraintViolationException")
ex.response["Error"]["Message"].should.equal(
"You cannot register master account/yourself as delegated administrator for your organization."
)
# deregister not existing Account
# when
with assert_raises(ClientError) as e:
client.deregister_delegated_administrator(
AccountId="000000000000", ServicePrincipal="ssm.amazonaws.com"
)
# then
ex = e.exception
ex.operation_name.should.equal("DeregisterDelegatedAdministrator")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("AccountNotFoundException")
ex.response["Error"]["Message"].should.equal(
"You specified an account that doesn't exist."
)
# deregister not registered Account
# when
with assert_raises(ClientError) as e:
client.deregister_delegated_administrator(
AccountId=account_id, ServicePrincipal="ssm.amazonaws.com"
)
# then
ex = e.exception
ex.operation_name.should.equal("DeregisterDelegatedAdministrator")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("AccountNotRegisteredException")
ex.response["Error"]["Message"].should.equal(
"The provided account is not a registered delegated administrator for your organization."
)
# given
client.register_delegated_administrator(
AccountId=account_id, ServicePrincipal="ssm.amazonaws.com"
)
# deregister not registered service
# when
with assert_raises(ClientError) as e:
client.deregister_delegated_administrator(
AccountId=account_id, ServicePrincipal="guardduty.amazonaws.com"
)
# then
ex = e.exception
ex.operation_name.should.equal("DeregisterDelegatedAdministrator")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("InvalidInputException")
ex.response["Error"]["Message"].should.equal(
"You specified an unrecognized service principal."
)

View File

@ -0,0 +1,251 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import boto3
from moto import mock_secretsmanager
from botocore.exceptions import ClientError
import sure # noqa
from nose.tools import assert_raises
try:
from nose.tools import assert_items_equal
except ImportError:
from nose.tools import assert_count_equal as assert_items_equal
def boto_client():
return boto3.client("secretsmanager", region_name="us-west-2")
@mock_secretsmanager
def test_empty():
conn = boto_client()
secrets = conn.list_secrets()
assert_items_equal(secrets["SecretList"], [])
@mock_secretsmanager
def test_list_secrets():
conn = boto_client()
conn.create_secret(Name="test-secret", SecretString="foosecret")
conn.create_secret(
Name="test-secret-2",
SecretString="barsecret",
Tags=[{"Key": "a", "Value": "1"}],
)
secrets = conn.list_secrets()
assert secrets["SecretList"][0]["ARN"] is not None
assert secrets["SecretList"][0]["Name"] == "test-secret"
assert secrets["SecretList"][1]["ARN"] is not None
assert secrets["SecretList"][1]["Name"] == "test-secret-2"
assert secrets["SecretList"][1]["Tags"] == [{"Key": "a", "Value": "1"}]
@mock_secretsmanager
def test_with_name_filter():
conn = boto_client()
conn.create_secret(Name="foo", SecretString="secret")
conn.create_secret(Name="bar", SecretString="secret")
secrets = conn.list_secrets(Filters=[{"Key": "name", "Values": ["foo"]}])
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
assert_items_equal(secret_names, ["foo"])
@mock_secretsmanager
def test_with_tag_key_filter():
conn = boto_client()
conn.create_secret(
Name="foo", SecretString="secret", Tags=[{"Key": "baz", "Value": "1"}]
)
conn.create_secret(Name="bar", SecretString="secret")
secrets = conn.list_secrets(Filters=[{"Key": "tag-key", "Values": ["baz"]}])
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
assert_items_equal(secret_names, ["foo"])
@mock_secretsmanager
def test_with_tag_value_filter():
conn = boto_client()
conn.create_secret(
Name="foo", SecretString="secret", Tags=[{"Key": "1", "Value": "baz"}]
)
conn.create_secret(Name="bar", SecretString="secret")
secrets = conn.list_secrets(Filters=[{"Key": "tag-value", "Values": ["baz"]}])
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
assert_items_equal(secret_names, ["foo"])
@mock_secretsmanager
def test_with_description_filter():
conn = boto_client()
conn.create_secret(Name="foo", SecretString="secret", Description="baz qux")
conn.create_secret(Name="bar", SecretString="secret")
secrets = conn.list_secrets(Filters=[{"Key": "description", "Values": ["baz"]}])
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
assert_items_equal(secret_names, ["foo"])
@mock_secretsmanager
def test_with_all_filter():
# The 'all' filter will match a secret that contains ANY field with the criteria. In other words an implicit OR.
conn = boto_client()
conn.create_secret(Name="foo", SecretString="secret")
conn.create_secret(Name="bar", SecretString="secret", Description="foo")
conn.create_secret(
Name="baz", SecretString="secret", Tags=[{"Key": "foo", "Value": "1"}]
)
conn.create_secret(
Name="qux", SecretString="secret", Tags=[{"Key": "1", "Value": "foo"}]
)
conn.create_secret(
Name="multi", SecretString="secret", Tags=[{"Key": "foo", "Value": "foo"}]
)
conn.create_secret(Name="none", SecretString="secret")
secrets = conn.list_secrets(Filters=[{"Key": "all", "Values": ["foo"]}])
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
assert_items_equal(secret_names, ["foo", "bar", "baz", "qux", "multi"])
@mock_secretsmanager
def test_with_no_filter_key():
conn = boto_client()
with assert_raises(ClientError) as ire:
conn.list_secrets(Filters=[{"Values": ["foo"]}])
ire.exception.response["Error"]["Code"].should.equal("InvalidParameterException")
ire.exception.response["Error"]["Message"].should.equal("Invalid filter key")
@mock_secretsmanager
def test_with_no_filter_values():
conn = boto_client()
conn.create_secret(Name="foo", SecretString="secret", Description="hello")
with assert_raises(ClientError) as ire:
conn.list_secrets(Filters=[{"Key": "description"}])
ire.exception.response["Error"]["Code"].should.equal("InvalidParameterException")
ire.exception.response["Error"]["Message"].should.equal(
"Invalid filter values for key: description"
)
@mock_secretsmanager
def test_with_invalid_filter_key():
conn = boto_client()
with assert_raises(ClientError) as ire:
conn.list_secrets(Filters=[{"Key": "invalid", "Values": ["foo"]}])
ire.exception.response["Error"]["Code"].should.equal("ValidationException")
ire.exception.response["Error"]["Message"].should.equal(
"1 validation error detected: Value 'invalid' at 'filters.1.member.key' failed to satisfy constraint: Member "
"must satisfy enum value set: [all, name, tag-key, description, tag-value]"
)
@mock_secretsmanager
def test_with_duplicate_filter_keys():
# Multiple filters with the same key combine with an implicit AND operator
conn = boto_client()
conn.create_secret(Name="foo", SecretString="secret", Description="one two")
conn.create_secret(Name="bar", SecretString="secret", Description="one")
conn.create_secret(Name="baz", SecretString="secret", Description="two")
conn.create_secret(Name="qux", SecretString="secret", Description="unrelated")
secrets = conn.list_secrets(
Filters=[
{"Key": "description", "Values": ["one"]},
{"Key": "description", "Values": ["two"]},
]
)
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
assert_items_equal(secret_names, ["foo"])
@mock_secretsmanager
def test_with_multiple_filters():
# Multiple filters combine with an implicit AND operator
conn = boto_client()
conn.create_secret(
Name="foo", SecretString="secret", Tags=[{"Key": "right", "Value": "right"}]
)
conn.create_secret(
Name="bar", SecretString="secret", Tags=[{"Key": "right", "Value": "wrong"}]
)
conn.create_secret(
Name="baz", SecretString="secret", Tags=[{"Key": "wrong", "Value": "right"}]
)
conn.create_secret(
Name="qux", SecretString="secret", Tags=[{"Key": "wrong", "Value": "wrong"}]
)
secrets = conn.list_secrets(
Filters=[
{"Key": "tag-key", "Values": ["right"]},
{"Key": "tag-value", "Values": ["right"]},
]
)
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
assert_items_equal(secret_names, ["foo"])
@mock_secretsmanager
def test_with_filter_with_multiple_values():
conn = boto_client()
conn.create_secret(Name="foo", SecretString="secret")
conn.create_secret(Name="bar", SecretString="secret")
conn.create_secret(Name="baz", SecretString="secret")
secrets = conn.list_secrets(Filters=[{"Key": "name", "Values": ["foo", "bar"]}])
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
assert_items_equal(secret_names, ["foo", "bar"])
@mock_secretsmanager
def test_with_filter_with_value_with_multiple_words():
conn = boto_client()
conn.create_secret(Name="foo", SecretString="secret", Description="one two")
conn.create_secret(Name="bar", SecretString="secret", Description="one and two")
conn.create_secret(Name="baz", SecretString="secret", Description="one")
conn.create_secret(Name="qux", SecretString="secret", Description="two")
conn.create_secret(Name="none", SecretString="secret", Description="unrelated")
secrets = conn.list_secrets(Filters=[{"Key": "description", "Values": ["one two"]}])
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
assert_items_equal(secret_names, ["foo", "bar"])

View File

@ -459,36 +459,6 @@ def test_describe_secret_that_does_not_match():
result = conn.get_secret_value(SecretId="i-dont-match")
@mock_secretsmanager
def test_list_secrets_empty():
conn = boto3.client("secretsmanager", region_name="us-west-2")
secrets = conn.list_secrets()
assert secrets["SecretList"] == []
@mock_secretsmanager
def test_list_secrets():
conn = boto3.client("secretsmanager", region_name="us-west-2")
conn.create_secret(Name="test-secret", SecretString="foosecret")
conn.create_secret(
Name="test-secret-2",
SecretString="barsecret",
Tags=[{"Key": "a", "Value": "1"}],
)
secrets = conn.list_secrets()
assert secrets["SecretList"][0]["ARN"] is not None
assert secrets["SecretList"][0]["Name"] == "test-secret"
assert secrets["SecretList"][1]["ARN"] is not None
assert secrets["SecretList"][1]["Name"] == "test-secret-2"
assert secrets["SecretList"][1]["Tags"] == [{"Key": "a", "Value": "1"}]
@mock_secretsmanager
def test_restore_secret():
conn = boto3.client("secretsmanager", region_name="us-west-2")