Support iot and iot-data (#1303)
* append appropriate urls when scaffolding * make dispatch for rest-api * fix dispatch for rest-json * fix moto/core/response to obtain path and body parameters * small fixes * remove unused import * fix get_int_param * Add features of things and thing-types * fix scaffold * basic crud of cert * support basic CRUD of policy * refactor * fix formatting of scaffold * support principal_pocicy * support thing_principal * update readme * escape service to handle service w/ hyphen like iot-data * escape service w/ hyphen * fix regexp to extract region from url * escape service * Implement basic iota-data feature * iot-data shadow delta * update readme * remove unused import * remove comment * fix syntax * specify region when creating boto3 client for test * use uuid for seed of generating cert id * specify region_name to iotdata client in test * specify region to boto3 client in moto response * excude iot and iotdata tests on server mode * fix handling of thingTypeName in describe-thing * test if server is up for iot
This commit is contained in:
parent
884fc6f260
commit
0de2e55b13
11
Makefile
11
Makefile
@ -1,5 +1,13 @@
|
|||||||
SHELL := /bin/bash
|
SHELL := /bin/bash
|
||||||
|
|
||||||
|
ifeq ($(TEST_SERVER_MODE), true)
|
||||||
|
# exclude test_iot and test_iotdata for now
|
||||||
|
# because authentication of iot is very complicated
|
||||||
|
TEST_EXCLUDE := --exclude='test_iot.*'
|
||||||
|
else
|
||||||
|
TEST_EXCLUDE :=
|
||||||
|
endif
|
||||||
|
|
||||||
init:
|
init:
|
||||||
@python setup.py develop
|
@python setup.py develop
|
||||||
@pip install -r requirements.txt
|
@pip install -r requirements.txt
|
||||||
@ -10,8 +18,7 @@ lint:
|
|||||||
test: lint
|
test: lint
|
||||||
rm -f .coverage
|
rm -f .coverage
|
||||||
rm -rf cover
|
rm -rf cover
|
||||||
@nosetests -sv --with-coverage --cover-html ./tests/
|
@nosetests -sv --with-coverage --cover-html ./tests/ $(TEST_EXCLUDE)
|
||||||
|
|
||||||
test_server:
|
test_server:
|
||||||
@TEST_SERVER_MODE=true nosetests -sv --with-coverage --cover-html ./tests/
|
@TEST_SERVER_MODE=true nosetests -sv --with-coverage --cover-html ./tests/
|
||||||
|
|
||||||
|
@ -96,6 +96,9 @@ It gets even better! Moto isn't just for Python code and it isn't just for S3. L
|
|||||||
|------------------------------------------------------------------------------|
|
|------------------------------------------------------------------------------|
|
||||||
| IAM | @mock_iam | core endpoints done |
|
| IAM | @mock_iam | core endpoints done |
|
||||||
|------------------------------------------------------------------------------|
|
|------------------------------------------------------------------------------|
|
||||||
|
| IoT | @mock_iot | core endpoints done |
|
||||||
|
| | @mock_iotdata | core endpoints done |
|
||||||
|
|------------------------------------------------------------------------------|
|
||||||
| Lambda | @mock_lambda | basic endpoints done, requires |
|
| Lambda | @mock_lambda | basic endpoints done, requires |
|
||||||
| | | docker |
|
| | | docker |
|
||||||
|------------------------------------------------------------------------------|
|
|------------------------------------------------------------------------------|
|
||||||
@ -299,6 +302,7 @@ boto3.resource(
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ pip install moto
|
$ pip install moto
|
||||||
```
|
```
|
||||||
|
@ -41,6 +41,8 @@ from .swf import mock_swf, mock_swf_deprecated # flake8: noqa
|
|||||||
from .xray import mock_xray, mock_xray_client, XRaySegment # flake8: noqa
|
from .xray import mock_xray, mock_xray_client, XRaySegment # flake8: noqa
|
||||||
from .logs import mock_logs, mock_logs_deprecated # flake8: noqa
|
from .logs import mock_logs, mock_logs_deprecated # flake8: noqa
|
||||||
from .batch import mock_batch # flake8: noqa
|
from .batch import mock_batch # flake8: noqa
|
||||||
|
from .iot import mock_iot # flake8: noqa
|
||||||
|
from .iotdata import mock_iotdata # flake8: noqa
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -35,6 +35,8 @@ from moto.sqs import sqs_backends
|
|||||||
from moto.ssm import ssm_backends
|
from moto.ssm import ssm_backends
|
||||||
from moto.sts import sts_backends
|
from moto.sts import sts_backends
|
||||||
from moto.xray import xray_backends
|
from moto.xray import xray_backends
|
||||||
|
from moto.iot import iot_backends
|
||||||
|
from moto.iotdata import iotdata_backends
|
||||||
from moto.batch import batch_backends
|
from moto.batch import batch_backends
|
||||||
|
|
||||||
|
|
||||||
@ -76,6 +78,8 @@ BACKENDS = {
|
|||||||
'route53': route53_backends,
|
'route53': route53_backends,
|
||||||
'lambda': lambda_backends,
|
'lambda': lambda_backends,
|
||||||
'xray': xray_backends,
|
'xray': xray_backends,
|
||||||
|
'iot': iot_backends,
|
||||||
|
'iot-data': iotdata_backends,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -214,7 +214,7 @@ class BaseResponse(_TemplateEnvironmentMixin):
|
|||||||
if not hasattr(self, 'SERVICE_NAME'):
|
if not hasattr(self, 'SERVICE_NAME'):
|
||||||
return None
|
return None
|
||||||
service = self.SERVICE_NAME
|
service = self.SERVICE_NAME
|
||||||
conn = boto3.client(service)
|
conn = boto3.client(service, region_name=self.region)
|
||||||
|
|
||||||
# make cache if it does not exist yet
|
# make cache if it does not exist yet
|
||||||
if not hasattr(self, 'method_urls'):
|
if not hasattr(self, 'method_urls'):
|
||||||
|
6
moto/iot/__init__.py
Normal file
6
moto/iot/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from .models import iot_backends
|
||||||
|
from ..core.models import base_decorator
|
||||||
|
|
||||||
|
iot_backend = iot_backends['us-east-1']
|
||||||
|
mock_iot = base_decorator(iot_backends)
|
24
moto/iot/exceptions.py
Normal file
24
moto/iot/exceptions.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from moto.core.exceptions import RESTError
|
||||||
|
|
||||||
|
|
||||||
|
class IoTClientError(RESTError):
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceNotFoundException(IoTClientError):
|
||||||
|
def __init__(self):
|
||||||
|
self.code = 400
|
||||||
|
super(ResourceNotFoundException, self).__init__(
|
||||||
|
"ResourceNotFoundException",
|
||||||
|
"The specified resource does not exist"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidRequestException(IoTClientError):
|
||||||
|
def __init__(self):
|
||||||
|
self.code = 400
|
||||||
|
super(InvalidRequestException, self).__init__(
|
||||||
|
"InvalidRequestException",
|
||||||
|
"The request is not valid."
|
||||||
|
)
|
364
moto/iot/models.py
Normal file
364
moto/iot/models.py
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
import time
|
||||||
|
import boto3
|
||||||
|
import string
|
||||||
|
import random
|
||||||
|
import hashlib
|
||||||
|
import uuid
|
||||||
|
from moto.core import BaseBackend, BaseModel
|
||||||
|
from collections import OrderedDict
|
||||||
|
from .exceptions import (
|
||||||
|
ResourceNotFoundException,
|
||||||
|
InvalidRequestException
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeThing(BaseModel):
|
||||||
|
def __init__(self, thing_name, thing_type, attributes, region_name):
|
||||||
|
self.region_name = region_name
|
||||||
|
self.thing_name = thing_name
|
||||||
|
self.thing_type = thing_type
|
||||||
|
self.attributes = attributes
|
||||||
|
self.arn = 'arn:aws:iot:%s:1:thing/%s' % (self.region_name, thing_name)
|
||||||
|
self.version = 1
|
||||||
|
# TODO: we need to handle 'version'?
|
||||||
|
|
||||||
|
# for iot-data
|
||||||
|
self.thing_shadow = None
|
||||||
|
|
||||||
|
def to_dict(self, include_default_client_id=False):
|
||||||
|
obj = {
|
||||||
|
'thingName': self.thing_name,
|
||||||
|
'attributes': self.attributes,
|
||||||
|
'version': self.version
|
||||||
|
}
|
||||||
|
if self.thing_type:
|
||||||
|
obj['thingTypeName'] = self.thing_type.thing_type_name
|
||||||
|
if include_default_client_id:
|
||||||
|
obj['defaultClientId'] = self.thing_name
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class FakeThingType(BaseModel):
|
||||||
|
def __init__(self, thing_type_name, thing_type_properties, region_name):
|
||||||
|
self.region_name = region_name
|
||||||
|
self.thing_type_name = thing_type_name
|
||||||
|
self.thing_type_properties = thing_type_properties
|
||||||
|
t = time.time()
|
||||||
|
self.metadata = {
|
||||||
|
'deprecated': False,
|
||||||
|
'creationData': int(t * 1000) / 1000.0
|
||||||
|
}
|
||||||
|
self.arn = 'arn:aws:iot:%s:1:thingtype/%s' % (self.region_name, thing_type_name)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'thingTypeName': self.thing_type_name,
|
||||||
|
'thingTypeProperties': self.thing_type_properties,
|
||||||
|
'thingTypeMetadata': self.metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FakeCertificate(BaseModel):
|
||||||
|
def __init__(self, certificate_pem, status, region_name):
|
||||||
|
m = hashlib.sha256()
|
||||||
|
m.update(str(uuid.uuid4()).encode('utf-8'))
|
||||||
|
self.certificate_id = m.hexdigest()
|
||||||
|
self.arn = 'arn:aws:iot:%s:1:cert/%s' % (region_name, self.certificate_id)
|
||||||
|
self.certificate_pem = certificate_pem
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
# TODO: must adjust
|
||||||
|
self.owner = '1'
|
||||||
|
self.transfer_data = {}
|
||||||
|
self.creation_date = time.time()
|
||||||
|
self.last_modified_date = self.creation_date
|
||||||
|
self.ca_certificate_id = None
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'certificateArn': self.arn,
|
||||||
|
'certificateId': self.certificate_id,
|
||||||
|
'status': self.status,
|
||||||
|
'creationDate': self.creation_date
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_description_dict(self):
|
||||||
|
"""
|
||||||
|
You might need keys below in some situation
|
||||||
|
- caCertificateId
|
||||||
|
- previousOwnedBy
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'certificateArn': self.arn,
|
||||||
|
'certificateId': self.certificate_id,
|
||||||
|
'status': self.status,
|
||||||
|
'certificatePem': self.certificate_pem,
|
||||||
|
'ownedBy': self.owner,
|
||||||
|
'creationDate': self.creation_date,
|
||||||
|
'lastModifiedDate': self.last_modified_date,
|
||||||
|
'transferData': self.transfer_data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FakePolicy(BaseModel):
|
||||||
|
def __init__(self, name, document, region_name):
|
||||||
|
self.name = name
|
||||||
|
self.document = document
|
||||||
|
self.arn = 'arn:aws:iot:%s:1:policy/%s' % (region_name, name)
|
||||||
|
self.version = '1' # TODO: handle version
|
||||||
|
|
||||||
|
def to_get_dict(self):
|
||||||
|
return {
|
||||||
|
'policyName': self.name,
|
||||||
|
'policyArn': self.arn,
|
||||||
|
'policyDocument': self.document,
|
||||||
|
'defaultVersionId': self.version
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_dict_at_creation(self):
|
||||||
|
return {
|
||||||
|
'policyName': self.name,
|
||||||
|
'policyArn': self.arn,
|
||||||
|
'policyDocument': self.document,
|
||||||
|
'policyVersionId': self.version
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'policyName': self.name,
|
||||||
|
'policyArn': self.arn,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class IoTBackend(BaseBackend):
|
||||||
|
def __init__(self, region_name=None):
|
||||||
|
super(IoTBackend, self).__init__()
|
||||||
|
self.region_name = region_name
|
||||||
|
self.things = OrderedDict()
|
||||||
|
self.thing_types = OrderedDict()
|
||||||
|
self.certificates = OrderedDict()
|
||||||
|
self.policies = OrderedDict()
|
||||||
|
self.principal_policies = OrderedDict()
|
||||||
|
self.principal_things = OrderedDict()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
region_name = self.region_name
|
||||||
|
self.__dict__ = {}
|
||||||
|
self.__init__(region_name)
|
||||||
|
|
||||||
|
def create_thing(self, thing_name, thing_type_name, attribute_payload):
|
||||||
|
thing_types = self.list_thing_types()
|
||||||
|
thing_type = None
|
||||||
|
if thing_type_name:
|
||||||
|
filtered_thing_types = [_ for _ in thing_types if _.thing_type_name == thing_type_name]
|
||||||
|
if len(filtered_thing_types) == 0:
|
||||||
|
raise ResourceNotFoundException()
|
||||||
|
thing_type = filtered_thing_types[0]
|
||||||
|
if attribute_payload is None:
|
||||||
|
attributes = {}
|
||||||
|
elif 'attributes' not in attribute_payload:
|
||||||
|
attributes = {}
|
||||||
|
else:
|
||||||
|
attributes = attribute_payload['attributes']
|
||||||
|
thing = FakeThing(thing_name, thing_type, attributes, self.region_name)
|
||||||
|
self.things[thing.arn] = thing
|
||||||
|
return thing.thing_name, thing.arn
|
||||||
|
|
||||||
|
def create_thing_type(self, thing_type_name, thing_type_properties):
|
||||||
|
if thing_type_properties is None:
|
||||||
|
thing_type_properties = {}
|
||||||
|
thing_type = FakeThingType(thing_type_name, thing_type_properties, self.region_name)
|
||||||
|
self.thing_types[thing_type.arn] = thing_type
|
||||||
|
return thing_type.thing_type_name, thing_type.arn
|
||||||
|
|
||||||
|
def list_thing_types(self, thing_type_name=None):
|
||||||
|
if thing_type_name:
|
||||||
|
# It's wierd but thing_type_name is filterd by forward match, not complete match
|
||||||
|
return [_ for _ in self.thing_types.values() if _.thing_type_name.startswith(thing_type_name)]
|
||||||
|
thing_types = self.thing_types.values()
|
||||||
|
return thing_types
|
||||||
|
|
||||||
|
def list_things(self, attribute_name, attribute_value, thing_type_name):
|
||||||
|
# TODO: filter by attributess or thing_type
|
||||||
|
things = self.things.values()
|
||||||
|
return things
|
||||||
|
|
||||||
|
def describe_thing(self, thing_name):
|
||||||
|
things = [_ for _ in self.things.values() if _.thing_name == thing_name]
|
||||||
|
if len(things) == 0:
|
||||||
|
raise ResourceNotFoundException()
|
||||||
|
return things[0]
|
||||||
|
|
||||||
|
def describe_thing_type(self, thing_type_name):
|
||||||
|
thing_types = [_ for _ in self.thing_types.values() if _.thing_type_name == thing_type_name]
|
||||||
|
if len(thing_types) == 0:
|
||||||
|
raise ResourceNotFoundException()
|
||||||
|
return thing_types[0]
|
||||||
|
|
||||||
|
def delete_thing(self, thing_name, expected_version):
|
||||||
|
# TODO: handle expected_version
|
||||||
|
|
||||||
|
# can raise ResourceNotFoundError
|
||||||
|
thing = self.describe_thing(thing_name)
|
||||||
|
del self.things[thing.arn]
|
||||||
|
|
||||||
|
def delete_thing_type(self, thing_type_name):
|
||||||
|
# can raise ResourceNotFoundError
|
||||||
|
thing_type = self.describe_thing_type(thing_type_name)
|
||||||
|
del self.thing_types[thing_type.arn]
|
||||||
|
|
||||||
|
def update_thing(self, thing_name, thing_type_name, attribute_payload, expected_version, remove_thing_type):
|
||||||
|
# if attributes payload = {}, nothing
|
||||||
|
thing = self.describe_thing(thing_name)
|
||||||
|
thing_type = None
|
||||||
|
|
||||||
|
if remove_thing_type and thing_type_name:
|
||||||
|
raise InvalidRequestException()
|
||||||
|
|
||||||
|
# thing_type
|
||||||
|
if thing_type_name:
|
||||||
|
thing_types = self.list_thing_types()
|
||||||
|
filtered_thing_types = [_ for _ in thing_types if _.thing_type_name == thing_type_name]
|
||||||
|
if len(filtered_thing_types) == 0:
|
||||||
|
raise ResourceNotFoundException()
|
||||||
|
thing_type = filtered_thing_types[0]
|
||||||
|
thing.thing_type = thing_type
|
||||||
|
|
||||||
|
if remove_thing_type:
|
||||||
|
thing.thing_type = None
|
||||||
|
|
||||||
|
# attribute
|
||||||
|
if attribute_payload is not None and 'attributes' in attribute_payload:
|
||||||
|
do_merge = attribute_payload.get('merge', False)
|
||||||
|
attributes = attribute_payload['attributes']
|
||||||
|
if not do_merge:
|
||||||
|
thing.attributes = attributes
|
||||||
|
else:
|
||||||
|
thing.attributes.update(attributes)
|
||||||
|
|
||||||
|
def _random_string(self):
|
||||||
|
n = 20
|
||||||
|
random_str = ''.join([random.choice(string.ascii_letters + string.digits) for i in range(n)])
|
||||||
|
return random_str
|
||||||
|
|
||||||
|
def create_keys_and_certificate(self, set_as_active):
|
||||||
|
# implement here
|
||||||
|
# caCertificate can be blank
|
||||||
|
key_pair = {
|
||||||
|
'PublicKey': self._random_string(),
|
||||||
|
'PrivateKey': self._random_string()
|
||||||
|
}
|
||||||
|
certificate_pem = self._random_string()
|
||||||
|
status = 'ACTIVE' if set_as_active else 'INACTIVE'
|
||||||
|
certificate = FakeCertificate(certificate_pem, status, self.region_name)
|
||||||
|
self.certificates[certificate.certificate_id] = certificate
|
||||||
|
return certificate, key_pair
|
||||||
|
|
||||||
|
def delete_certificate(self, certificate_id):
|
||||||
|
self.describe_certificate(certificate_id)
|
||||||
|
del self.certificates[certificate_id]
|
||||||
|
|
||||||
|
def describe_certificate(self, certificate_id):
|
||||||
|
certs = [_ for _ in self.certificates.values() if _.certificate_id == certificate_id]
|
||||||
|
if len(certs) == 0:
|
||||||
|
raise ResourceNotFoundException()
|
||||||
|
return certs[0]
|
||||||
|
|
||||||
|
def list_certificates(self):
|
||||||
|
return self.certificates.values()
|
||||||
|
|
||||||
|
def update_certificate(self, certificate_id, new_status):
|
||||||
|
cert = self.describe_certificate(certificate_id)
|
||||||
|
# TODO: validate new_status
|
||||||
|
cert.status = new_status
|
||||||
|
|
||||||
|
def create_policy(self, policy_name, policy_document):
|
||||||
|
policy = FakePolicy(policy_name, policy_document, self.region_name)
|
||||||
|
self.policies[policy.name] = policy
|
||||||
|
return policy
|
||||||
|
|
||||||
|
def list_policies(self):
|
||||||
|
policies = self.policies.values()
|
||||||
|
return policies
|
||||||
|
|
||||||
|
def get_policy(self, policy_name):
|
||||||
|
policies = [_ for _ in self.policies.values() if _.name == policy_name]
|
||||||
|
if len(policies) == 0:
|
||||||
|
raise ResourceNotFoundException()
|
||||||
|
return policies[0]
|
||||||
|
|
||||||
|
def delete_policy(self, policy_name):
|
||||||
|
policy = self.get_policy(policy_name)
|
||||||
|
del self.policies[policy.name]
|
||||||
|
|
||||||
|
def _get_principal(self, principal_arn):
|
||||||
|
"""
|
||||||
|
raise ResourceNotFoundException
|
||||||
|
"""
|
||||||
|
if ':cert/' in principal_arn:
|
||||||
|
certs = [_ for _ in self.certificates.values() if _.arn == principal_arn]
|
||||||
|
if len(certs) == 0:
|
||||||
|
raise ResourceNotFoundException()
|
||||||
|
principal = certs[0]
|
||||||
|
return principal
|
||||||
|
else:
|
||||||
|
# TODO: search for cognito_ids
|
||||||
|
pass
|
||||||
|
raise ResourceNotFoundException()
|
||||||
|
|
||||||
|
def attach_principal_policy(self, policy_name, principal_arn):
|
||||||
|
principal = self._get_principal(principal_arn)
|
||||||
|
policy = self.get_policy(policy_name)
|
||||||
|
k = (principal_arn, policy_name)
|
||||||
|
if k in self.principal_policies:
|
||||||
|
return
|
||||||
|
self.principal_policies[k] = (principal, policy)
|
||||||
|
|
||||||
|
def detach_principal_policy(self, policy_name, principal_arn):
|
||||||
|
# this may raises ResourceNotFoundException
|
||||||
|
self._get_principal(principal_arn)
|
||||||
|
self.get_policy(policy_name)
|
||||||
|
|
||||||
|
k = (principal_arn, policy_name)
|
||||||
|
if k not in self.principal_policies:
|
||||||
|
raise ResourceNotFoundException()
|
||||||
|
del self.principal_policies[k]
|
||||||
|
|
||||||
|
def list_principal_policies(self, principal_arn):
|
||||||
|
policies = [v[1] for k, v in self.principal_policies.items() if k[0] == principal_arn]
|
||||||
|
return policies
|
||||||
|
|
||||||
|
def list_policy_principals(self, policy_name):
|
||||||
|
principals = [k[0] for k, v in self.principal_policies.items() if k[1] == policy_name]
|
||||||
|
return principals
|
||||||
|
|
||||||
|
def attach_thing_principal(self, thing_name, principal_arn):
|
||||||
|
principal = self._get_principal(principal_arn)
|
||||||
|
thing = self.describe_thing(thing_name)
|
||||||
|
k = (principal_arn, thing_name)
|
||||||
|
if k in self.principal_things:
|
||||||
|
return
|
||||||
|
self.principal_things[k] = (principal, thing)
|
||||||
|
|
||||||
|
def detach_thing_principal(self, thing_name, principal_arn):
|
||||||
|
# this may raises ResourceNotFoundException
|
||||||
|
self._get_principal(principal_arn)
|
||||||
|
self.describe_thing(thing_name)
|
||||||
|
|
||||||
|
k = (principal_arn, thing_name)
|
||||||
|
if k not in self.principal_things:
|
||||||
|
raise ResourceNotFoundException()
|
||||||
|
del self.principal_things[k]
|
||||||
|
|
||||||
|
def list_principal_things(self, principal_arn):
|
||||||
|
thing_names = [k[0] for k, v in self.principal_things.items() if k[0] == principal_arn]
|
||||||
|
return thing_names
|
||||||
|
|
||||||
|
def list_thing_principals(self, thing_name):
|
||||||
|
principals = [k[0] for k, v in self.principal_things.items() if k[1] == thing_name]
|
||||||
|
return principals
|
||||||
|
|
||||||
|
|
||||||
|
available_regions = boto3.session.Session().get_available_regions("iot")
|
||||||
|
iot_backends = {region: IoTBackend(region) for region in available_regions}
|
258
moto/iot/responses.py
Normal file
258
moto/iot/responses.py
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from moto.core.responses import BaseResponse
|
||||||
|
from .models import iot_backends
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class IoTResponse(BaseResponse):
|
||||||
|
SERVICE_NAME = 'iot'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def iot_backend(self):
|
||||||
|
return iot_backends[self.region]
|
||||||
|
|
||||||
|
def create_thing(self):
|
||||||
|
thing_name = self._get_param("thingName")
|
||||||
|
thing_type_name = self._get_param("thingTypeName")
|
||||||
|
attribute_payload = self._get_param("attributePayload")
|
||||||
|
thing_name, thing_arn = self.iot_backend.create_thing(
|
||||||
|
thing_name=thing_name,
|
||||||
|
thing_type_name=thing_type_name,
|
||||||
|
attribute_payload=attribute_payload,
|
||||||
|
)
|
||||||
|
return json.dumps(dict(thingName=thing_name, thingArn=thing_arn))
|
||||||
|
|
||||||
|
def create_thing_type(self):
|
||||||
|
thing_type_name = self._get_param("thingTypeName")
|
||||||
|
thing_type_properties = self._get_param("thingTypeProperties")
|
||||||
|
thing_type_name, thing_type_arn = self.iot_backend.create_thing_type(
|
||||||
|
thing_type_name=thing_type_name,
|
||||||
|
thing_type_properties=thing_type_properties,
|
||||||
|
)
|
||||||
|
return json.dumps(dict(thingTypeName=thing_type_name, thingTypeArn=thing_type_arn))
|
||||||
|
|
||||||
|
def list_thing_types(self):
|
||||||
|
# previous_next_token = self._get_param("nextToken")
|
||||||
|
# max_results = self._get_int_param("maxResults")
|
||||||
|
thing_type_name = self._get_param("thingTypeName")
|
||||||
|
thing_types = self.iot_backend.list_thing_types(
|
||||||
|
thing_type_name=thing_type_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: support next_token and max_results
|
||||||
|
next_token = None
|
||||||
|
return json.dumps(dict(thingTypes=[_.to_dict() for _ in thing_types], nextToken=next_token))
|
||||||
|
|
||||||
|
def list_things(self):
|
||||||
|
# previous_next_token = self._get_param("nextToken")
|
||||||
|
# max_results = self._get_int_param("maxResults")
|
||||||
|
attribute_name = self._get_param("attributeName")
|
||||||
|
attribute_value = self._get_param("attributeValue")
|
||||||
|
thing_type_name = self._get_param("thingTypeName")
|
||||||
|
things = self.iot_backend.list_things(
|
||||||
|
attribute_name=attribute_name,
|
||||||
|
attribute_value=attribute_value,
|
||||||
|
thing_type_name=thing_type_name,
|
||||||
|
)
|
||||||
|
# TODO: support next_token and max_results
|
||||||
|
next_token = None
|
||||||
|
return json.dumps(dict(things=[_.to_dict() for _ in things], nextToken=next_token))
|
||||||
|
|
||||||
|
def describe_thing(self):
|
||||||
|
thing_name = self._get_param("thingName")
|
||||||
|
thing = self.iot_backend.describe_thing(
|
||||||
|
thing_name=thing_name,
|
||||||
|
)
|
||||||
|
print(thing.to_dict(include_default_client_id=True))
|
||||||
|
return json.dumps(thing.to_dict(include_default_client_id=True))
|
||||||
|
|
||||||
|
def describe_thing_type(self):
|
||||||
|
thing_type_name = self._get_param("thingTypeName")
|
||||||
|
thing_type = self.iot_backend.describe_thing_type(
|
||||||
|
thing_type_name=thing_type_name,
|
||||||
|
)
|
||||||
|
return json.dumps(thing_type.to_dict())
|
||||||
|
|
||||||
|
def delete_thing(self):
|
||||||
|
thing_name = self._get_param("thingName")
|
||||||
|
expected_version = self._get_param("expectedVersion")
|
||||||
|
self.iot_backend.delete_thing(
|
||||||
|
thing_name=thing_name,
|
||||||
|
expected_version=expected_version,
|
||||||
|
)
|
||||||
|
return json.dumps(dict())
|
||||||
|
|
||||||
|
def delete_thing_type(self):
|
||||||
|
thing_type_name = self._get_param("thingTypeName")
|
||||||
|
self.iot_backend.delete_thing_type(
|
||||||
|
thing_type_name=thing_type_name,
|
||||||
|
)
|
||||||
|
return json.dumps(dict())
|
||||||
|
|
||||||
|
def update_thing(self):
|
||||||
|
thing_name = self._get_param("thingName")
|
||||||
|
thing_type_name = self._get_param("thingTypeName")
|
||||||
|
attribute_payload = self._get_param("attributePayload")
|
||||||
|
expected_version = self._get_param("expectedVersion")
|
||||||
|
remove_thing_type = self._get_param("removeThingType")
|
||||||
|
self.iot_backend.update_thing(
|
||||||
|
thing_name=thing_name,
|
||||||
|
thing_type_name=thing_type_name,
|
||||||
|
attribute_payload=attribute_payload,
|
||||||
|
expected_version=expected_version,
|
||||||
|
remove_thing_type=remove_thing_type,
|
||||||
|
)
|
||||||
|
return json.dumps(dict())
|
||||||
|
|
||||||
|
def create_keys_and_certificate(self):
|
||||||
|
set_as_active = self._get_param("setAsActive")
|
||||||
|
cert, key_pair = self.iot_backend.create_keys_and_certificate(
|
||||||
|
set_as_active=set_as_active,
|
||||||
|
)
|
||||||
|
return json.dumps(dict(
|
||||||
|
certificateArn=cert.arn,
|
||||||
|
certificateId=cert.certificate_id,
|
||||||
|
certificatePem=cert.certificate_pem,
|
||||||
|
keyPair=key_pair
|
||||||
|
))
|
||||||
|
|
||||||
|
def delete_certificate(self):
|
||||||
|
certificate_id = self._get_param("certificateId")
|
||||||
|
self.iot_backend.delete_certificate(
|
||||||
|
certificate_id=certificate_id,
|
||||||
|
)
|
||||||
|
return json.dumps(dict())
|
||||||
|
|
||||||
|
def describe_certificate(self):
|
||||||
|
certificate_id = self._get_param("certificateId")
|
||||||
|
certificate = self.iot_backend.describe_certificate(
|
||||||
|
certificate_id=certificate_id,
|
||||||
|
)
|
||||||
|
return json.dumps(dict(certificateDescription=certificate.to_description_dict()))
|
||||||
|
|
||||||
|
def list_certificates(self):
|
||||||
|
# page_size = self._get_int_param("pageSize")
|
||||||
|
# marker = self._get_param("marker")
|
||||||
|
# ascending_order = self._get_param("ascendingOrder")
|
||||||
|
certificates = self.iot_backend.list_certificates()
|
||||||
|
# TODO: handle pagination
|
||||||
|
return json.dumps(dict(certificates=[_.to_dict() for _ in certificates]))
|
||||||
|
|
||||||
|
def update_certificate(self):
|
||||||
|
certificate_id = self._get_param("certificateId")
|
||||||
|
new_status = self._get_param("newStatus")
|
||||||
|
self.iot_backend.update_certificate(
|
||||||
|
certificate_id=certificate_id,
|
||||||
|
new_status=new_status,
|
||||||
|
)
|
||||||
|
return json.dumps(dict())
|
||||||
|
|
||||||
|
def create_policy(self):
|
||||||
|
policy_name = self._get_param("policyName")
|
||||||
|
policy_document = self._get_param("policyDocument")
|
||||||
|
policy = self.iot_backend.create_policy(
|
||||||
|
policy_name=policy_name,
|
||||||
|
policy_document=policy_document,
|
||||||
|
)
|
||||||
|
return json.dumps(policy.to_dict_at_creation())
|
||||||
|
|
||||||
|
def list_policies(self):
|
||||||
|
# marker = self._get_param("marker")
|
||||||
|
# page_size = self._get_int_param("pageSize")
|
||||||
|
# ascending_order = self._get_param("ascendingOrder")
|
||||||
|
policies = self.iot_backend.list_policies()
|
||||||
|
|
||||||
|
# TODO: handle pagination
|
||||||
|
return json.dumps(dict(policies=[_.to_dict() for _ in policies]))
|
||||||
|
|
||||||
|
def get_policy(self):
|
||||||
|
policy_name = self._get_param("policyName")
|
||||||
|
policy = self.iot_backend.get_policy(
|
||||||
|
policy_name=policy_name,
|
||||||
|
)
|
||||||
|
return json.dumps(policy.to_get_dict())
|
||||||
|
|
||||||
|
def delete_policy(self):
|
||||||
|
policy_name = self._get_param("policyName")
|
||||||
|
self.iot_backend.delete_policy(
|
||||||
|
policy_name=policy_name,
|
||||||
|
)
|
||||||
|
return json.dumps(dict())
|
||||||
|
|
||||||
|
def attach_principal_policy(self):
|
||||||
|
policy_name = self._get_param("policyName")
|
||||||
|
principal = self.headers.get('x-amzn-iot-principal')
|
||||||
|
self.iot_backend.attach_principal_policy(
|
||||||
|
policy_name=policy_name,
|
||||||
|
principal_arn=principal,
|
||||||
|
)
|
||||||
|
return json.dumps(dict())
|
||||||
|
|
||||||
|
def detach_principal_policy(self):
|
||||||
|
policy_name = self._get_param("policyName")
|
||||||
|
principal = self.headers.get('x-amzn-iot-principal')
|
||||||
|
self.iot_backend.detach_principal_policy(
|
||||||
|
policy_name=policy_name,
|
||||||
|
principal_arn=principal,
|
||||||
|
)
|
||||||
|
return json.dumps(dict())
|
||||||
|
|
||||||
|
def list_principal_policies(self):
|
||||||
|
principal = self.headers.get('x-amzn-iot-principal')
|
||||||
|
# marker = self._get_param("marker")
|
||||||
|
# page_size = self._get_int_param("pageSize")
|
||||||
|
# ascending_order = self._get_param("ascendingOrder")
|
||||||
|
policies = self.iot_backend.list_principal_policies(
|
||||||
|
principal_arn=principal
|
||||||
|
)
|
||||||
|
# TODO: handle pagination
|
||||||
|
next_marker = None
|
||||||
|
return json.dumps(dict(policies=[_.to_dict() for _ in policies], nextMarker=next_marker))
|
||||||
|
|
||||||
|
def list_policy_principals(self):
|
||||||
|
policy_name = self.headers.get('x-amzn-iot-policy')
|
||||||
|
# marker = self._get_param("marker")
|
||||||
|
# page_size = self._get_int_param("pageSize")
|
||||||
|
# ascending_order = self._get_param("ascendingOrder")
|
||||||
|
principals = self.iot_backend.list_policy_principals(
|
||||||
|
policy_name=policy_name,
|
||||||
|
)
|
||||||
|
# TODO: handle pagination
|
||||||
|
next_marker = None
|
||||||
|
return json.dumps(dict(principals=principals, nextMarker=next_marker))
|
||||||
|
|
||||||
|
def attach_thing_principal(self):
|
||||||
|
thing_name = self._get_param("thingName")
|
||||||
|
principal = self.headers.get('x-amzn-principal')
|
||||||
|
self.iot_backend.attach_thing_principal(
|
||||||
|
thing_name=thing_name,
|
||||||
|
principal_arn=principal,
|
||||||
|
)
|
||||||
|
return json.dumps(dict())
|
||||||
|
|
||||||
|
def detach_thing_principal(self):
|
||||||
|
thing_name = self._get_param("thingName")
|
||||||
|
principal = self.headers.get('x-amzn-principal')
|
||||||
|
self.iot_backend.detach_thing_principal(
|
||||||
|
thing_name=thing_name,
|
||||||
|
principal_arn=principal,
|
||||||
|
)
|
||||||
|
return json.dumps(dict())
|
||||||
|
|
||||||
|
def list_principal_things(self):
|
||||||
|
next_token = self._get_param("nextToken")
|
||||||
|
# max_results = self._get_int_param("maxResults")
|
||||||
|
principal = self.headers.get('x-amzn-principal')
|
||||||
|
things = self.iot_backend.list_principal_things(
|
||||||
|
principal_arn=principal,
|
||||||
|
)
|
||||||
|
# TODO: handle pagination
|
||||||
|
next_token = None
|
||||||
|
return json.dumps(dict(things=things, nextToken=next_token))
|
||||||
|
|
||||||
|
def list_thing_principals(self):
|
||||||
|
thing_name = self._get_param("thingName")
|
||||||
|
principals = self.iot_backend.list_thing_principals(
|
||||||
|
thing_name=thing_name,
|
||||||
|
)
|
||||||
|
return json.dumps(dict(principals=principals))
|
14
moto/iot/urls.py
Normal file
14
moto/iot/urls.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from .responses import IoTResponse
|
||||||
|
|
||||||
|
url_bases = [
|
||||||
|
"https?://iot.(.+).amazonaws.com",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
response = IoTResponse()
|
||||||
|
|
||||||
|
|
||||||
|
url_paths = {
|
||||||
|
'{0}/.*$': response.dispatch,
|
||||||
|
}
|
6
moto/iotdata/__init__.py
Normal file
6
moto/iotdata/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from .models import iotdata_backends
|
||||||
|
from ..core.models import base_decorator
|
||||||
|
|
||||||
|
iotdata_backend = iotdata_backends['us-east-1']
|
||||||
|
mock_iotdata = base_decorator(iotdata_backends)
|
23
moto/iotdata/exceptions.py
Normal file
23
moto/iotdata/exceptions.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from moto.core.exceptions import RESTError
|
||||||
|
|
||||||
|
|
||||||
|
class IoTDataPlaneClientError(RESTError):
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceNotFoundException(IoTDataPlaneClientError):
|
||||||
|
def __init__(self):
|
||||||
|
self.code = 400
|
||||||
|
super(ResourceNotFoundException, self).__init__(
|
||||||
|
"ResourceNotFoundException",
|
||||||
|
"The specified resource does not exist"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidRequestException(IoTDataPlaneClientError):
|
||||||
|
def __init__(self, message):
|
||||||
|
self.code = 400
|
||||||
|
super(InvalidRequestException, self).__init__(
|
||||||
|
"InvalidRequestException", message
|
||||||
|
)
|
189
moto/iotdata/models.py
Normal file
189
moto/iotdata/models.py
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import boto3
|
||||||
|
import jsondiff
|
||||||
|
from moto.core import BaseBackend, BaseModel
|
||||||
|
from moto.iot import iot_backends
|
||||||
|
from .exceptions import (
|
||||||
|
ResourceNotFoundException,
|
||||||
|
InvalidRequestException
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeShadow(BaseModel):
|
||||||
|
"""See the specification:
|
||||||
|
http://docs.aws.amazon.com/iot/latest/developerguide/thing-shadow-document-syntax.html
|
||||||
|
"""
|
||||||
|
def __init__(self, desired, reported, requested_payload, version, deleted=False):
|
||||||
|
self.desired = desired
|
||||||
|
self.reported = reported
|
||||||
|
self.requested_payload = requested_payload
|
||||||
|
self.version = version
|
||||||
|
self.timestamp = int(time.time())
|
||||||
|
self.deleted = deleted
|
||||||
|
|
||||||
|
self.metadata_desired = self._create_metadata_from_state(self.desired, self.timestamp)
|
||||||
|
self.metadata_reported = self._create_metadata_from_state(self.reported, self.timestamp)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_from_previous_version(cls, previous_shadow, payload):
|
||||||
|
"""
|
||||||
|
set None to payload when you want to delete shadow
|
||||||
|
"""
|
||||||
|
version, previous_payload = (previous_shadow.version + 1, previous_shadow.to_dict(include_delta=False)) if previous_shadow else (1, {})
|
||||||
|
|
||||||
|
if payload is None:
|
||||||
|
# if given payload is None, delete existing payload
|
||||||
|
# this means the request was delete_thing_shadow
|
||||||
|
shadow = FakeShadow(None, None, None, version, deleted=True)
|
||||||
|
return shadow
|
||||||
|
|
||||||
|
# we can make sure that payload has 'state' key
|
||||||
|
desired = payload['state'].get(
|
||||||
|
'desired',
|
||||||
|
previous_payload.get('state', {}).get('desired', None)
|
||||||
|
)
|
||||||
|
reported = payload['state'].get(
|
||||||
|
'reported',
|
||||||
|
previous_payload.get('state', {}).get('reported', None)
|
||||||
|
)
|
||||||
|
shadow = FakeShadow(desired, reported, payload, version)
|
||||||
|
return shadow
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_payload(cls, desired, reported):
|
||||||
|
if desired is None:
|
||||||
|
delta = reported
|
||||||
|
elif reported is None:
|
||||||
|
delta = desired
|
||||||
|
else:
|
||||||
|
delta = jsondiff.diff(desired, reported)
|
||||||
|
return delta
|
||||||
|
|
||||||
|
def _create_metadata_from_state(self, state, ts):
|
||||||
|
"""
|
||||||
|
state must be disired or reported stype dict object
|
||||||
|
replces primitive type with {"timestamp": ts} in dict
|
||||||
|
"""
|
||||||
|
if state is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _f(elem, ts):
|
||||||
|
if isinstance(elem, dict):
|
||||||
|
return {_: _f(elem[_], ts) for _ in elem.keys()}
|
||||||
|
if isinstance(elem, list):
|
||||||
|
return [_f(_, ts) for _ in elem]
|
||||||
|
return {"timestamp": ts}
|
||||||
|
return _f(state, ts)
|
||||||
|
|
||||||
|
def to_response_dict(self):
|
||||||
|
desired = self.requested_payload['state'].get('desired', None)
|
||||||
|
reported = self.requested_payload['state'].get('reported', None)
|
||||||
|
|
||||||
|
payload = {}
|
||||||
|
if desired is not None:
|
||||||
|
payload['desired'] = desired
|
||||||
|
if reported is not None:
|
||||||
|
payload['reported'] = reported
|
||||||
|
|
||||||
|
metadata = {}
|
||||||
|
if desired is not None:
|
||||||
|
metadata['desired'] = self._create_metadata_from_state(desired, self.timestamp)
|
||||||
|
if reported is not None:
|
||||||
|
metadata['reported'] = self._create_metadata_from_state(reported, self.timestamp)
|
||||||
|
return {
|
||||||
|
'state': payload,
|
||||||
|
'metadata': metadata,
|
||||||
|
'timestamp': self.timestamp,
|
||||||
|
'version': self.version
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_dict(self, include_delta=True):
|
||||||
|
"""returning nothing except for just top-level keys for now.
|
||||||
|
"""
|
||||||
|
if self.deleted:
|
||||||
|
return {
|
||||||
|
'timestamp': self.timestamp,
|
||||||
|
'version': self.version
|
||||||
|
}
|
||||||
|
delta = self.parse_payload(self.desired, self.reported)
|
||||||
|
payload = {}
|
||||||
|
if self.desired is not None:
|
||||||
|
payload['desired'] = self.desired
|
||||||
|
if self.reported is not None:
|
||||||
|
payload['reported'] = self.reported
|
||||||
|
if include_delta and (delta is not None and len(delta.keys()) != 0):
|
||||||
|
payload['delta'] = delta
|
||||||
|
|
||||||
|
metadata = {}
|
||||||
|
if self.metadata_desired is not None:
|
||||||
|
metadata['desired'] = self.metadata_desired
|
||||||
|
if self.metadata_reported is not None:
|
||||||
|
metadata['reported'] = self.metadata_reported
|
||||||
|
|
||||||
|
return {
|
||||||
|
'state': payload,
|
||||||
|
'metadata': metadata,
|
||||||
|
'timestamp': self.timestamp,
|
||||||
|
'version': self.version
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class IoTDataPlaneBackend(BaseBackend):
|
||||||
|
def __init__(self, region_name=None):
|
||||||
|
super(IoTDataPlaneBackend, self).__init__()
|
||||||
|
self.region_name = region_name
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
region_name = self.region_name
|
||||||
|
self.__dict__ = {}
|
||||||
|
self.__init__(region_name)
|
||||||
|
|
||||||
|
def update_thing_shadow(self, thing_name, payload):
|
||||||
|
"""
|
||||||
|
spec of payload:
|
||||||
|
- need node `state`
|
||||||
|
- state node must be an Object
|
||||||
|
- State contains an invalid node: 'foo'
|
||||||
|
"""
|
||||||
|
thing = iot_backends[self.region_name].describe_thing(thing_name)
|
||||||
|
|
||||||
|
# validate
|
||||||
|
try:
|
||||||
|
payload = json.loads(payload)
|
||||||
|
except ValueError:
|
||||||
|
raise InvalidRequestException('invalid json')
|
||||||
|
if 'state' not in payload:
|
||||||
|
raise InvalidRequestException('need node `state`')
|
||||||
|
if not isinstance(payload['state'], dict):
|
||||||
|
raise InvalidRequestException('state node must be an Object')
|
||||||
|
if any(_ for _ in payload['state'].keys() if _ not in ['desired', 'reported']):
|
||||||
|
raise InvalidRequestException('State contains an invalid node')
|
||||||
|
|
||||||
|
new_shadow = FakeShadow.create_from_previous_version(thing.thing_shadow, payload)
|
||||||
|
thing.thing_shadow = new_shadow
|
||||||
|
return thing.thing_shadow
|
||||||
|
|
||||||
|
def get_thing_shadow(self, thing_name):
|
||||||
|
thing = iot_backends[self.region_name].describe_thing(thing_name)
|
||||||
|
|
||||||
|
if thing.thing_shadow is None or thing.thing_shadow.deleted:
|
||||||
|
raise ResourceNotFoundException()
|
||||||
|
return thing.thing_shadow
|
||||||
|
|
||||||
|
def delete_thing_shadow(self, thing_name):
|
||||||
|
"""after deleting, get_thing_shadow will raise ResourceNotFound.
|
||||||
|
But version of the shadow keep increasing...
|
||||||
|
"""
|
||||||
|
thing = iot_backends[self.region_name].describe_thing(thing_name)
|
||||||
|
if thing.thing_shadow is None:
|
||||||
|
raise ResourceNotFoundException()
|
||||||
|
payload = None
|
||||||
|
new_shadow = FakeShadow.create_from_previous_version(thing.thing_shadow, payload)
|
||||||
|
thing.thing_shadow = new_shadow
|
||||||
|
return thing.thing_shadow
|
||||||
|
|
||||||
|
|
||||||
|
available_regions = boto3.session.Session().get_available_regions("iot-data")
|
||||||
|
iotdata_backends = {region: IoTDataPlaneBackend(region) for region in available_regions}
|
35
moto/iotdata/responses.py
Normal file
35
moto/iotdata/responses.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from moto.core.responses import BaseResponse
|
||||||
|
from .models import iotdata_backends
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class IoTDataPlaneResponse(BaseResponse):
|
||||||
|
SERVICE_NAME = 'iot-data'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def iotdata_backend(self):
|
||||||
|
return iotdata_backends[self.region]
|
||||||
|
|
||||||
|
def update_thing_shadow(self):
|
||||||
|
thing_name = self._get_param("thingName")
|
||||||
|
payload = self.body
|
||||||
|
payload = self.iotdata_backend.update_thing_shadow(
|
||||||
|
thing_name=thing_name,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
return json.dumps(payload.to_response_dict())
|
||||||
|
|
||||||
|
def get_thing_shadow(self):
|
||||||
|
thing_name = self._get_param("thingName")
|
||||||
|
payload = self.iotdata_backend.get_thing_shadow(
|
||||||
|
thing_name=thing_name,
|
||||||
|
)
|
||||||
|
return json.dumps(payload.to_dict())
|
||||||
|
|
||||||
|
def delete_thing_shadow(self):
|
||||||
|
thing_name = self._get_param("thingName")
|
||||||
|
payload = self.iotdata_backend.delete_thing_shadow(
|
||||||
|
thing_name=thing_name,
|
||||||
|
)
|
||||||
|
return json.dumps(payload.to_dict())
|
14
moto/iotdata/urls.py
Normal file
14
moto/iotdata/urls.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from .responses import IoTDataPlaneResponse
|
||||||
|
|
||||||
|
url_bases = [
|
||||||
|
"https?://data.iot.(.+).amazonaws.com",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
response = IoTDataPlaneResponse()
|
||||||
|
|
||||||
|
|
||||||
|
url_paths = {
|
||||||
|
'{0}/.*$': response.dispatch,
|
||||||
|
}
|
3
setup.py
3
setup.py
@ -21,7 +21,8 @@ install_requires = [
|
|||||||
"python-dateutil<3.0.0,>=2.1",
|
"python-dateutil<3.0.0,>=2.1",
|
||||||
"mock",
|
"mock",
|
||||||
"docker>=2.5.1",
|
"docker>=2.5.1",
|
||||||
"aws-xray-sdk>=0.93"
|
"jsondiff==1.1.1",
|
||||||
|
"aws-xray-sdk>=0.93",
|
||||||
]
|
]
|
||||||
|
|
||||||
extras_require = {
|
extras_require = {
|
||||||
|
179
tests/test_iot/test_iot.py
Normal file
179
tests/test_iot/test_iot.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
import sure # noqa
|
||||||
|
from moto import mock_iot
|
||||||
|
|
||||||
|
|
||||||
|
@mock_iot
|
||||||
|
def test_things():
|
||||||
|
client = boto3.client('iot', region_name='ap-northeast-1')
|
||||||
|
name = 'my-thing'
|
||||||
|
type_name = 'my-type-name'
|
||||||
|
|
||||||
|
# thing type
|
||||||
|
thing_type = client.create_thing_type(thingTypeName=type_name)
|
||||||
|
thing_type.should.have.key('thingTypeName').which.should.equal(type_name)
|
||||||
|
thing_type.should.have.key('thingTypeArn')
|
||||||
|
|
||||||
|
res = client.list_thing_types()
|
||||||
|
res.should.have.key('thingTypes').which.should.have.length_of(1)
|
||||||
|
for thing_type in res['thingTypes']:
|
||||||
|
thing_type.should.have.key('thingTypeName').which.should_not.be.none
|
||||||
|
|
||||||
|
thing_type = client.describe_thing_type(thingTypeName=type_name)
|
||||||
|
thing_type.should.have.key('thingTypeName').which.should.equal(type_name)
|
||||||
|
thing_type.should.have.key('thingTypeProperties')
|
||||||
|
thing_type.should.have.key('thingTypeMetadata')
|
||||||
|
|
||||||
|
# thing
|
||||||
|
thing = client.create_thing(thingName=name, thingTypeName=type_name)
|
||||||
|
thing.should.have.key('thingName').which.should.equal(name)
|
||||||
|
thing.should.have.key('thingArn')
|
||||||
|
res = client.list_things()
|
||||||
|
res.should.have.key('things').which.should.have.length_of(1)
|
||||||
|
for thing in res['things']:
|
||||||
|
thing.should.have.key('thingName').which.should_not.be.none
|
||||||
|
|
||||||
|
thing = client.update_thing(thingName=name, attributePayload={'attributes': {'k1': 'v1'}})
|
||||||
|
res = client.list_things()
|
||||||
|
res.should.have.key('things').which.should.have.length_of(1)
|
||||||
|
for thing in res['things']:
|
||||||
|
thing.should.have.key('thingName').which.should_not.be.none
|
||||||
|
res['things'][0]['attributes'].should.have.key('k1').which.should.equal('v1')
|
||||||
|
|
||||||
|
thing = client.describe_thing(thingName=name)
|
||||||
|
thing.should.have.key('thingName').which.should.equal(name)
|
||||||
|
thing.should.have.key('defaultClientId')
|
||||||
|
thing.should.have.key('thingTypeName')
|
||||||
|
thing.should.have.key('attributes')
|
||||||
|
thing.should.have.key('version')
|
||||||
|
|
||||||
|
# delete thing
|
||||||
|
client.delete_thing(thingName=name)
|
||||||
|
res = client.list_things()
|
||||||
|
res.should.have.key('things').which.should.have.length_of(0)
|
||||||
|
|
||||||
|
# delete thing type
|
||||||
|
client.delete_thing_type(thingTypeName=type_name)
|
||||||
|
res = client.list_thing_types()
|
||||||
|
res.should.have.key('thingTypes').which.should.have.length_of(0)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_iot
|
||||||
|
def test_certs():
|
||||||
|
client = boto3.client('iot', region_name='ap-northeast-1')
|
||||||
|
cert = client.create_keys_and_certificate(setAsActive=True)
|
||||||
|
cert.should.have.key('certificateArn').which.should_not.be.none
|
||||||
|
cert.should.have.key('certificateId').which.should_not.be.none
|
||||||
|
cert.should.have.key('certificatePem').which.should_not.be.none
|
||||||
|
cert.should.have.key('keyPair')
|
||||||
|
cert['keyPair'].should.have.key('PublicKey').which.should_not.be.none
|
||||||
|
cert['keyPair'].should.have.key('PrivateKey').which.should_not.be.none
|
||||||
|
cert_id = cert['certificateId']
|
||||||
|
|
||||||
|
cert = client.describe_certificate(certificateId=cert_id)
|
||||||
|
cert.should.have.key('certificateDescription')
|
||||||
|
cert_desc = cert['certificateDescription']
|
||||||
|
cert_desc.should.have.key('certificateArn').which.should_not.be.none
|
||||||
|
cert_desc.should.have.key('certificateId').which.should_not.be.none
|
||||||
|
cert_desc.should.have.key('certificatePem').which.should_not.be.none
|
||||||
|
cert_desc.should.have.key('status').which.should.equal('ACTIVE')
|
||||||
|
|
||||||
|
res = client.list_certificates()
|
||||||
|
res.should.have.key('certificates').which.should.have.length_of(1)
|
||||||
|
for cert in res['certificates']:
|
||||||
|
cert.should.have.key('certificateArn').which.should_not.be.none
|
||||||
|
cert.should.have.key('certificateId').which.should_not.be.none
|
||||||
|
cert.should.have.key('status').which.should_not.be.none
|
||||||
|
cert.should.have.key('creationDate').which.should_not.be.none
|
||||||
|
|
||||||
|
client.update_certificate(certificateId=cert_id, newStatus='REVOKED')
|
||||||
|
cert = client.describe_certificate(certificateId=cert_id)
|
||||||
|
cert_desc.should.have.key('status').which.should.equal('ACTIVE')
|
||||||
|
|
||||||
|
client.delete_certificate(certificateId=cert_id)
|
||||||
|
res = client.list_certificates()
|
||||||
|
res.should.have.key('certificates').which.should.have.length_of(0)
|
||||||
|
|
||||||
|
@mock_iot
|
||||||
|
def test_policy():
|
||||||
|
client = boto3.client('iot', region_name='ap-northeast-1')
|
||||||
|
name = 'my-policy'
|
||||||
|
doc = '{}'
|
||||||
|
policy = client.create_policy(policyName=name, policyDocument=doc)
|
||||||
|
policy.should.have.key('policyName').which.should.equal(name)
|
||||||
|
policy.should.have.key('policyArn').which.should_not.be.none
|
||||||
|
policy.should.have.key('policyDocument').which.should.equal(doc)
|
||||||
|
policy.should.have.key('policyVersionId').which.should.equal('1')
|
||||||
|
|
||||||
|
policy = client.get_policy(policyName=name)
|
||||||
|
policy.should.have.key('policyName').which.should.equal(name)
|
||||||
|
policy.should.have.key('policyArn').which.should_not.be.none
|
||||||
|
policy.should.have.key('policyDocument').which.should.equal(doc)
|
||||||
|
policy.should.have.key('defaultVersionId').which.should.equal('1')
|
||||||
|
|
||||||
|
res = client.list_policies()
|
||||||
|
res.should.have.key('policies').which.should.have.length_of(1)
|
||||||
|
for policy in res['policies']:
|
||||||
|
policy.should.have.key('policyName').which.should_not.be.none
|
||||||
|
policy.should.have.key('policyArn').which.should_not.be.none
|
||||||
|
|
||||||
|
client.delete_policy(policyName=name)
|
||||||
|
res = client.list_policies()
|
||||||
|
res.should.have.key('policies').which.should.have.length_of(0)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_iot
|
||||||
|
def test_principal_policy():
|
||||||
|
client = boto3.client('iot', region_name='ap-northeast-1')
|
||||||
|
policy_name = 'my-policy'
|
||||||
|
doc = '{}'
|
||||||
|
policy = client.create_policy(policyName=policy_name, policyDocument=doc)
|
||||||
|
cert = client.create_keys_and_certificate(setAsActive=True)
|
||||||
|
cert_arn = cert['certificateArn']
|
||||||
|
|
||||||
|
client.attach_principal_policy(policyName=policy_name, principal=cert_arn)
|
||||||
|
|
||||||
|
res = client.list_principal_policies(principal=cert_arn)
|
||||||
|
res.should.have.key('policies').which.should.have.length_of(1)
|
||||||
|
for policy in res['policies']:
|
||||||
|
policy.should.have.key('policyName').which.should_not.be.none
|
||||||
|
policy.should.have.key('policyArn').which.should_not.be.none
|
||||||
|
|
||||||
|
res = client.list_policy_principals(policyName=policy_name)
|
||||||
|
res.should.have.key('principals').which.should.have.length_of(1)
|
||||||
|
for principal in res['principals']:
|
||||||
|
principal.should_not.be.none
|
||||||
|
|
||||||
|
client.detach_principal_policy(policyName=policy_name, principal=cert_arn)
|
||||||
|
res = client.list_principal_policies(principal=cert_arn)
|
||||||
|
res.should.have.key('policies').which.should.have.length_of(0)
|
||||||
|
res = client.list_policy_principals(policyName=policy_name)
|
||||||
|
res.should.have.key('principals').which.should.have.length_of(0)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_iot
|
||||||
|
def test_principal_thing():
|
||||||
|
client = boto3.client('iot', region_name='ap-northeast-1')
|
||||||
|
thing_name = 'my-thing'
|
||||||
|
thing = client.create_thing(thingName=thing_name)
|
||||||
|
cert = client.create_keys_and_certificate(setAsActive=True)
|
||||||
|
cert_arn = cert['certificateArn']
|
||||||
|
|
||||||
|
client.attach_thing_principal(thingName=thing_name, principal=cert_arn)
|
||||||
|
|
||||||
|
res = client.list_principal_things(principal=cert_arn)
|
||||||
|
res.should.have.key('things').which.should.have.length_of(1)
|
||||||
|
for thing in res['things']:
|
||||||
|
thing.should_not.be.none
|
||||||
|
res = client.list_thing_principals(thingName=thing_name)
|
||||||
|
res.should.have.key('principals').which.should.have.length_of(1)
|
||||||
|
for principal in res['principals']:
|
||||||
|
principal.should_not.be.none
|
||||||
|
|
||||||
|
client.detach_thing_principal(thingName=thing_name, principal=cert_arn)
|
||||||
|
res = client.list_principal_things(principal=cert_arn)
|
||||||
|
res.should.have.key('things').which.should.have.length_of(0)
|
||||||
|
res = client.list_thing_principals(thingName=thing_name)
|
||||||
|
res.should.have.key('principals').which.should.have.length_of(0)
|
19
tests/test_iot/test_server.py
Normal file
19
tests/test_iot/test_server.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import sure # noqa
|
||||||
|
|
||||||
|
import moto.server as server
|
||||||
|
from moto import mock_iot
|
||||||
|
|
||||||
|
'''
|
||||||
|
Test the different server responses
|
||||||
|
'''
|
||||||
|
|
||||||
|
@mock_iot
|
||||||
|
def test_iot_list():
|
||||||
|
backend = server.create_backend_app("iot")
|
||||||
|
test_client = backend.test_client()
|
||||||
|
|
||||||
|
# just making sure that server is up
|
||||||
|
res = test_client.get('/things')
|
||||||
|
res.status_code.should.equal(404)
|
87
tests/test_iotdata/test_iotdata.py
Normal file
87
tests/test_iotdata/test_iotdata.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
import boto3
|
||||||
|
import sure # noqa
|
||||||
|
from nose.tools import assert_raises
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
|
from moto import mock_iotdata, mock_iot
|
||||||
|
|
||||||
|
|
||||||
|
@mock_iot
|
||||||
|
@mock_iotdata
|
||||||
|
def test_basic():
|
||||||
|
iot_client = boto3.client('iot', region_name='ap-northeast-1')
|
||||||
|
client = boto3.client('iot-data', region_name='ap-northeast-1')
|
||||||
|
name = 'my-thing'
|
||||||
|
raw_payload = b'{"state": {"desired": {"led": "on"}}}'
|
||||||
|
iot_client.create_thing(thingName=name)
|
||||||
|
|
||||||
|
with assert_raises(ClientError):
|
||||||
|
client.get_thing_shadow(thingName=name)
|
||||||
|
|
||||||
|
res = client.update_thing_shadow(thingName=name, payload=raw_payload)
|
||||||
|
|
||||||
|
payload = json.loads(res['payload'].read())
|
||||||
|
expected_state = '{"desired": {"led": "on"}}'
|
||||||
|
payload.should.have.key('state').which.should.equal(json.loads(expected_state))
|
||||||
|
payload.should.have.key('metadata').which.should.have.key('desired').which.should.have.key('led')
|
||||||
|
payload.should.have.key('version').which.should.equal(1)
|
||||||
|
payload.should.have.key('timestamp')
|
||||||
|
|
||||||
|
res = client.get_thing_shadow(thingName=name)
|
||||||
|
payload = json.loads(res['payload'].read())
|
||||||
|
expected_state = b'{"desired": {"led": "on"}, "delta": {"led": "on"}}'
|
||||||
|
payload.should.have.key('state').which.should.equal(json.loads(expected_state))
|
||||||
|
payload.should.have.key('metadata').which.should.have.key('desired').which.should.have.key('led')
|
||||||
|
payload.should.have.key('version').which.should.equal(1)
|
||||||
|
payload.should.have.key('timestamp')
|
||||||
|
|
||||||
|
client.delete_thing_shadow(thingName=name)
|
||||||
|
with assert_raises(ClientError):
|
||||||
|
client.get_thing_shadow(thingName=name)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_iot
|
||||||
|
@mock_iotdata
|
||||||
|
def test_update():
|
||||||
|
iot_client = boto3.client('iot', region_name='ap-northeast-1')
|
||||||
|
client = boto3.client('iot-data', region_name='ap-northeast-1')
|
||||||
|
name = 'my-thing'
|
||||||
|
raw_payload = b'{"state": {"desired": {"led": "on"}}}'
|
||||||
|
iot_client.create_thing(thingName=name)
|
||||||
|
|
||||||
|
# first update
|
||||||
|
res = client.update_thing_shadow(thingName=name, payload=raw_payload)
|
||||||
|
payload = json.loads(res['payload'].read())
|
||||||
|
expected_state = '{"desired": {"led": "on"}}'
|
||||||
|
payload.should.have.key('state').which.should.equal(json.loads(expected_state))
|
||||||
|
payload.should.have.key('metadata').which.should.have.key('desired').which.should.have.key('led')
|
||||||
|
payload.should.have.key('version').which.should.equal(1)
|
||||||
|
payload.should.have.key('timestamp')
|
||||||
|
|
||||||
|
res = client.get_thing_shadow(thingName=name)
|
||||||
|
payload = json.loads(res['payload'].read())
|
||||||
|
expected_state = b'{"desired": {"led": "on"}, "delta": {"led": "on"}}'
|
||||||
|
payload.should.have.key('state').which.should.equal(json.loads(expected_state))
|
||||||
|
payload.should.have.key('metadata').which.should.have.key('desired').which.should.have.key('led')
|
||||||
|
payload.should.have.key('version').which.should.equal(1)
|
||||||
|
payload.should.have.key('timestamp')
|
||||||
|
|
||||||
|
# reporting new state
|
||||||
|
new_payload = b'{"state": {"reported": {"led": "on"}}}'
|
||||||
|
res = client.update_thing_shadow(thingName=name, payload=new_payload)
|
||||||
|
payload = json.loads(res['payload'].read())
|
||||||
|
expected_state = '{"reported": {"led": "on"}}'
|
||||||
|
payload.should.have.key('state').which.should.equal(json.loads(expected_state))
|
||||||
|
payload.should.have.key('metadata').which.should.have.key('reported').which.should.have.key('led')
|
||||||
|
payload.should.have.key('version').which.should.equal(2)
|
||||||
|
payload.should.have.key('timestamp')
|
||||||
|
|
||||||
|
res = client.get_thing_shadow(thingName=name)
|
||||||
|
payload = json.loads(res['payload'].read())
|
||||||
|
expected_state = b'{"desired": {"led": "on"}, "reported": {"led": "on"}}'
|
||||||
|
payload.should.have.key('state').which.should.equal(json.loads(expected_state))
|
||||||
|
payload.should.have.key('metadata').which.should.have.key('desired').which.should.have.key('led')
|
||||||
|
payload.should.have.key('version').which.should.equal(2)
|
||||||
|
payload.should.have.key('timestamp')
|
20
tests/test_iotdata/test_server.py
Normal file
20
tests/test_iotdata/test_server.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import sure # noqa
|
||||||
|
|
||||||
|
import moto.server as server
|
||||||
|
from moto import mock_iotdata
|
||||||
|
|
||||||
|
'''
|
||||||
|
Test the different server responses
|
||||||
|
'''
|
||||||
|
|
||||||
|
@mock_iotdata
|
||||||
|
def test_iotdata_list():
|
||||||
|
backend = server.create_backend_app("iot-data")
|
||||||
|
test_client = backend.test_client()
|
||||||
|
|
||||||
|
# just making sure that server is up
|
||||||
|
thing_name = 'nothing'
|
||||||
|
res = test_client.get('/things/{}/shadow'.format(thing_name))
|
||||||
|
res.status_code.should.equal(404)
|
Loading…
Reference in New Issue
Block a user