Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
88596518f5
61 changed files with 2231 additions and 264 deletions
|
|
@ -3,7 +3,7 @@ import logging
|
|||
# logging.getLogger('boto').setLevel(logging.CRITICAL)
|
||||
|
||||
__title__ = 'moto'
|
||||
__version__ = '1.3.3'
|
||||
__version__ = '1.3.5'
|
||||
|
||||
from .acm import mock_acm # flake8: noqa
|
||||
from .apigateway import mock_apigateway, mock_apigateway_deprecated # flake8: noqa
|
||||
|
|
@ -24,6 +24,7 @@ from .elbv2 import mock_elbv2 # flake8: noqa
|
|||
from .emr import mock_emr, mock_emr_deprecated # flake8: noqa
|
||||
from .events import mock_events # flake8: noqa
|
||||
from .glacier import mock_glacier, mock_glacier_deprecated # flake8: noqa
|
||||
from .glue import mock_glue # flake8: noqa
|
||||
from .iam import mock_iam, mock_iam_deprecated # flake8: noqa
|
||||
from .kinesis import mock_kinesis, mock_kinesis_deprecated # flake8: noqa
|
||||
from .kms import mock_kms, mock_kms_deprecated # flake8: noqa
|
||||
|
|
|
|||
|
|
@ -8,3 +8,11 @@ class StageNotFoundException(RESTError):
|
|||
def __init__(self):
|
||||
super(StageNotFoundException, self).__init__(
|
||||
"NotFoundException", "Invalid stage identifier specified")
|
||||
|
||||
|
||||
class ApiKeyNotFoundException(RESTError):
|
||||
code = 404
|
||||
|
||||
def __init__(self):
|
||||
super(ApiKeyNotFoundException, self).__init__(
|
||||
"NotFoundException", "Invalid API Key identifier specified")
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from boto3.session import Session
|
|||
import responses
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from .utils import create_id
|
||||
from .exceptions import StageNotFoundException
|
||||
from .exceptions import StageNotFoundException, ApiKeyNotFoundException
|
||||
|
||||
STAGE_URL = "https://{api_id}.execute-api.{region_name}.amazonaws.com/{stage_name}"
|
||||
|
||||
|
|
@ -300,11 +300,7 @@ class ApiKey(BaseModel, dict):
|
|||
generateDistinctId=False, value=None, stageKeys=None, customerId=None):
|
||||
super(ApiKey, self).__init__()
|
||||
self['id'] = create_id()
|
||||
if generateDistinctId:
|
||||
# Best guess of what AWS does internally
|
||||
self['value'] = ''.join(random.sample(string.ascii_letters + string.digits, 40))
|
||||
else:
|
||||
self['value'] = value
|
||||
self['value'] = value if value else ''.join(random.sample(string.ascii_letters + string.digits, 40))
|
||||
self['name'] = name
|
||||
self['customerId'] = customerId
|
||||
self['description'] = description
|
||||
|
|
@ -313,6 +309,29 @@ class ApiKey(BaseModel, dict):
|
|||
self['stageKeys'] = stageKeys
|
||||
|
||||
|
||||
class UsagePlan(BaseModel, dict):
|
||||
|
||||
def __init__(self, name=None, description=None, apiStages=[],
|
||||
throttle=None, quota=None):
|
||||
super(UsagePlan, self).__init__()
|
||||
self['id'] = create_id()
|
||||
self['name'] = name
|
||||
self['description'] = description
|
||||
self['apiStages'] = apiStages
|
||||
self['throttle'] = throttle
|
||||
self['quota'] = quota
|
||||
|
||||
|
||||
class UsagePlanKey(BaseModel, dict):
|
||||
|
||||
def __init__(self, id, type, name, value):
|
||||
super(UsagePlanKey, self).__init__()
|
||||
self['id'] = id
|
||||
self['name'] = name
|
||||
self['type'] = type
|
||||
self['value'] = value
|
||||
|
||||
|
||||
class RestAPI(BaseModel):
|
||||
|
||||
def __init__(self, id, region_name, name, description):
|
||||
|
|
@ -412,6 +431,8 @@ class APIGatewayBackend(BaseBackend):
|
|||
super(APIGatewayBackend, self).__init__()
|
||||
self.apis = {}
|
||||
self.keys = {}
|
||||
self.usage_plans = {}
|
||||
self.usage_plan_keys = {}
|
||||
self.region_name = region_name
|
||||
|
||||
def reset(self):
|
||||
|
|
@ -580,6 +601,48 @@ class APIGatewayBackend(BaseBackend):
|
|||
self.keys.pop(api_key_id)
|
||||
return {}
|
||||
|
||||
def create_usage_plan(self, payload):
|
||||
plan = UsagePlan(**payload)
|
||||
self.usage_plans[plan['id']] = plan
|
||||
return plan
|
||||
|
||||
def get_usage_plans(self):
|
||||
return list(self.usage_plans.values())
|
||||
|
||||
def get_usage_plan(self, usage_plan_id):
|
||||
return self.usage_plans[usage_plan_id]
|
||||
|
||||
def delete_usage_plan(self, usage_plan_id):
|
||||
self.usage_plans.pop(usage_plan_id)
|
||||
return {}
|
||||
|
||||
def create_usage_plan_key(self, usage_plan_id, payload):
|
||||
if usage_plan_id not in self.usage_plan_keys:
|
||||
self.usage_plan_keys[usage_plan_id] = {}
|
||||
|
||||
key_id = payload["keyId"]
|
||||
if key_id not in self.keys:
|
||||
raise ApiKeyNotFoundException()
|
||||
|
||||
api_key = self.keys[key_id]
|
||||
|
||||
usage_plan_key = UsagePlanKey(id=key_id, type=payload["keyType"], name=api_key["name"], value=api_key["value"])
|
||||
self.usage_plan_keys[usage_plan_id][usage_plan_key['id']] = usage_plan_key
|
||||
return usage_plan_key
|
||||
|
||||
def get_usage_plan_keys(self, usage_plan_id):
|
||||
if usage_plan_id not in self.usage_plan_keys:
|
||||
return []
|
||||
|
||||
return list(self.usage_plan_keys[usage_plan_id].values())
|
||||
|
||||
def get_usage_plan_key(self, usage_plan_id, key_id):
|
||||
return self.usage_plan_keys[usage_plan_id][key_id]
|
||||
|
||||
def delete_usage_plan_key(self, usage_plan_id, key_id):
|
||||
self.usage_plan_keys[usage_plan_id].pop(key_id)
|
||||
return {}
|
||||
|
||||
|
||||
apigateway_backends = {}
|
||||
for region_name in Session().get_available_regions('apigateway'):
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import json
|
|||
|
||||
from moto.core.responses import BaseResponse
|
||||
from .models import apigateway_backends
|
||||
from .exceptions import StageNotFoundException
|
||||
from .exceptions import StageNotFoundException, ApiKeyNotFoundException
|
||||
|
||||
|
||||
class APIGatewayResponse(BaseResponse):
|
||||
|
|
@ -248,3 +248,56 @@ class APIGatewayResponse(BaseResponse):
|
|||
elif self.method == 'DELETE':
|
||||
apikey_response = self.backend.delete_apikey(apikey)
|
||||
return 200, {}, json.dumps(apikey_response)
|
||||
|
||||
def usage_plans(self, request, full_url, headers):
|
||||
self.setup_class(request, full_url, headers)
|
||||
|
||||
if self.method == 'POST':
|
||||
usage_plan_response = self.backend.create_usage_plan(json.loads(self.body))
|
||||
elif self.method == 'GET':
|
||||
usage_plans_response = self.backend.get_usage_plans()
|
||||
return 200, {}, json.dumps({"item": usage_plans_response})
|
||||
return 200, {}, json.dumps(usage_plan_response)
|
||||
|
||||
def usage_plan_individual(self, request, full_url, headers):
|
||||
self.setup_class(request, full_url, headers)
|
||||
|
||||
url_path_parts = self.path.split("/")
|
||||
usage_plan = url_path_parts[2]
|
||||
|
||||
if self.method == 'GET':
|
||||
usage_plan_response = self.backend.get_usage_plan(usage_plan)
|
||||
elif self.method == 'DELETE':
|
||||
usage_plan_response = self.backend.delete_usage_plan(usage_plan)
|
||||
return 200, {}, json.dumps(usage_plan_response)
|
||||
|
||||
def usage_plan_keys(self, request, full_url, headers):
|
||||
self.setup_class(request, full_url, headers)
|
||||
|
||||
url_path_parts = self.path.split("/")
|
||||
usage_plan_id = url_path_parts[2]
|
||||
|
||||
if self.method == 'POST':
|
||||
try:
|
||||
usage_plan_response = self.backend.create_usage_plan_key(usage_plan_id, json.loads(self.body))
|
||||
except ApiKeyNotFoundException as error:
|
||||
return error.code, {}, '{{"message":"{0}","code":"{1}"}}'.format(error.message, error.error_type)
|
||||
|
||||
elif self.method == 'GET':
|
||||
usage_plans_response = self.backend.get_usage_plan_keys(usage_plan_id)
|
||||
return 200, {}, json.dumps({"item": usage_plans_response})
|
||||
|
||||
return 200, {}, json.dumps(usage_plan_response)
|
||||
|
||||
def usage_plan_key_individual(self, request, full_url, headers):
|
||||
self.setup_class(request, full_url, headers)
|
||||
|
||||
url_path_parts = self.path.split("/")
|
||||
usage_plan_id = url_path_parts[2]
|
||||
key_id = url_path_parts[4]
|
||||
|
||||
if self.method == 'GET':
|
||||
usage_plan_response = self.backend.get_usage_plan_key(usage_plan_id, key_id)
|
||||
elif self.method == 'DELETE':
|
||||
usage_plan_response = self.backend.delete_usage_plan_key(usage_plan_id, key_id)
|
||||
return 200, {}, json.dumps(usage_plan_response)
|
||||
|
|
|
|||
|
|
@ -20,4 +20,8 @@ url_paths = {
|
|||
'{0}/restapis/(?P<function_id>[^/]+)/resources/(?P<resource_id>[^/]+)/methods/(?P<method_name>[^/]+)/integration/responses/(?P<status_code>\d+)/?$': APIGatewayResponse().integration_responses,
|
||||
'{0}/apikeys$': APIGatewayResponse().apikeys,
|
||||
'{0}/apikeys/(?P<apikey>[^/]+)': APIGatewayResponse().apikey_individual,
|
||||
'{0}/usageplans$': APIGatewayResponse().usage_plans,
|
||||
'{0}/usageplans/(?P<usage_plan_id>[^/]+)/?$': APIGatewayResponse().usage_plan_individual,
|
||||
'{0}/usageplans/(?P<usage_plan_id>[^/]+)/keys$': APIGatewayResponse().usage_plan_keys,
|
||||
'{0}/usageplans/(?P<usage_plan_id>[^/]+)/keys/(?P<api_key_id>[^/]+)/?$': APIGatewayResponse().usage_plan_key_individual,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from moto.elbv2 import elbv2_backends
|
|||
from moto.emr import emr_backends
|
||||
from moto.events import events_backends
|
||||
from moto.glacier import glacier_backends
|
||||
from moto.glue import glue_backends
|
||||
from moto.iam import iam_backends
|
||||
from moto.instance_metadata import instance_metadata_backends
|
||||
from moto.kinesis import kinesis_backends
|
||||
|
|
@ -65,6 +66,7 @@ BACKENDS = {
|
|||
'events': events_backends,
|
||||
'emr': emr_backends,
|
||||
'glacier': glacier_backends,
|
||||
'glue': glue_backends,
|
||||
'iam': iam_backends,
|
||||
'moto_api': moto_api_backends,
|
||||
'instance_metadata': instance_metadata_backends,
|
||||
|
|
|
|||
|
|
@ -29,8 +29,10 @@ COMPARISON_FUNCS = {
|
|||
'GT': GT_FUNCTION,
|
||||
'>': GT_FUNCTION,
|
||||
|
||||
'NULL': lambda item_value: item_value is None,
|
||||
'NOT_NULL': lambda item_value: item_value is not None,
|
||||
# NULL means the value should not exist at all
|
||||
'NULL': lambda item_value: False,
|
||||
# NOT_NULL means the value merely has to exist, and values of None are valid
|
||||
'NOT_NULL': lambda item_value: True,
|
||||
'CONTAINS': lambda item_value, test_value: test_value in item_value,
|
||||
'NOT_CONTAINS': lambda item_value, test_value: test_value not in item_value,
|
||||
'BEGINS_WITH': lambda item_value, test_value: item_value.startswith(test_value),
|
||||
|
|
|
|||
|
|
@ -409,7 +409,8 @@ class Table(BaseModel):
|
|||
current_attr = current
|
||||
|
||||
for key, val in expected.items():
|
||||
if 'Exists' in val and val['Exists'] is False:
|
||||
if 'Exists' in val and val['Exists'] is False \
|
||||
or 'ComparisonOperator' in val and val['ComparisonOperator'] == 'NULL':
|
||||
if key in current_attr:
|
||||
raise ValueError("The conditional request failed")
|
||||
elif key not in current_attr:
|
||||
|
|
@ -419,8 +420,10 @@ class Table(BaseModel):
|
|||
elif 'ComparisonOperator' in val:
|
||||
comparison_func = get_comparison_func(
|
||||
val['ComparisonOperator'])
|
||||
dynamo_types = [DynamoType(ele) for ele in val[
|
||||
"AttributeValueList"]]
|
||||
dynamo_types = [
|
||||
DynamoType(ele) for ele in
|
||||
val.get("AttributeValueList", [])
|
||||
]
|
||||
for t in dynamo_types:
|
||||
if not comparison_func(current_attr[key].value, t.value):
|
||||
raise ValueError('The conditional request failed')
|
||||
|
|
@ -827,7 +830,8 @@ class DynamoDBBackend(BaseBackend):
|
|||
expected = {}
|
||||
|
||||
for key, val in expected.items():
|
||||
if 'Exists' in val and val['Exists'] is False:
|
||||
if 'Exists' in val and val['Exists'] is False \
|
||||
or 'ComparisonOperator' in val and val['ComparisonOperator'] == 'NULL':
|
||||
if key in item_attr:
|
||||
raise ValueError("The conditional request failed")
|
||||
elif key not in item_attr:
|
||||
|
|
@ -837,8 +841,10 @@ class DynamoDBBackend(BaseBackend):
|
|||
elif 'ComparisonOperator' in val:
|
||||
comparison_func = get_comparison_func(
|
||||
val['ComparisonOperator'])
|
||||
dynamo_types = [DynamoType(ele) for ele in val[
|
||||
"AttributeValueList"]]
|
||||
dynamo_types = [
|
||||
DynamoType(ele) for ele in
|
||||
val.get("AttributeValueList", [])
|
||||
]
|
||||
for t in dynamo_types:
|
||||
if not comparison_func(item_attr[key].value, t.value):
|
||||
raise ValueError('The conditional request failed')
|
||||
|
|
|
|||
|
|
@ -209,17 +209,22 @@ class ECRBackend(BaseBackend):
|
|||
"""
|
||||
maxResults and filtering not implemented
|
||||
"""
|
||||
images = []
|
||||
for repository in self.repositories.values():
|
||||
if repository_name:
|
||||
if repository.name != repository_name:
|
||||
continue
|
||||
repository = None
|
||||
found = False
|
||||
if repository_name in self.repositories:
|
||||
repository = self.repositories[repository_name]
|
||||
if registry_id:
|
||||
if repository.registry_id != registry_id:
|
||||
continue
|
||||
if repository.registry_id == registry_id:
|
||||
found = True
|
||||
else:
|
||||
found = True
|
||||
|
||||
for image in repository.images:
|
||||
images.append(image)
|
||||
if not found:
|
||||
raise RepositoryNotFoundException(repository_name, registry_id or DEFAULT_REGISTRY_ID)
|
||||
|
||||
images = []
|
||||
for image in repository.images:
|
||||
images.append(image)
|
||||
return images
|
||||
|
||||
def describe_images(self, repository_name, registry_id=None, image_ids=None):
|
||||
|
|
|
|||
11
moto/ecs/exceptions.py
Normal file
11
moto/ecs/exceptions.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from __future__ import unicode_literals
|
||||
from moto.core.exceptions import RESTError
|
||||
|
||||
|
||||
class ServiceNotFoundException(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, service_name):
|
||||
super(ServiceNotFoundException, self).__init__(
|
||||
error_type="ServiceNotFoundException",
|
||||
message="The service {0} does not exist".format(service_name))
|
||||
|
|
@ -10,6 +10,8 @@ from moto.core import BaseBackend, BaseModel
|
|||
from moto.ec2 import ec2_backends
|
||||
from copy import copy
|
||||
|
||||
from .exceptions import ServiceNotFoundException
|
||||
|
||||
|
||||
class BaseObject(BaseModel):
|
||||
|
||||
|
|
@ -601,8 +603,9 @@ class EC2ContainerServiceBackend(BaseBackend):
|
|||
raise Exception("tasks cannot be empty")
|
||||
response = []
|
||||
for cluster, cluster_tasks in self.tasks.items():
|
||||
for task_id, task in cluster_tasks.items():
|
||||
if task_id in tasks or task.task_arn in tasks:
|
||||
for task_arn, task in cluster_tasks.items():
|
||||
task_id = task_arn.split("/")[-1]
|
||||
if task_arn in tasks or task.task_arn in tasks or any(task_id in task for task in tasks):
|
||||
response.append(task)
|
||||
return response
|
||||
|
||||
|
|
@ -700,8 +703,7 @@ class EC2ContainerServiceBackend(BaseBackend):
|
|||
cluster_service_pair].desired_count = desired_count
|
||||
return self.services[cluster_service_pair]
|
||||
else:
|
||||
raise Exception("cluster {0} or service {1} does not exist".format(
|
||||
cluster_name, service_name))
|
||||
raise ServiceNotFoundException(service_name)
|
||||
|
||||
def delete_service(self, cluster_name, service_name):
|
||||
cluster_service_pair = '{0}:{1}'.format(cluster_name, service_name)
|
||||
|
|
|
|||
5
moto/glue/__init__.py
Normal file
5
moto/glue/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
from .models import glue_backend
|
||||
|
||||
glue_backends = {"global": glue_backend}
|
||||
mock_glue = glue_backend.decorator
|
||||
24
moto/glue/exceptions.py
Normal file
24
moto/glue/exceptions.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from __future__ import unicode_literals
|
||||
from moto.core.exceptions import JsonRESTError
|
||||
|
||||
|
||||
class GlueClientError(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
|
||||
class DatabaseAlreadyExistsException(GlueClientError):
|
||||
def __init__(self):
|
||||
self.code = 400
|
||||
super(DatabaseAlreadyExistsException, self).__init__(
|
||||
'DatabaseAlreadyExistsException',
|
||||
'Database already exists.'
|
||||
)
|
||||
|
||||
|
||||
class TableAlreadyExistsException(GlueClientError):
|
||||
def __init__(self):
|
||||
self.code = 400
|
||||
super(TableAlreadyExistsException, self).__init__(
|
||||
'TableAlreadyExistsException',
|
||||
'Table already exists.'
|
||||
)
|
||||
60
moto/glue/models.py
Normal file
60
moto/glue/models.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.compat import OrderedDict
|
||||
from.exceptions import DatabaseAlreadyExistsException, TableAlreadyExistsException
|
||||
|
||||
|
||||
class GlueBackend(BaseBackend):
|
||||
|
||||
def __init__(self):
|
||||
self.databases = OrderedDict()
|
||||
|
||||
def create_database(self, database_name):
|
||||
if database_name in self.databases:
|
||||
raise DatabaseAlreadyExistsException()
|
||||
|
||||
database = FakeDatabase(database_name)
|
||||
self.databases[database_name] = database
|
||||
return database
|
||||
|
||||
def get_database(self, database_name):
|
||||
return self.databases[database_name]
|
||||
|
||||
def create_table(self, database_name, table_name, table_input):
|
||||
database = self.get_database(database_name)
|
||||
|
||||
if table_name in database.tables:
|
||||
raise TableAlreadyExistsException()
|
||||
|
||||
table = FakeTable(database_name, table_name, table_input)
|
||||
database.tables[table_name] = table
|
||||
return table
|
||||
|
||||
def get_table(self, database_name, table_name):
|
||||
database = self.get_database(database_name)
|
||||
return database.tables[table_name]
|
||||
|
||||
def get_tables(self, database_name):
|
||||
database = self.get_database(database_name)
|
||||
return [table for table_name, table in database.tables.items()]
|
||||
|
||||
|
||||
class FakeDatabase(BaseModel):
|
||||
|
||||
def __init__(self, database_name):
|
||||
self.name = database_name
|
||||
self.tables = OrderedDict()
|
||||
|
||||
|
||||
class FakeTable(BaseModel):
|
||||
|
||||
def __init__(self, database_name, table_name, table_input):
|
||||
self.database_name = database_name
|
||||
self.name = table_name
|
||||
self.table_input = table_input
|
||||
self.storage_descriptor = self.table_input.get('StorageDescriptor', {})
|
||||
self.partition_keys = self.table_input.get('PartitionKeys', [])
|
||||
|
||||
|
||||
glue_backend = GlueBackend()
|
||||
63
moto/glue/responses.py
Normal file
63
moto/glue/responses.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
|
||||
from moto.core.responses import BaseResponse
|
||||
from .models import glue_backend
|
||||
|
||||
|
||||
class GlueResponse(BaseResponse):
|
||||
|
||||
@property
|
||||
def glue_backend(self):
|
||||
return glue_backend
|
||||
|
||||
@property
|
||||
def parameters(self):
|
||||
return json.loads(self.body)
|
||||
|
||||
def create_database(self):
|
||||
database_name = self.parameters['DatabaseInput']['Name']
|
||||
self.glue_backend.create_database(database_name)
|
||||
return ""
|
||||
|
||||
def get_database(self):
|
||||
database_name = self.parameters.get('Name')
|
||||
database = self.glue_backend.get_database(database_name)
|
||||
return json.dumps({'Database': {'Name': database.name}})
|
||||
|
||||
def create_table(self):
|
||||
database_name = self.parameters.get('DatabaseName')
|
||||
table_input = self.parameters.get('TableInput')
|
||||
table_name = table_input.get('Name')
|
||||
self.glue_backend.create_table(database_name, table_name, table_input)
|
||||
return ""
|
||||
|
||||
def get_table(self):
|
||||
database_name = self.parameters.get('DatabaseName')
|
||||
table_name = self.parameters.get('Name')
|
||||
table = self.glue_backend.get_table(database_name, table_name)
|
||||
return json.dumps({
|
||||
'Table': {
|
||||
'DatabaseName': table.database_name,
|
||||
'Name': table.name,
|
||||
'PartitionKeys': table.partition_keys,
|
||||
'StorageDescriptor': table.storage_descriptor
|
||||
}
|
||||
})
|
||||
|
||||
def get_tables(self):
|
||||
database_name = self.parameters.get('DatabaseName')
|
||||
tables = self.glue_backend.get_tables(database_name)
|
||||
return json.dumps(
|
||||
{
|
||||
'TableList': [
|
||||
{
|
||||
'DatabaseName': table.database_name,
|
||||
'Name': table.name,
|
||||
'PartitionKeys': table.partition_keys,
|
||||
'StorageDescriptor': table.storage_descriptor
|
||||
} for table in tables
|
||||
]
|
||||
}
|
||||
)
|
||||
11
moto/glue/urls.py
Normal file
11
moto/glue/urls.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from .responses import GlueResponse
|
||||
|
||||
url_bases = [
|
||||
"https?://glue(.*).amazonaws.com"
|
||||
]
|
||||
|
||||
url_paths = {
|
||||
'{0}/$': GlueResponse.dispatch
|
||||
}
|
||||
1
moto/glue/utils.py
Normal file
1
moto/glue/utils.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from __future__ import unicode_literals
|
||||
|
|
@ -50,10 +50,6 @@ class Policy(BaseModel):
|
|||
self.create_datetime = datetime.now(pytz.utc)
|
||||
self.update_datetime = datetime.now(pytz.utc)
|
||||
|
||||
@property
|
||||
def arn(self):
|
||||
return 'arn:aws:iam::aws:policy{0}{1}'.format(self.path, self.name)
|
||||
|
||||
|
||||
class PolicyVersion(object):
|
||||
|
||||
|
|
@ -82,6 +78,10 @@ class ManagedPolicy(Policy):
|
|||
self.attachment_count -= 1
|
||||
del obj.managed_policies[self.name]
|
||||
|
||||
@property
|
||||
def arn(self):
|
||||
return "arn:aws:iam::{0}:policy{1}{2}".format(ACCOUNT_ID, self.path, self.name)
|
||||
|
||||
|
||||
class AWSManagedPolicy(ManagedPolicy):
|
||||
"""AWS-managed policy."""
|
||||
|
|
@ -93,6 +93,10 @@ class AWSManagedPolicy(ManagedPolicy):
|
|||
path=data.get('Path'),
|
||||
document=data.get('Document'))
|
||||
|
||||
@property
|
||||
def arn(self):
|
||||
return 'arn:aws:iam::aws:policy{0}{1}'.format(self.path, self.name)
|
||||
|
||||
|
||||
# AWS defines some of its own managed policies and we periodically
|
||||
# import them via `make aws_managed_policies`
|
||||
|
|
@ -901,5 +905,32 @@ class IAMBackend(BaseBackend):
|
|||
def delete_account_alias(self, alias):
|
||||
self.account_aliases = []
|
||||
|
||||
def get_account_authorization_details(self, filter):
|
||||
policies = self.managed_policies.values()
|
||||
local_policies = set(policies) - set(aws_managed_policies)
|
||||
returned_policies = []
|
||||
|
||||
if len(filter) == 0:
|
||||
return {
|
||||
'instance_profiles': self.instance_profiles.values(),
|
||||
'roles': self.roles.values(),
|
||||
'groups': self.groups.values(),
|
||||
'users': self.users.values(),
|
||||
'managed_policies': self.managed_policies.values()
|
||||
}
|
||||
|
||||
if 'AWSManagedPolicy' in filter:
|
||||
returned_policies = aws_managed_policies
|
||||
if 'LocalManagedPolicy' in filter:
|
||||
returned_policies = returned_policies + list(local_policies)
|
||||
|
||||
return {
|
||||
'instance_profiles': self.instance_profiles.values(),
|
||||
'roles': self.roles.values() if 'Role' in filter else [],
|
||||
'groups': self.groups.values() if 'Group' in filter else [],
|
||||
'users': self.users.values() if 'User' in filter else [],
|
||||
'managed_policies': returned_policies
|
||||
}
|
||||
|
||||
|
||||
iam_backend = IAMBackend()
|
||||
|
|
|
|||
|
|
@ -534,6 +534,18 @@ class IamResponse(BaseResponse):
|
|||
template = self.response_template(DELETE_ACCOUNT_ALIAS_TEMPLATE)
|
||||
return template.render()
|
||||
|
||||
def get_account_authorization_details(self):
|
||||
filter_param = self._get_multi_param('Filter.member')
|
||||
account_details = iam_backend.get_account_authorization_details(filter_param)
|
||||
template = self.response_template(GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE)
|
||||
return template.render(
|
||||
instance_profiles=account_details['instance_profiles'],
|
||||
policies=account_details['managed_policies'],
|
||||
users=account_details['users'],
|
||||
groups=account_details['groups'],
|
||||
roles=account_details['roles']
|
||||
)
|
||||
|
||||
|
||||
ATTACH_ROLE_POLICY_TEMPLATE = """<AttachRolePolicyResponse>
|
||||
<ResponseMetadata>
|
||||
|
|
@ -1309,3 +1321,144 @@ DELETE_ACCOUNT_ALIAS_TEMPLATE = """<DeleteAccountAliasResponse xmlns="https://ia
|
|||
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</DeleteAccountAliasResponse>"""
|
||||
|
||||
|
||||
LIST_GROUPS_FOR_USER_TEMPLATE = """<ListGroupsForUserResponse>
|
||||
<ListGroupsForUserResult>
|
||||
<Groups>
|
||||
{% for group in groups %}
|
||||
<member>
|
||||
<Path>{{ group.path }}</Path>
|
||||
<GroupName>{{ group.name }}</GroupName>
|
||||
<GroupId>{{ group.id }}</GroupId>
|
||||
<Arn>{{ group.arn }}</Arn>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Groups>
|
||||
<IsTruncated>false</IsTruncated>
|
||||
</ListGroupsForUserResult>
|
||||
<ResponseMetadata>
|
||||
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</ListGroupsForUserResponse>"""
|
||||
|
||||
|
||||
GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
|
||||
<GetAccountAuthorizationDetailsResult>
|
||||
<IsTruncated>false</IsTruncated>
|
||||
<UserDetailList>
|
||||
{% for user in users %}
|
||||
<member>
|
||||
<GroupList />
|
||||
<AttachedManagedPolicies/>
|
||||
<UserId>{{ user.id }}</UserId>
|
||||
<Path>{{ user.path }}</Path>
|
||||
<UserName>{{ user.name }}</UserName>
|
||||
<Arn>{{ user.arn }}</Arn>
|
||||
<CreateDate>2012-05-09T15:45:35Z</CreateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</UserDetailList>
|
||||
<Marker>
|
||||
EXAMPLEkakv9BCuUNFDtxWSyfzetYwEx2ADc8dnzfvERF5S6YMvXKx41t6gCl/eeaCX3Jo94/
|
||||
bKqezEAg8TEVS99EKFLxm3jtbpl25FDWEXAMPLE
|
||||
</Marker>
|
||||
<GroupDetailList>
|
||||
{% for group in groups %}
|
||||
<member>
|
||||
<GroupId>{{ group.id }}</GroupId>
|
||||
<AttachedManagedPolicies>
|
||||
{% for policy in group.managed_policies %}
|
||||
<member>
|
||||
<PolicyName>{{ policy.name }}</PolicyName>
|
||||
<PolicyArn>{{ policy.arn }}</PolicyArn>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</AttachedManagedPolicies>
|
||||
<GroupName>{{ group.name }}</GroupName>
|
||||
<Path>{{ group.path }}</Path>
|
||||
<Arn>{{ group.arn }}</Arn>
|
||||
<CreateDate>2012-05-09T16:27:11Z</CreateDate>
|
||||
<GroupPolicyList/>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</GroupDetailList>
|
||||
<RoleDetailList>
|
||||
{% for role in roles %}
|
||||
<member>
|
||||
<RolePolicyList/>
|
||||
<AttachedManagedPolicies>
|
||||
{% for policy in role.managed_policies %}
|
||||
<member>
|
||||
<PolicyName>{{ policy.name }}</PolicyName>
|
||||
<PolicyArn>{{ policy.arn }}</PolicyArn>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</AttachedManagedPolicies>
|
||||
<InstanceProfileList>
|
||||
{% for profile in instance_profiles %}
|
||||
<member>
|
||||
<Id>{{ profile.id }}</Id>
|
||||
<Roles>
|
||||
{% for role in profile.roles %}
|
||||
<member>
|
||||
<Path>{{ role.path }}</Path>
|
||||
<Arn>{{ role.arn }}</Arn>
|
||||
<RoleName>{{ role.name }}</RoleName>
|
||||
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
|
||||
<CreateDate>2012-05-09T15:45:35Z</CreateDate>
|
||||
<RoleId>{{ role.id }}</RoleId>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Roles>
|
||||
<InstanceProfileName>{{ profile.name }}</InstanceProfileName>
|
||||
<Path>{{ profile.path }}</Path>
|
||||
<Arn>{{ profile.arn }}</Arn>
|
||||
<CreateDate>2012-05-09T16:27:11Z</CreateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</InstanceProfileList>
|
||||
<Path>{{ role.path }}</Path>
|
||||
<Arn>{{ role.arn }}</Arn>
|
||||
<RoleName>{{ role.name }}</RoleName>
|
||||
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
|
||||
<CreateDate>2014-07-30T17:09:20Z</CreateDate>
|
||||
<RoleId>{{ role.id }}</RoleId>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</RoleDetailList>
|
||||
<Policies>
|
||||
{% for policy in policies %}
|
||||
<member>
|
||||
<PolicyName>{{ policy.name }}</PolicyName>
|
||||
<DefaultVersionId>{{ policy.default_version_id }}</DefaultVersionId>
|
||||
<PolicyId>{{ policy.id }}</PolicyId>
|
||||
<Path>{{ policy.path }}</Path>
|
||||
<PolicyVersionList>
|
||||
<member>
|
||||
<Document>
|
||||
{"Version":"2012-10-17","Statement":{"Effect":"Allow",
|
||||
"Action":["iam:CreatePolicy","iam:CreatePolicyVersion",
|
||||
"iam:DeletePolicy","iam:DeletePolicyVersion","iam:GetPolicy",
|
||||
"iam:GetPolicyVersion","iam:ListPolicies",
|
||||
"iam:ListPolicyVersions","iam:SetDefaultPolicyVersion"],
|
||||
"Resource":"*"}}
|
||||
</Document>
|
||||
<IsDefaultVersion>true</IsDefaultVersion>
|
||||
<VersionId>v1</VersionId>
|
||||
<CreateDate>2012-05-09T16:27:11Z</CreateDate>
|
||||
</member>
|
||||
</PolicyVersionList>
|
||||
<Arn>{{ policy.arn }}</Arn>
|
||||
<AttachmentCount>1</AttachmentCount>
|
||||
<CreateDate>2012-05-09T16:27:11Z</CreateDate>
|
||||
<IsAttachable>true</IsAttachable>
|
||||
<UpdateDate>2012-05-09T16:27:11Z</UpdateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Policies>
|
||||
</GetAccountAuthorizationDetailsResult>
|
||||
<ResponseMetadata>
|
||||
<RequestId>92e79ae7-7399-11e4-8c85-4b53eEXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</GetAccountAuthorizationDetailsResponse>"""
|
||||
|
|
|
|||
|
|
@ -19,19 +19,20 @@ from .utils import compose_shard_iterator, compose_new_shard_iterator, decompose
|
|||
|
||||
|
||||
class Record(BaseModel):
|
||||
|
||||
def __init__(self, partition_key, data, sequence_number, explicit_hash_key):
|
||||
self.partition_key = partition_key
|
||||
self.data = data
|
||||
self.sequence_number = sequence_number
|
||||
self.explicit_hash_key = explicit_hash_key
|
||||
self.create_at = unix_time()
|
||||
self.created_at_datetime = datetime.datetime.utcnow()
|
||||
self.created_at = unix_time(self.created_at_datetime)
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
"Data": self.data,
|
||||
"PartitionKey": self.partition_key,
|
||||
"SequenceNumber": str(self.sequence_number),
|
||||
"ApproximateArrivalTimestamp": self.created_at_datetime.isoformat()
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -50,16 +51,21 @@ class Shard(BaseModel):
|
|||
def get_records(self, last_sequence_id, limit):
|
||||
last_sequence_id = int(last_sequence_id)
|
||||
results = []
|
||||
secs_behind_latest = 0
|
||||
|
||||
for sequence_number, record in self.records.items():
|
||||
if sequence_number > last_sequence_id:
|
||||
results.append(record)
|
||||
last_sequence_id = sequence_number
|
||||
|
||||
very_last_record = self.records[next(reversed(self.records))]
|
||||
secs_behind_latest = very_last_record.created_at - record.created_at
|
||||
|
||||
if len(results) == limit:
|
||||
break
|
||||
|
||||
return results, last_sequence_id
|
||||
millis_behind_latest = int(secs_behind_latest * 1000)
|
||||
return results, last_sequence_id, millis_behind_latest
|
||||
|
||||
def put_record(self, partition_key, data, explicit_hash_key):
|
||||
# Note: this function is not safe for concurrency
|
||||
|
|
@ -83,12 +89,12 @@ class Shard(BaseModel):
|
|||
return 0
|
||||
|
||||
def get_sequence_number_at(self, at_timestamp):
|
||||
if not self.records or at_timestamp < list(self.records.values())[0].create_at:
|
||||
if not self.records or at_timestamp < list(self.records.values())[0].created_at:
|
||||
return 0
|
||||
else:
|
||||
# find the last item in the list that was created before
|
||||
# at_timestamp
|
||||
r = next((r for r in reversed(self.records.values()) if r.create_at < at_timestamp), None)
|
||||
r = next((r for r in reversed(self.records.values()) if r.created_at < at_timestamp), None)
|
||||
return r.sequence_number
|
||||
|
||||
def to_json(self):
|
||||
|
|
@ -226,7 +232,7 @@ class DeliveryStream(BaseModel):
|
|||
|
||||
self.records = []
|
||||
self.status = 'ACTIVE'
|
||||
self.create_at = datetime.datetime.utcnow()
|
||||
self.created_at = datetime.datetime.utcnow()
|
||||
self.last_updated = datetime.datetime.utcnow()
|
||||
|
||||
@property
|
||||
|
|
@ -267,7 +273,7 @@ class DeliveryStream(BaseModel):
|
|||
def to_dict(self):
|
||||
return {
|
||||
"DeliveryStreamDescription": {
|
||||
"CreateTimestamp": time.mktime(self.create_at.timetuple()),
|
||||
"CreateTimestamp": time.mktime(self.created_at.timetuple()),
|
||||
"DeliveryStreamARN": self.arn,
|
||||
"DeliveryStreamName": self.name,
|
||||
"DeliveryStreamStatus": self.status,
|
||||
|
|
@ -329,12 +335,12 @@ class KinesisBackend(BaseBackend):
|
|||
stream = self.describe_stream(stream_name)
|
||||
shard = stream.get_shard(shard_id)
|
||||
|
||||
records, last_sequence_id = shard.get_records(last_sequence_id, limit)
|
||||
records, last_sequence_id, millis_behind_latest = shard.get_records(last_sequence_id, limit)
|
||||
|
||||
next_shard_iterator = compose_shard_iterator(
|
||||
stream_name, shard, last_sequence_id)
|
||||
|
||||
return next_shard_iterator, records
|
||||
return next_shard_iterator, records, millis_behind_latest
|
||||
|
||||
def put_record(self, stream_name, partition_key, explicit_hash_key, sequence_number_for_ordering, data):
|
||||
stream = self.describe_stream(stream_name)
|
||||
|
|
|
|||
|
|
@ -80,12 +80,13 @@ class KinesisResponse(BaseResponse):
|
|||
shard_iterator = self.parameters.get("ShardIterator")
|
||||
limit = self.parameters.get("Limit")
|
||||
|
||||
next_shard_iterator, records = self.kinesis_backend.get_records(
|
||||
next_shard_iterator, records, millis_behind_latest = self.kinesis_backend.get_records(
|
||||
shard_iterator, limit)
|
||||
|
||||
return json.dumps({
|
||||
"NextShardIterator": next_shard_iterator,
|
||||
"Records": [record.to_json() for record in records]
|
||||
"Records": [record.to_json() for record in records],
|
||||
'MillisBehindLatest': millis_behind_latest
|
||||
})
|
||||
|
||||
def put_record(self):
|
||||
|
|
|
|||
|
|
@ -58,6 +58,12 @@ class Key(BaseModel):
|
|||
|
||||
return key
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
if attribute_name == 'Arn':
|
||||
return self.arn
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
|
||||
class KmsBackend(BaseBackend):
|
||||
|
||||
|
|
|
|||
|
|
@ -175,11 +175,14 @@ class FakeMultipart(BaseModel):
|
|||
count = 0
|
||||
for pn, etag in body:
|
||||
part = self.parts.get(pn)
|
||||
if part is None or part.etag != etag:
|
||||
part_etag = None
|
||||
if part is not None:
|
||||
part_etag = part.etag.replace('"', '')
|
||||
etag = etag.replace('"', '')
|
||||
if part is None or part_etag != etag:
|
||||
raise InvalidPart()
|
||||
if last is not None and len(last.value) < UPLOAD_PART_MIN_SIZE:
|
||||
raise EntityTooSmall()
|
||||
part_etag = part.etag.replace('"', '')
|
||||
md5s.extend(decode_hex(part_etag)[0])
|
||||
total.extend(part.value)
|
||||
last = part
|
||||
|
|
@ -718,7 +721,7 @@ class S3Backend(BaseBackend):
|
|||
if key_name in bucket.keys:
|
||||
key = bucket.keys[key_name]
|
||||
else:
|
||||
for key_version in bucket.keys.getlist(key_name):
|
||||
for key_version in bucket.keys.getlist(key_name, default=[]):
|
||||
if str(key_version.version_id) == str(version_id):
|
||||
key = key_version
|
||||
break
|
||||
|
|
|
|||
29
moto/secretsmanager/exceptions.py
Normal file
29
moto/secretsmanager/exceptions.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
from __future__ import unicode_literals
|
||||
from moto.core.exceptions import JsonRESTError
|
||||
|
||||
|
||||
class SecretsManagerClientError(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
|
||||
class ResourceNotFoundException(SecretsManagerClientError):
|
||||
def __init__(self):
|
||||
self.code = 404
|
||||
super(ResourceNotFoundException, self).__init__(
|
||||
"ResourceNotFoundException",
|
||||
"Secrets Manager can't find the specified secret"
|
||||
)
|
||||
|
||||
|
||||
class ClientError(SecretsManagerClientError):
|
||||
def __init__(self, message):
|
||||
super(ClientError, self).__init__(
|
||||
'InvalidParameterValue',
|
||||
message)
|
||||
|
||||
|
||||
class InvalidParameterException(SecretsManagerClientError):
|
||||
def __init__(self, message):
|
||||
super(InvalidParameterException, self).__init__(
|
||||
'InvalidParameterException',
|
||||
message)
|
||||
|
|
@ -6,14 +6,22 @@ import json
|
|||
import boto3
|
||||
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from .exceptions import (
|
||||
ResourceNotFoundException,
|
||||
InvalidParameterException,
|
||||
ClientError
|
||||
)
|
||||
from .utils import random_password, secret_arn
|
||||
|
||||
|
||||
class SecretsManager(BaseModel):
|
||||
|
||||
def __init__(self, region_name, **kwargs):
|
||||
self.region = region_name
|
||||
self.secret_id = kwargs.get('secret_id', '')
|
||||
self.version_id = kwargs.get('version_id', '')
|
||||
self.version_stage = kwargs.get('version_stage', '')
|
||||
self.secret_string = ''
|
||||
|
||||
|
||||
class SecretsManagerBackend(BaseBackend):
|
||||
|
|
@ -22,15 +30,31 @@ class SecretsManagerBackend(BaseBackend):
|
|||
super(SecretsManagerBackend, self).__init__()
|
||||
self.region = region_name
|
||||
self.secret_id = kwargs.get('secret_id', '')
|
||||
self.name = kwargs.get('name', '')
|
||||
self.createdate = int(time.time())
|
||||
self.secret_string = ''
|
||||
self.rotation_enabled = False
|
||||
self.rotation_lambda_arn = ''
|
||||
self.auto_rotate_after_days = 0
|
||||
|
||||
def reset(self):
|
||||
region_name = self.region
|
||||
self.__dict__ = {}
|
||||
self.__init__(region_name)
|
||||
|
||||
def _is_valid_identifier(self, identifier):
|
||||
return identifier in (self.name, self.secret_id)
|
||||
|
||||
def get_secret_value(self, secret_id, version_id, version_stage):
|
||||
|
||||
if not self._is_valid_identifier(secret_id):
|
||||
raise ResourceNotFoundException()
|
||||
|
||||
response = json.dumps({
|
||||
"ARN": self.secret_arn(),
|
||||
"Name": self.secret_id,
|
||||
"ARN": secret_arn(self.region, self.secret_id),
|
||||
"Name": self.name,
|
||||
"VersionId": "A435958A-D821-4193-B719-B7769357AER4",
|
||||
"SecretString": "mysecretstring",
|
||||
"SecretString": self.secret_string,
|
||||
"VersionStages": [
|
||||
"AWSCURRENT",
|
||||
],
|
||||
|
|
@ -39,11 +63,80 @@ class SecretsManagerBackend(BaseBackend):
|
|||
|
||||
return response
|
||||
|
||||
def secret_arn(self):
|
||||
return "arn:aws:secretsmanager:{0}:1234567890:secret:{1}-rIjad".format(
|
||||
self.region, self.secret_id)
|
||||
def create_secret(self, name, secret_string, **kwargs):
|
||||
|
||||
self.secret_string = secret_string
|
||||
self.secret_id = name
|
||||
self.name = name
|
||||
|
||||
response = json.dumps({
|
||||
"ARN": secret_arn(self.region, name),
|
||||
"Name": self.name,
|
||||
"VersionId": "A435958A-D821-4193-B719-B7769357AER4",
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
def describe_secret(self, secret_id):
|
||||
if not self._is_valid_identifier(secret_id):
|
||||
raise ResourceNotFoundException
|
||||
|
||||
response = json.dumps({
|
||||
"ARN": secret_arn(self.region, self.secret_id),
|
||||
"Name": self.name,
|
||||
"Description": "",
|
||||
"KmsKeyId": "",
|
||||
"RotationEnabled": self.rotation_enabled,
|
||||
"RotationLambdaARN": self.rotation_lambda_arn,
|
||||
"RotationRules": {
|
||||
"AutomaticallyAfterDays": self.auto_rotate_after_days
|
||||
},
|
||||
"LastRotatedDate": None,
|
||||
"LastChangedDate": None,
|
||||
"LastAccessedDate": None,
|
||||
"DeletedDate": None,
|
||||
"Tags": [
|
||||
{
|
||||
"Key": "",
|
||||
"Value": ""
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
def get_random_password(self, password_length,
|
||||
exclude_characters, exclude_numbers,
|
||||
exclude_punctuation, exclude_uppercase,
|
||||
exclude_lowercase, include_space,
|
||||
require_each_included_type):
|
||||
# password size must have value less than or equal to 4096
|
||||
if password_length > 4096:
|
||||
raise ClientError(
|
||||
"ClientError: An error occurred (ValidationException) \
|
||||
when calling the GetRandomPassword operation: 1 validation error detected: Value '{}' at 'passwordLength' \
|
||||
failed to satisfy constraint: Member must have value less than or equal to 4096".format(password_length))
|
||||
if password_length < 4:
|
||||
raise InvalidParameterException(
|
||||
"InvalidParameterException: An error occurred (InvalidParameterException) \
|
||||
when calling the GetRandomPassword operation: Password length is too short based on the required types.")
|
||||
|
||||
response = json.dumps({
|
||||
"RandomPassword": random_password(password_length,
|
||||
exclude_characters,
|
||||
exclude_numbers,
|
||||
exclude_punctuation,
|
||||
exclude_uppercase,
|
||||
exclude_lowercase,
|
||||
include_space,
|
||||
require_each_included_type)
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
|
||||
available_regions = boto3.session.Session().get_available_regions("secretsmanager")
|
||||
print(available_regions)
|
||||
secretsmanager_backends = {region: SecretsManagerBackend(region_name=region) for region in available_regions}
|
||||
available_regions = (
|
||||
boto3.session.Session().get_available_regions("secretsmanager")
|
||||
)
|
||||
secretsmanager_backends = {region: SecretsManagerBackend(region_name=region)
|
||||
for region in available_regions}
|
||||
|
|
|
|||
|
|
@ -15,3 +15,38 @@ class SecretsManagerResponse(BaseResponse):
|
|||
secret_id=secret_id,
|
||||
version_id=version_id,
|
||||
version_stage=version_stage)
|
||||
|
||||
def create_secret(self):
|
||||
name = self._get_param('Name')
|
||||
secret_string = self._get_param('SecretString')
|
||||
return secretsmanager_backends[self.region].create_secret(
|
||||
name=name,
|
||||
secret_string=secret_string
|
||||
)
|
||||
|
||||
def get_random_password(self):
|
||||
password_length = self._get_param('PasswordLength', if_none=32)
|
||||
exclude_characters = self._get_param('ExcludeCharacters', if_none='')
|
||||
exclude_numbers = self._get_param('ExcludeNumbers', if_none=False)
|
||||
exclude_punctuation = self._get_param('ExcludePunctuation', if_none=False)
|
||||
exclude_uppercase = self._get_param('ExcludeUppercase', if_none=False)
|
||||
exclude_lowercase = self._get_param('ExcludeLowercase', if_none=False)
|
||||
include_space = self._get_param('IncludeSpace', if_none=False)
|
||||
require_each_included_type = self._get_param(
|
||||
'RequireEachIncludedType', if_none=True)
|
||||
return secretsmanager_backends[self.region].get_random_password(
|
||||
password_length=password_length,
|
||||
exclude_characters=exclude_characters,
|
||||
exclude_numbers=exclude_numbers,
|
||||
exclude_punctuation=exclude_punctuation,
|
||||
exclude_uppercase=exclude_uppercase,
|
||||
exclude_lowercase=exclude_lowercase,
|
||||
include_space=include_space,
|
||||
require_each_included_type=require_each_included_type
|
||||
)
|
||||
|
||||
def describe_secret(self):
|
||||
secret_id = self._get_param('SecretId')
|
||||
return secretsmanager_backends[self.region].describe_secret(
|
||||
secret_id=secret_id
|
||||
)
|
||||
|
|
|
|||
72
moto/secretsmanager/utils.py
Normal file
72
moto/secretsmanager/utils.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import random
|
||||
import string
|
||||
import six
|
||||
import re
|
||||
|
||||
|
||||
def random_password(password_length, exclude_characters, exclude_numbers,
|
||||
exclude_punctuation, exclude_uppercase, exclude_lowercase,
|
||||
include_space, require_each_included_type):
|
||||
|
||||
password = ''
|
||||
required_characters = ''
|
||||
|
||||
if not exclude_lowercase and not exclude_uppercase:
|
||||
password += string.ascii_letters
|
||||
required_characters += random.choice(_exclude_characters(
|
||||
string.ascii_lowercase, exclude_characters))
|
||||
required_characters += random.choice(_exclude_characters(
|
||||
string.ascii_uppercase, exclude_characters))
|
||||
elif not exclude_lowercase:
|
||||
password += string.ascii_lowercase
|
||||
required_characters += random.choice(_exclude_characters(
|
||||
string.ascii_lowercase, exclude_characters))
|
||||
elif not exclude_uppercase:
|
||||
password += string.ascii_uppercase
|
||||
required_characters += random.choice(_exclude_characters(
|
||||
string.ascii_uppercase, exclude_characters))
|
||||
if not exclude_numbers:
|
||||
password += string.digits
|
||||
required_characters += random.choice(_exclude_characters(
|
||||
string.digits, exclude_characters))
|
||||
if not exclude_punctuation:
|
||||
password += string.punctuation
|
||||
required_characters += random.choice(_exclude_characters(
|
||||
string.punctuation, exclude_characters))
|
||||
if include_space:
|
||||
password += " "
|
||||
required_characters += " "
|
||||
|
||||
password = ''.join(
|
||||
six.text_type(random.choice(password))
|
||||
for x in range(password_length))
|
||||
|
||||
if require_each_included_type:
|
||||
password = _add_password_require_each_included_type(
|
||||
password, required_characters)
|
||||
|
||||
password = _exclude_characters(password, exclude_characters)
|
||||
return password
|
||||
|
||||
|
||||
def secret_arn(region, secret_id):
|
||||
return "arn:aws:secretsmanager:{0}:1234567890:secret:{1}-rIjad".format(
|
||||
region, secret_id)
|
||||
|
||||
|
||||
def _exclude_characters(password, exclude_characters):
|
||||
for c in exclude_characters:
|
||||
if c in string.punctuation:
|
||||
# Escape punctuation regex usage
|
||||
c = "\{0}".format(c)
|
||||
password = re.sub(c, '', str(password))
|
||||
return password
|
||||
|
||||
|
||||
def _add_password_require_each_included_type(password, required_characters):
|
||||
password_with_required_char = password[:-len(required_characters)]
|
||||
password_with_required_char += required_characters
|
||||
|
||||
return password_with_required_char
|
||||
|
|
@ -13,14 +13,21 @@ RECIPIENT_LIMIT = 50
|
|||
|
||||
class Message(BaseModel):
|
||||
|
||||
def __init__(self, message_id):
|
||||
def __init__(self, message_id, source, subject, body, destinations):
|
||||
self.id = message_id
|
||||
self.source = source
|
||||
self.subject = subject
|
||||
self.body = body
|
||||
self.destinations = destinations
|
||||
|
||||
|
||||
class RawMessage(BaseModel):
|
||||
|
||||
def __init__(self, message_id):
|
||||
def __init__(self, message_id, source, destinations, raw_data):
|
||||
self.id = message_id
|
||||
self.source = source
|
||||
self.destinations = destinations
|
||||
self.raw_data = raw_data
|
||||
|
||||
|
||||
class SESQuota(BaseModel):
|
||||
|
|
@ -79,7 +86,7 @@ class SESBackend(BaseBackend):
|
|||
)
|
||||
|
||||
message_id = get_random_message_id()
|
||||
message = Message(message_id)
|
||||
message = Message(message_id, source, subject, body, destinations)
|
||||
self.sent_messages.append(message)
|
||||
self.sent_message_count += recipient_count
|
||||
return message
|
||||
|
|
@ -116,7 +123,7 @@ class SESBackend(BaseBackend):
|
|||
|
||||
self.sent_message_count += recipient_count
|
||||
message_id = get_random_message_id()
|
||||
message = RawMessage(message_id)
|
||||
message = RawMessage(message_id, source, destinations, raw_data)
|
||||
self.sent_messages.append(message)
|
||||
return message
|
||||
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ class SNSResponse(BaseResponse):
|
|||
topic_arn = self._get_param('TopicArn')
|
||||
endpoint = self._get_param('Endpoint')
|
||||
protocol = self._get_param('Protocol')
|
||||
attributes = self._get_attributes()
|
||||
|
||||
if protocol == 'sms' and not is_e164(endpoint):
|
||||
return self._error(
|
||||
|
|
@ -190,6 +191,10 @@ class SNSResponse(BaseResponse):
|
|||
|
||||
subscription = self.backend.subscribe(topic_arn, endpoint, protocol)
|
||||
|
||||
if attributes is not None:
|
||||
for attr_name, attr_value in attributes.items():
|
||||
self.backend.set_subscription_attributes(subscription.arn, attr_name, attr_value)
|
||||
|
||||
if self.request_json:
|
||||
return json.dumps({
|
||||
"SubscribeResponse": {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||
from collections import defaultdict
|
||||
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.core.exceptions import RESTError
|
||||
from moto.ec2 import ec2_backends
|
||||
|
||||
import datetime
|
||||
|
|
@ -58,11 +59,86 @@ class Parameter(BaseModel):
|
|||
return r
|
||||
|
||||
|
||||
MAX_TIMEOUT_SECONDS = 3600
|
||||
|
||||
|
||||
class Command(BaseModel):
|
||||
def __init__(self, comment='', document_name='', timeout_seconds=MAX_TIMEOUT_SECONDS,
|
||||
instance_ids=None, max_concurrency='', max_errors='',
|
||||
notification_config=None, output_s3_bucket_name='',
|
||||
output_s3_key_prefix='', output_s3_region='', parameters=None,
|
||||
service_role_arn='', targets=None):
|
||||
|
||||
if instance_ids is None:
|
||||
instance_ids = []
|
||||
|
||||
if notification_config is None:
|
||||
notification_config = {}
|
||||
|
||||
if parameters is None:
|
||||
parameters = {}
|
||||
|
||||
if targets is None:
|
||||
targets = []
|
||||
|
||||
self.error_count = 0
|
||||
self.completed_count = len(instance_ids)
|
||||
self.target_count = len(instance_ids)
|
||||
self.command_id = str(uuid.uuid4())
|
||||
self.status = 'Success'
|
||||
self.status_details = 'Details placeholder'
|
||||
|
||||
now = datetime.datetime.now()
|
||||
self.requested_date_time = now.isoformat()
|
||||
expires_after = now + datetime.timedelta(0, timeout_seconds)
|
||||
self.expires_after = expires_after.isoformat()
|
||||
|
||||
self.comment = comment
|
||||
self.document_name = document_name
|
||||
self.instance_ids = instance_ids
|
||||
self.max_concurrency = max_concurrency
|
||||
self.max_errors = max_errors
|
||||
self.notification_config = notification_config
|
||||
self.output_s3_bucket_name = output_s3_bucket_name
|
||||
self.output_s3_key_prefix = output_s3_key_prefix
|
||||
self.output_s3_region = output_s3_region
|
||||
self.parameters = parameters
|
||||
self.service_role_arn = service_role_arn
|
||||
self.targets = targets
|
||||
|
||||
def response_object(self):
|
||||
r = {
|
||||
'CommandId': self.command_id,
|
||||
'Comment': self.comment,
|
||||
'CompletedCount': self.completed_count,
|
||||
'DocumentName': self.document_name,
|
||||
'ErrorCount': self.error_count,
|
||||
'ExpiresAfter': self.expires_after,
|
||||
'InstanceIds': self.instance_ids,
|
||||
'MaxConcurrency': self.max_concurrency,
|
||||
'MaxErrors': self.max_errors,
|
||||
'NotificationConfig': self.notification_config,
|
||||
'OutputS3Region': self.output_s3_region,
|
||||
'OutputS3BucketName': self.output_s3_bucket_name,
|
||||
'OutputS3KeyPrefix': self.output_s3_key_prefix,
|
||||
'Parameters': self.parameters,
|
||||
'RequestedDateTime': self.requested_date_time,
|
||||
'ServiceRole': self.service_role_arn,
|
||||
'Status': self.status,
|
||||
'StatusDetails': self.status_details,
|
||||
'TargetCount': self.target_count,
|
||||
'Targets': self.targets,
|
||||
}
|
||||
|
||||
return r
|
||||
|
||||
|
||||
class SimpleSystemManagerBackend(BaseBackend):
|
||||
|
||||
def __init__(self):
|
||||
self._parameters = {}
|
||||
self._resource_tags = defaultdict(lambda: defaultdict(dict))
|
||||
self._commands = []
|
||||
|
||||
def delete_parameter(self, name):
|
||||
try:
|
||||
|
|
@ -100,7 +176,7 @@ class SimpleSystemManagerBackend(BaseBackend):
|
|||
# difference here.
|
||||
path = path.rstrip('/') + '/'
|
||||
for param in self._parameters:
|
||||
if not param.startswith(path):
|
||||
if path != '/' and not param.startswith(path):
|
||||
continue
|
||||
if '/' in param[len(path) + 1:] and not recursive:
|
||||
continue
|
||||
|
|
@ -167,38 +243,61 @@ class SimpleSystemManagerBackend(BaseBackend):
|
|||
return self._resource_tags[resource_type][resource_id]
|
||||
|
||||
def send_command(self, **kwargs):
|
||||
instances = kwargs.get('InstanceIds', [])
|
||||
now = datetime.datetime.now()
|
||||
expires_after = now + datetime.timedelta(0, int(kwargs.get('TimeoutSeconds', 3600)))
|
||||
command = Command(
|
||||
comment=kwargs.get('Comment', ''),
|
||||
document_name=kwargs.get('DocumentName'),
|
||||
timeout_seconds=kwargs.get('TimeoutSeconds', 3600),
|
||||
instance_ids=kwargs.get('InstanceIds', []),
|
||||
max_concurrency=kwargs.get('MaxConcurrency', '50'),
|
||||
max_errors=kwargs.get('MaxErrors', '0'),
|
||||
notification_config=kwargs.get('NotificationConfig', {
|
||||
'NotificationArn': 'string',
|
||||
'NotificationEvents': ['Success'],
|
||||
'NotificationType': 'Command'
|
||||
}),
|
||||
output_s3_bucket_name=kwargs.get('OutputS3BucketName', ''),
|
||||
output_s3_key_prefix=kwargs.get('OutputS3KeyPrefix', ''),
|
||||
output_s3_region=kwargs.get('OutputS3Region', ''),
|
||||
parameters=kwargs.get('Parameters', {}),
|
||||
service_role_arn=kwargs.get('ServiceRoleArn', ''),
|
||||
targets=kwargs.get('Targets', []))
|
||||
|
||||
self._commands.append(command)
|
||||
return {
|
||||
'Command': {
|
||||
'CommandId': str(uuid.uuid4()),
|
||||
'DocumentName': kwargs['DocumentName'],
|
||||
'Comment': kwargs.get('Comment'),
|
||||
'ExpiresAfter': expires_after.isoformat(),
|
||||
'Parameters': kwargs['Parameters'],
|
||||
'InstanceIds': kwargs['InstanceIds'],
|
||||
'Targets': kwargs.get('targets'),
|
||||
'RequestedDateTime': now.isoformat(),
|
||||
'Status': 'Success',
|
||||
'StatusDetails': 'string',
|
||||
'OutputS3Region': kwargs.get('OutputS3Region'),
|
||||
'OutputS3BucketName': kwargs.get('OutputS3BucketName'),
|
||||
'OutputS3KeyPrefix': kwargs.get('OutputS3KeyPrefix'),
|
||||
'MaxConcurrency': 'string',
|
||||
'MaxErrors': 'string',
|
||||
'TargetCount': len(instances),
|
||||
'CompletedCount': len(instances),
|
||||
'ErrorCount': 0,
|
||||
'ServiceRole': kwargs.get('ServiceRoleArn'),
|
||||
'NotificationConfig': {
|
||||
'NotificationArn': 'string',
|
||||
'NotificationEvents': ['Success'],
|
||||
'NotificationType': 'Command'
|
||||
}
|
||||
}
|
||||
'Command': command.response_object()
|
||||
}
|
||||
|
||||
def list_commands(self, **kwargs):
|
||||
"""
|
||||
https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_ListCommands.html
|
||||
"""
|
||||
commands = self._commands
|
||||
|
||||
command_id = kwargs.get('CommandId', None)
|
||||
if command_id:
|
||||
commands = [self.get_command_by_id(command_id)]
|
||||
instance_id = kwargs.get('InstanceId', None)
|
||||
if instance_id:
|
||||
commands = self.get_commands_by_instance_id(instance_id)
|
||||
|
||||
return {
|
||||
'Commands': [command.response_object() for command in commands]
|
||||
}
|
||||
|
||||
def get_command_by_id(self, id):
|
||||
command = next(
|
||||
(command for command in self._commands if command.command_id == id), None)
|
||||
|
||||
if command is None:
|
||||
raise RESTError('InvalidCommandId', 'Invalid command id.')
|
||||
|
||||
return command
|
||||
|
||||
def get_commands_by_instance_id(self, instance_id):
|
||||
return [
|
||||
command for command in self._commands
|
||||
if instance_id in command.instance_ids]
|
||||
|
||||
|
||||
ssm_backends = {}
|
||||
for region, ec2_backend in ec2_backends.items():
|
||||
|
|
|
|||
|
|
@ -205,3 +205,8 @@ class SimpleSystemManagerResponse(BaseResponse):
|
|||
return json.dumps(
|
||||
self.ssm_backend.send_command(**self.request_params)
|
||||
)
|
||||
|
||||
def list_commands(self):
|
||||
return json.dumps(
|
||||
self.ssm_backend.list_commands(**self.request_params)
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue