IoT: Added Rules implementation (#3552)

* feat(iot): Added IoT Rules implementation

* fix(iot): Split IoT Rules tests into multiple test cases

* fix(iot): Updated rule creation to work with python 2

Co-authored-by: Szymon Zmilczak <szymon.zmilczak@machiq.com>
This commit is contained in:
Szymon Zmilczak 2021-01-14 15:52:23 +01:00 committed by GitHub
parent d1696a37e5
commit c8151e1bb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 307 additions and 7 deletions

View File

@ -4406,7 +4406,7 @@
- [X] create_thing - [X] create_thing
- [X] create_thing_group - [X] create_thing_group
- [X] create_thing_type - [X] create_thing_type
- [ ] create_topic_rule - [X] create_topic_rule
- [ ] create_topic_rule_destination - [ ] create_topic_rule_destination
- [ ] delete_account_audit_configuration - [ ] delete_account_audit_configuration
- [ ] delete_authorizer - [ ] delete_authorizer
@ -4432,7 +4432,7 @@
- [X] delete_thing - [X] delete_thing
- [X] delete_thing_group - [X] delete_thing_group
- [X] delete_thing_type - [X] delete_thing_type
- [ ] delete_topic_rule - [X] delete_topic_rule
- [ ] delete_topic_rule_destination - [ ] delete_topic_rule_destination
- [ ] delete_v2_logging_level - [ ] delete_v2_logging_level
- [ ] deprecate_thing_type - [ ] deprecate_thing_type
@ -4467,8 +4467,8 @@
- [X] detach_principal_policy - [X] detach_principal_policy
- [ ] detach_security_profile - [ ] detach_security_profile
- [X] detach_thing_principal - [X] detach_thing_principal
- [ ] disable_topic_rule - [X] disable_topic_rule
- [ ] enable_topic_rule - [X] enable_topic_rule
- [ ] get_cardinality - [ ] get_cardinality
- [ ] get_effective_policies - [ ] get_effective_policies
- [ ] get_indexing_configuration - [ ] get_indexing_configuration
@ -4480,7 +4480,7 @@
- [X] get_policy_version - [X] get_policy_version
- [ ] get_registration_code - [ ] get_registration_code
- [ ] get_statistics - [ ] get_statistics
- [ ] get_topic_rule - [X] get_topic_rule
- [ ] get_topic_rule_destination - [ ] get_topic_rule_destination
- [ ] get_v2_logging_options - [ ] get_v2_logging_options
- [ ] list_active_violations - [ ] list_active_violations
@ -4528,7 +4528,7 @@
- [ ] list_things_in_billing_group - [ ] list_things_in_billing_group
- [X] list_things_in_thing_group - [X] list_things_in_thing_group
- [ ] list_topic_rule_destinations - [ ] list_topic_rule_destinations
- [ ] list_topic_rules - [X] list_topic_rules
- [ ] list_v2_logging_levels - [ ] list_v2_logging_levels
- [ ] list_violation_events - [ ] list_violation_events
- [ ] register_ca_certificate - [ ] register_ca_certificate
@ -4538,7 +4538,7 @@
- [ ] reject_certificate_transfer - [ ] reject_certificate_transfer
- [ ] remove_thing_from_billing_group - [ ] remove_thing_from_billing_group
- [X] remove_thing_from_thing_group - [X] remove_thing_from_thing_group
- [ ] replace_topic_rule - [X] replace_topic_rule
- [ ] search_index - [ ] search_index
- [ ] set_default_authorizer - [ ] set_default_authorizer
- [X] set_default_policy_version - [X] set_default_policy_version

View File

@ -425,6 +425,57 @@ class FakeEndpoint(BaseModel):
return obj return obj
class FakeRule(BaseModel):
def __init__(
self,
rule_name,
description,
created_at,
rule_disabled,
topic_pattern,
actions,
error_action,
sql,
aws_iot_sql_version,
region_name,
):
self.region_name = region_name
self.rule_name = rule_name
self.description = description or ""
self.created_at = created_at
self.rule_disabled = bool(rule_disabled)
self.topic_pattern = topic_pattern
self.actions = actions or []
self.error_action = error_action or {}
self.sql = sql
self.aws_iot_sql_version = aws_iot_sql_version or "2016-03-23"
self.arn = "arn:aws:iot:%s:1:rule/%s" % (self.region_name, rule_name)
def to_get_dict(self):
return {
"rule": {
"actions": self.actions,
"awsIotSqlVersion": self.aws_iot_sql_version,
"createdAt": self.created_at,
"description": self.description,
"errorAction": self.error_action,
"ruleDisabled": self.rule_disabled,
"ruleName": self.rule_name,
"sql": self.sql,
},
"ruleArn": self.arn,
}
def to_dict(self):
return {
"ruleName": self.rule_name,
"createdAt": self.created_at,
"ruleArn": self.arn,
"ruleDisabled": self.rule_disabled,
"topicPattern": self.topic_pattern,
}
class IoTBackend(BaseBackend): class IoTBackend(BaseBackend):
def __init__(self, region_name=None): def __init__(self, region_name=None):
super(IoTBackend, self).__init__() super(IoTBackend, self).__init__()
@ -438,6 +489,7 @@ class IoTBackend(BaseBackend):
self.policies = OrderedDict() self.policies = OrderedDict()
self.principal_policies = OrderedDict() self.principal_policies = OrderedDict()
self.principal_things = OrderedDict() self.principal_things = OrderedDict()
self.rules = OrderedDict()
self.endpoint = None self.endpoint = None
def reset(self): def reset(self):
@ -1275,6 +1327,47 @@ class IoTBackend(BaseBackend):
return job_executions, next_token return job_executions, next_token
def list_topic_rules(self):
return [r.to_dict() for r in self.rules.values()]
def get_topic_rule(self, rule_name):
if rule_name not in self.rules:
raise ResourceNotFoundException()
return self.rules[rule_name].to_get_dict()
def create_topic_rule(self, rule_name, sql, **kwargs):
if rule_name in self.rules:
raise ResourceAlreadyExistsException("Rule with given name already exists")
result = re.search(r"FROM\s+([^\s]*)", sql)
topic = result.group(1).strip("'") if result else None
self.rules[rule_name] = FakeRule(
rule_name=rule_name,
created_at=int(time.time()),
topic_pattern=topic,
sql=sql,
region_name=self.region_name,
**kwargs
)
def replace_topic_rule(self, rule_name, **kwargs):
self.delete_topic_rule(rule_name)
self.create_topic_rule(rule_name, **kwargs)
def delete_topic_rule(self, rule_name):
if rule_name not in self.rules:
raise ResourceNotFoundException()
del self.rules[rule_name]
def enable_topic_rule(self, rule_name):
if rule_name not in self.rules:
raise ResourceNotFoundException()
self.rules[rule_name].rule_disabled = False
def disable_topic_rule(self, rule_name):
if rule_name not in self.rules:
raise ResourceNotFoundException()
self.rules[rule_name].rule_disabled = True
iot_backends = {} iot_backends = {}
for region in Session().get_available_regions("iot"): for region in Session().get_available_regions("iot"):

View File

@ -635,3 +635,47 @@ class IoTResponse(BaseResponse):
thing_groups_to_remove=thing_groups_to_remove, thing_groups_to_remove=thing_groups_to_remove,
) )
return json.dumps(dict()) return json.dumps(dict())
def list_topic_rules(self):
return json.dumps(dict(rules=self.iot_backend.list_topic_rules()))
def get_topic_rule(self):
return json.dumps(
self.iot_backend.get_topic_rule(rule_name=self._get_param("ruleName"))
)
def create_topic_rule(self):
self.iot_backend.create_topic_rule(
rule_name=self._get_param("ruleName"),
description=self._get_param("description"),
rule_disabled=self._get_param("ruleDisabled"),
actions=self._get_param("actions"),
error_action=self._get_param("errorAction"),
sql=self._get_param("sql"),
aws_iot_sql_version=self._get_param("awsIotSqlVersion"),
)
return json.dumps(dict())
def replace_topic_rule(self):
self.iot_backend.replace_topic_rule(
rule_name=self._get_param("ruleName"),
description=self._get_param("description"),
rule_disabled=self._get_param("ruleDisabled"),
actions=self._get_param("actions"),
error_action=self._get_param("errorAction"),
sql=self._get_param("sql"),
aws_iot_sql_version=self._get_param("awsIotSqlVersion"),
)
return json.dumps(dict())
def delete_topic_rule(self):
self.iot_backend.delete_topic_rule(rule_name=self._get_param("ruleName"))
return json.dumps(dict())
def enable_topic_rule(self):
self.iot_backend.enable_topic_rule(rule_name=self._get_param("ruleName"))
return json.dumps(dict())
def disable_topic_rule(self):
self.iot_backend.disable_topic_rule(rule_name=self._get_param("ruleName"))
return json.dumps(dict())

View File

@ -1991,3 +1991,166 @@ def test_list_job_executions_for_thing():
job_execution["executionSummaries"][0].should.have.key("jobId").which.should.equal( job_execution["executionSummaries"][0].should.have.key("jobId").which.should.equal(
job_id job_id
) )
class TestTopicRules:
name = "my-rule"
payload = {
"sql": "SELECT * FROM 'topic/*' WHERE something > 0",
"actions": [
{"dynamoDBv2": {"putItem": {"tableName": "my-table"}, "roleArn": "my-role"}}
],
"errorAction": {
"republish": {"qos": 0, "roleArn": "my-role", "topic": "other-topic"}
},
"description": "my-description",
"ruleDisabled": False,
"awsIotSqlVersion": "2016-03-23",
}
@mock_iot
def test_topic_rule_create(self):
client = boto3.client("iot", region_name="ap-northeast-1")
client.create_topic_rule(ruleName=self.name, topicRulePayload=self.payload)
# duplicated rule name
with pytest.raises(ClientError) as ex:
client.create_topic_rule(ruleName=self.name, topicRulePayload=self.payload)
error_code = ex.value.response["Error"]["Code"]
error_code.should.equal("ResourceAlreadyExistsException")
@mock_iot
def test_topic_rule_list(self):
client = boto3.client("iot", region_name="ap-northeast-1")
# empty response
res = client.list_topic_rules()
res.should.have.key("rules").which.should.have.length_of(0)
client.create_topic_rule(ruleName=self.name, topicRulePayload=self.payload)
client.create_topic_rule(ruleName="my-rule-2", topicRulePayload=self.payload)
res = client.list_topic_rules()
res.should.have.key("rules").which.should.have.length_of(2)
for rule, name in zip(res["rules"], [self.name, "my-rule-2"]):
rule.should.have.key("ruleName").which.should.equal(name)
rule.should.have.key("createdAt").which.should_not.be.none
rule.should.have.key("ruleArn").which.should_not.be.none
rule.should.have.key("ruleDisabled").which.should.equal(
self.payload["ruleDisabled"]
)
rule.should.have.key("topicPattern").which.should.equal("topic/*")
@mock_iot
def test_topic_rule_get(self):
client = boto3.client("iot", region_name="ap-northeast-1")
# no such rule
with pytest.raises(ClientError) as ex:
client.get_topic_rule(ruleName=self.name)
error_code = ex.value.response["Error"]["Code"]
error_code.should.equal("ResourceNotFoundException")
client.create_topic_rule(ruleName=self.name, topicRulePayload=self.payload)
rule = client.get_topic_rule(ruleName=self.name)
rule.should.have.key("ruleArn").which.should_not.be.none
rule.should.have.key("rule")
rrule = rule["rule"]
rrule.should.have.key("actions").which.should.equal(self.payload["actions"])
rrule.should.have.key("awsIotSqlVersion").which.should.equal(
self.payload["awsIotSqlVersion"]
)
rrule.should.have.key("createdAt").which.should_not.be.none
rrule.should.have.key("description").which.should.equal(
self.payload["description"]
)
rrule.should.have.key("errorAction").which.should.equal(
self.payload["errorAction"]
)
rrule.should.have.key("ruleDisabled").which.should.equal(
self.payload["ruleDisabled"]
)
rrule.should.have.key("ruleName").which.should.equal(self.name)
rrule.should.have.key("sql").which.should.equal(self.payload["sql"])
@mock_iot
def test_topic_rule_replace(self):
client = boto3.client("iot", region_name="ap-northeast-1")
# no such rule
with pytest.raises(ClientError) as ex:
client.replace_topic_rule(ruleName=self.name, topicRulePayload=self.payload)
error_code = ex.value.response["Error"]["Code"]
error_code.should.equal("ResourceNotFoundException")
client.create_topic_rule(ruleName=self.name, topicRulePayload=self.payload)
payload = self.payload.copy()
payload["description"] = "new-description"
client.replace_topic_rule(
ruleName=self.name, topicRulePayload=payload,
)
rule = client.get_topic_rule(ruleName=self.name)
rule["rule"]["ruleName"].should.equal(self.name)
rule["rule"]["description"].should.equal(payload["description"])
@mock_iot
def test_topic_rule_disable(self):
client = boto3.client("iot", region_name="ap-northeast-1")
# no such rule
with pytest.raises(ClientError) as ex:
client.disable_topic_rule(ruleName=self.name)
error_code = ex.value.response["Error"]["Code"]
error_code.should.equal("ResourceNotFoundException")
client.create_topic_rule(ruleName=self.name, topicRulePayload=self.payload)
client.disable_topic_rule(ruleName=self.name)
rule = client.get_topic_rule(ruleName=self.name)
rule["rule"]["ruleName"].should.equal(self.name)
rule["rule"]["ruleDisabled"].should.equal(True)
@mock_iot
def test_topic_rule_enable(self):
client = boto3.client("iot", region_name="ap-northeast-1")
# no such rule
with pytest.raises(ClientError) as ex:
client.enable_topic_rule(ruleName=self.name)
error_code = ex.value.response["Error"]["Code"]
error_code.should.equal("ResourceNotFoundException")
payload = self.payload.copy()
payload["ruleDisabled"] = True
client.create_topic_rule(ruleName=self.name, topicRulePayload=payload)
client.enable_topic_rule(ruleName=self.name)
rule = client.get_topic_rule(ruleName=self.name)
rule["rule"]["ruleName"].should.equal(self.name)
rule["rule"]["ruleDisabled"].should.equal(False)
@mock_iot
def test_topic_rule_delete(self):
client = boto3.client("iot", region_name="ap-northeast-1")
# no such rule
with pytest.raises(ClientError) as ex:
client.delete_topic_rule(ruleName=self.name)
error_code = ex.value.response["Error"]["Code"]
error_code.should.equal("ResourceNotFoundException")
client.create_topic_rule(ruleName=self.name, topicRulePayload=self.payload)
client.enable_topic_rule(ruleName=self.name)
client.delete_topic_rule(ruleName=self.name)
res = client.list_topic_rules()
res.should.have.key("rules").which.should.have.length_of(0)