Merge branch 'master' into redshift-copy-grants

This commit is contained in:
Steve Pulec 2018-03-06 22:11:34 -05:00 committed by GitHub
commit 8a0cf49b7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 420 additions and 25 deletions

View file

@ -3,7 +3,7 @@ import logging
# logging.getLogger('boto').setLevel(logging.CRITICAL)
__title__ = 'moto'
__version__ = '1.2.0',
__version__ = '1.2.0'
from .acm import mock_acm # flake8: noqa
from .apigateway import mock_apigateway, mock_apigateway_deprecated # flake8: noqa

View file

@ -1,12 +1,11 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import datetime
import requests
import time
from moto.packages.responses import responses
from moto.core import BaseBackend, BaseModel
from moto.core.utils import iso_8601_datetime_with_milliseconds
from .utils import create_id
from .exceptions import StageNotFoundException
@ -20,8 +19,7 @@ class Deployment(BaseModel, dict):
self['id'] = deployment_id
self['stageName'] = name
self['description'] = description
self['createdDate'] = iso_8601_datetime_with_milliseconds(
datetime.datetime.now())
self['createdDate'] = int(time.time())
class IntegrationResponse(BaseModel, dict):
@ -300,7 +298,7 @@ class RestAPI(BaseModel):
self.region_name = region_name
self.name = name
self.description = description
self.create_date = datetime.datetime.utcnow()
self.create_date = int(time.time())
self.deployments = {}
self.stages = {}
@ -313,7 +311,7 @@ class RestAPI(BaseModel):
"id": self.id,
"name": self.name,
"description": self.description,
"createdDate": iso_8601_datetime_with_milliseconds(self.create_date),
"createdDate": int(time.time()),
}
def add_child(self, path, parent_id=None):

View file

@ -104,7 +104,7 @@ class _DockerDataVolumeContext:
# It doesn't exist so we need to create it
self._vol_ref.volume = self._lambda_func.docker_client.volumes.create(self._lambda_func.code_sha_256)
container = self._lambda_func.docker_client.containers.run('alpine', 'sleep 100', volumes={self.name: '/tmp/data'}, detach=True)
container = self._lambda_func.docker_client.containers.run('alpine', 'sleep 100', volumes={self.name: {'bind': '/tmp/data', 'mode': 'rw'}}, detach=True)
try:
tar_bytes = zip2tar(self._lambda_func.code_bytes)
container.put_archive('/tmp/data', tar_bytes)
@ -309,7 +309,7 @@ class LambdaFunction(BaseModel):
finally:
if container:
try:
exit_code = container.wait(timeout=300)
exit_code = container.wait(timeout=300)['StatusCode']
except requests.exceptions.ReadTimeout:
exit_code = -1
container.stop()

View file

@ -345,6 +345,10 @@ class BaseResponse(_TemplateEnvironmentMixin):
if is_tracked(name) or not name.startswith(param_prefix):
continue
if len(name) > len(param_prefix) and \
not name[len(param_prefix):].startswith('.'):
continue
match = self.param_list_regex.search(name[len(param_prefix):]) if len(name) > len(param_prefix) else None
if match:
prefix = param_prefix + match.group(1)

View file

@ -2943,7 +2943,7 @@ class SpotFleetRequest(TaggedEC2Resource):
'Properties']['SpotFleetRequestConfigData']
ec2_backend = ec2_backends[region_name]
spot_price = properties['SpotPrice']
spot_price = properties.get('SpotPrice')
target_capacity = properties['TargetCapacity']
iam_fleet_role = properties['IamFleetRole']
allocation_strategy = properties['AllocationStrategy']
@ -2977,7 +2977,8 @@ class SpotFleetRequest(TaggedEC2Resource):
launch_spec_index += 1
else: # lowestPrice
cheapest_spec = sorted(
self.launch_specs, key=lambda spec: float(spec.spot_price))[0]
# FIXME: change `+inf` to the on demand price scaled to weighted capacity when it's not present
self.launch_specs, key=lambda spec: float(spec.spot_price or '+inf'))[0]
weight_so_far = weight_to_add + (weight_to_add % cheapest_spec.weighted_capacity)
weight_map[cheapest_spec] = int(
weight_so_far // cheapest_spec.weighted_capacity)

View file

@ -40,7 +40,7 @@ class SpotFleets(BaseResponse):
def request_spot_fleet(self):
spot_config = self._get_dict_param("SpotFleetRequestConfig.")
spot_price = spot_config['spot_price']
spot_price = spot_config.get('spot_price')
target_capacity = spot_config['target_capacity']
iam_fleet_role = spot_config['iam_fleet_role']
allocation_strategy = spot_config['allocation_strategy']
@ -78,7 +78,9 @@ DESCRIBE_SPOT_FLEET_TEMPLATE = """<DescribeSpotFleetRequestsResponse xmlns="http
<spotFleetRequestId>{{ request.id }}</spotFleetRequestId>
<spotFleetRequestState>{{ request.state }}</spotFleetRequestState>
<spotFleetRequestConfig>
{% if request.spot_price %}
<spotPrice>{{ request.spot_price }}</spotPrice>
{% endif %}
<targetCapacity>{{ request.target_capacity }}</targetCapacity>
<iamFleetRole>{{ request.iam_fleet_role }}</iamFleetRole>
<allocationStrategy>{{ request.allocation_strategy }}</allocationStrategy>
@ -93,7 +95,9 @@ DESCRIBE_SPOT_FLEET_TEMPLATE = """<DescribeSpotFleetRequestsResponse xmlns="http
<iamInstanceProfile><arn>{{ launch_spec.iam_instance_profile }}</arn></iamInstanceProfile>
<keyName>{{ launch_spec.key_name }}</keyName>
<monitoring><enabled>{{ launch_spec.monitoring }}</enabled></monitoring>
{% if launch_spec.spot_price %}
<spotPrice>{{ launch_spec.spot_price }}</spotPrice>
{% endif %}
<userData>{{ launch_spec.user_data }}</userData>
<weightedCapacity>{{ launch_spec.weighted_capacity }}</weightedCapacity>
<groupSet>

View file

@ -486,6 +486,10 @@ class ELBv2Backend(BaseBackend):
arn = load_balancer_arn.replace(':loadbalancer/', ':listener/') + "/%s%s" % (port, id(self))
listener = FakeListener(load_balancer_arn, arn, protocol, port, ssl_policy, certificate, default_actions)
balancer.listeners[listener.arn] = listener
for action in default_actions:
if action['target_group_arn'] in self.target_groups.keys():
target_group = self.target_groups[action['target_group_arn']]
target_group.load_balancer_arns.append(load_balancer_arn)
return listener
def describe_load_balancers(self, arns, names):

View file

@ -108,3 +108,24 @@ class ResourceNotFoundFaultError(RedshiftClientError):
msg = message
super(ResourceNotFoundFaultError, self).__init__(
'ResourceNotFoundFault', msg)
class SnapshotCopyDisabledFaultError(RedshiftClientError):
def __init__(self, cluster_identifier):
super(SnapshotCopyDisabledFaultError, self).__init__(
'SnapshotCopyDisabledFault',
"Cannot modify retention period because snapshot copy is disabled on Cluster {0}.".format(cluster_identifier))
class SnapshotCopyAlreadyDisabledFaultError(RedshiftClientError):
def __init__(self, cluster_identifier):
super(SnapshotCopyAlreadyDisabledFaultError, self).__init__(
'SnapshotCopyAlreadyDisabledFault',
"Snapshot Copy is already disabled on Cluster {0}.".format(cluster_identifier))
class SnapshotCopyAlreadyEnabledFaultError(RedshiftClientError):
def __init__(self, cluster_identifier):
super(SnapshotCopyAlreadyEnabledFaultError, self).__init__(
'SnapshotCopyAlreadyEnabledFault',
"Snapshot Copy is already enabled on Cluster {0}.".format(cluster_identifier))

View file

@ -4,6 +4,7 @@ import copy
import datetime
import boto.redshift
from botocore.exceptions import ClientError
from moto.compat import OrderedDict
from moto.core import BaseBackend, BaseModel
from moto.core.utils import iso_8601_datetime_with_milliseconds
@ -18,8 +19,11 @@ from .exceptions import (
InvalidParameterValueError,
InvalidSubnetError,
ResourceNotFoundFaultError,
SnapshotCopyAlreadyDisabledFaultError,
SnapshotCopyAlreadyEnabledFaultError,
SnapshotCopyDisabledFaultError,
SnapshotCopyGrantAlreadyExistsFaultError,
SnapshotCopyGrantNotFoundFaultError
SnapshotCopyGrantNotFoundFaultError,
)
@ -196,7 +200,7 @@ class Cluster(TaggableResourceMixin, BaseModel):
return self.cluster_identifier
def to_json(self):
return {
json_response = {
"MasterUsername": self.master_username,
"MasterUserPassword": "****",
"ClusterVersion": self.cluster_version,
@ -233,6 +237,12 @@ class Cluster(TaggableResourceMixin, BaseModel):
"Tags": self.tags
}
try:
json_response['ClusterSnapshotCopyStatus'] = self.cluster_snapshot_copy_status
except AttributeError:
pass
return json_response
class SnapshotCopyGrant(TaggableResourceMixin, BaseModel):
@ -435,6 +445,43 @@ class RedshiftBackend(BaseBackend):
self.__dict__ = {}
self.__init__(ec2_backend, region_name)
def enable_snapshot_copy(self, **kwargs):
cluster_identifier = kwargs['cluster_identifier']
cluster = self.clusters[cluster_identifier]
if not hasattr(cluster, 'cluster_snapshot_copy_status'):
if cluster.encrypted == 'true' and kwargs['snapshot_copy_grant_name'] is None:
raise ClientError(
'InvalidParameterValue',
'SnapshotCopyGrantName is required for Snapshot Copy '
'on KMS encrypted clusters.'
)
status = {
'DestinationRegion': kwargs['destination_region'],
'RetentionPeriod': kwargs['retention_period'],
'SnapshotCopyGrantName': kwargs['snapshot_copy_grant_name'],
}
cluster.cluster_snapshot_copy_status = status
return cluster
else:
raise SnapshotCopyAlreadyEnabledFaultError(cluster_identifier)
def disable_snapshot_copy(self, **kwargs):
cluster_identifier = kwargs['cluster_identifier']
cluster = self.clusters[cluster_identifier]
if hasattr(cluster, 'cluster_snapshot_copy_status'):
del cluster.cluster_snapshot_copy_status
return cluster
else:
raise SnapshotCopyAlreadyDisabledFaultError(cluster_identifier)
def modify_snapshot_copy_retention_period(self, cluster_identifier, retention_period):
cluster = self.clusters[cluster_identifier]
if hasattr(cluster, 'cluster_snapshot_copy_status'):
cluster.cluster_snapshot_copy_status['RetentionPeriod'] = retention_period
return cluster
else:
raise SnapshotCopyDisabledFaultError(cluster_identifier)
def create_cluster(self, **cluster_kwargs):
cluster_identifier = cluster_kwargs['cluster_identifier']
cluster = Cluster(self, **cluster_kwargs)

View file

@ -550,3 +550,58 @@ class RedshiftResponse(BaseResponse):
}
}
})
def enable_snapshot_copy(self):
snapshot_copy_kwargs = {
'cluster_identifier': self._get_param('ClusterIdentifier'),
'destination_region': self._get_param('DestinationRegion'),
'retention_period': self._get_param('RetentionPeriod', 7),
'snapshot_copy_grant_name': self._get_param('SnapshotCopyGrantName'),
}
cluster = self.redshift_backend.enable_snapshot_copy(**snapshot_copy_kwargs)
return self.get_response({
"EnableSnapshotCopyResponse": {
"EnableSnapshotCopyResult": {
"Cluster": cluster.to_json()
},
"ResponseMetadata": {
"RequestId": "384ac68d-3775-11df-8963-01868b7c937a",
}
}
})
def disable_snapshot_copy(self):
snapshot_copy_kwargs = {
'cluster_identifier': self._get_param('ClusterIdentifier'),
}
cluster = self.redshift_backend.disable_snapshot_copy(**snapshot_copy_kwargs)
return self.get_response({
"DisableSnapshotCopyResponse": {
"DisableSnapshotCopyResult": {
"Cluster": cluster.to_json()
},
"ResponseMetadata": {
"RequestId": "384ac68d-3775-11df-8963-01868b7c937a",
}
}
})
def modify_snapshot_copy_retention_period(self):
snapshot_copy_kwargs = {
'cluster_identifier': self._get_param('ClusterIdentifier'),
'retention_period': self._get_param('RetentionPeriod'),
}
cluster = self.redshift_backend.modify_snapshot_copy_retention_period(**snapshot_copy_kwargs)
return self.get_response({
"ModifySnapshotCopyRetentionPeriodResponse": {
"ModifySnapshotCopyRetentionPeriodResult": {
"Clusters": [cluster.to_json()]
},
"ResponseMetadata": {
"RequestId": "384ac68d-3775-11df-8963-01868b7c937a",
}
}
})

View file

@ -18,10 +18,10 @@ from .exceptions import BucketAlreadyExists, S3ClientError, MissingBucket, Missi
MalformedACLError
from .models import s3_backend, get_canned_acl, FakeGrantee, FakeGrant, FakeAcl, FakeKey, FakeTagging, FakeTagSet, \
FakeTag
from .utils import bucket_name_from_url, metadata_from_headers
from .utils import bucket_name_from_url, metadata_from_headers, parse_region_from_url
from xml.dom import minidom
REGION_URL_REGEX = r'\.s3-(.+?)\.amazonaws\.com'
DEFAULT_REGION_NAME = 'us-east-1'
@ -128,10 +128,7 @@ class ResponseObject(_TemplateEnvironmentMixin):
parsed_url = urlparse(full_url)
querystring = parse_qs(parsed_url.query, keep_blank_values=True)
method = request.method
region_name = DEFAULT_REGION_NAME
region_match = re.search(REGION_URL_REGEX, full_url)
if region_match:
region_name = region_match.groups()[0]
region_name = parse_region_from_url(full_url)
bucket_name = self.parse_bucket_name_from_url(request, full_url)
if not bucket_name:
@ -172,7 +169,7 @@ class ResponseObject(_TemplateEnvironmentMixin):
# HEAD (which the real API responds with), and instead
# raises NoSuchBucket, leading to inconsistency in
# error response between real and mocked responses.
return 404, {}, "Not Found"
return 404, {}, ""
return 200, {}, ""
def _bucket_response_get(self, bucket_name, querystring, headers):

View file

@ -1,4 +1,5 @@
from __future__ import unicode_literals
import logging
from boto.s3.key import Key
import re
@ -6,6 +7,10 @@ import six
from six.moves.urllib.parse import urlparse, unquote
import sys
log = logging.getLogger(__name__)
bucket_name_regex = re.compile("(.+).s3(.*).amazonaws.com")
@ -27,6 +32,20 @@ def bucket_name_from_url(url):
return None
REGION_URL_REGEX = re.compile(
r'^https?://(s3[-\.](?P<region1>.+)\.amazonaws\.com/(.+)|'
r'(.+)\.s3-(?P<region2>.+)\.amazonaws\.com)/?')
def parse_region_from_url(url):
match = REGION_URL_REGEX.search(url)
if match:
region = match.group('region1') or match.group('region2')
else:
region = 'us-east-1'
return region
def metadata_from_headers(headers):
metadata = {}
meta_regex = re.compile(