# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import boto3 import logging from botocore.exceptions import WaiterError from crhelper import CfnResource from retrying import retry from datetime import datetime, timezone logger = logging.getLogger(__name__) cfnResource = CfnResource(json_logging=False, log_level='INFO', polling_interval=1) iotsitewise = boto3.client('iotsitewise') iotevents = boto3.client('iotevents') STANDARD_RETRY_MAX_ATTEMPT_COUNT = 10 WAITER_ERROR_RETRY_MAX_ATTEMPT_COUNT = 2 WAIT_TIME_IN_MILLISECONDS = 3000 @cfnResource.create def create_alarm(event, context): logger.info('Create alarm model') logger.info(event) try: alarm_model_name, alarm_model_arn = create_alarm_model(event) update_asset_model_with_alarm_model_arn(event, alarm_model_arn) current_time = datetime.now(timezone.utc).timestamp() release_date = datetime(2021, 5, 27, 16, 0, 0).timestamp() if current_time < release_date: update_sitewise_iotevents_subscription(event) return alarm_model_name except Exception as e: logger.exception(e) raise e def create_alarm_model(event): name = event['ResourceProperties']['name'] description = event['ResourceProperties']['description'] severity = event['ResourceProperties']['severity'] asset_model_id = event['ResourceProperties']['assetModelId'] role_arn = event['ResourceProperties']['roleArn'] alarm_rule = event['ResourceProperties']['alarmRule'] alarm_event_actions = event['ResourceProperties']['alarmEventActions'] alarm_capabilities = event['ResourceProperties']['alarmCapabilities'] asset_model_composite_model_name = event['ResourceProperties']['assetModelCompositeModelName'] alarm_model_name = name + '_assetModel_' + asset_model_id alarm_capabilities['initializationConfiguration']['disabledOnInitialization'] = json.loads(alarm_capabilities['initializationConfiguration']['disabledOnInitialization']) alarm_capabilities['acknowledgeFlow']['enabled'] = json.loads(alarm_capabilities['acknowledgeFlow']['enabled']) alarm_rule = build_iot_events_alarm_rule_expression(alarm_rule, asset_model_id) alarm_event_actions = build_iot_events_sitewise_action_expression(alarm_event_actions, asset_model_id, asset_model_composite_model_name) return create_iotevents_alarm_model(alarm_model_name, description, role_arn, int(severity), alarm_rule, alarm_event_actions, alarm_capabilities) def build_iot_events_alarm_rule_expression(alarm_rule, asset_model_id): input_property_id = alarm_rule['simpleRule']['inputProperty'] threshold_property_id = alarm_rule['simpleRule']['threshold'] return { 'simpleRule': { 'inputProperty': f"$sitewise.assetModel.`{asset_model_id}`.`{input_property_id}`.propertyValue.value", 'comparisonOperator': alarm_rule['simpleRule']['comparisonOperator'], 'threshold': f"$sitewise.assetModel.`{asset_model_id}`.`{threshold_property_id}`.propertyValue.value", } } def build_iot_events_sitewise_action_expression(alarm_event_actions, asset_model_id, asset_model_composite_model_name): property_id = alarm_event_actions['alarmActions'][0]["iotSiteWise"]["assetId"] alarm_event_actions['alarmActions'][0]["iotSiteWise"]["assetId"] = f"$sitewise.assetModel.`{asset_model_id}`.`{property_id}`.assetId" alarm_event_actions['alarmActions'][0]["iotSiteWise"]["propertyId"] = f"'{find_alarm_state_property_id(asset_model_id, asset_model_composite_model_name)}'" return alarm_event_actions def find_alarm_state_property_id(asset_model_id, asset_model_composite_model_name): asset_model = iotsitewise.describe_asset_model(assetModelId=asset_model_id) try: composite_model = [cm for cm in asset_model['assetModelCompositeModels'] if asset_model_composite_model_name in cm['name']] sitewise_property = [p for p in composite_model[0]['properties'] if 'measurement' in p['type']] return sitewise_property[0]['id'] except IndexError: logger.error(f"Error finding alarm state property of assetModelCompositeModel {asset_model_composite_model_name} in asset model {asset_model_id}") def create_iotevents_alarm_model(name, description, role_arn, severity, alarm_rule, alarm_event_actions, alarm_capabilities): try: create_alarm_model_response = iotevents.create_alarm_model(alarmModelName=name, alarmModelDescription=description, roleArn=role_arn, severity=severity, alarmRule=alarm_rule, alarmCapabilities=alarm_capabilities, alarmEventActions=alarm_event_actions) logger.debug(f"Received the following create alarm model response {create_alarm_model_response}") return name, create_alarm_model_response['alarmModelArn'] except Exception as e: logger.warning('Failed to create AlarmModel. Trying again with a modified name') return create_alarm_model_uuid_name_with_retries(name, description, role_arn, severity, alarm_rule, alarm_event_actions, alarm_capabilities) @retry(wait_fixed=WAIT_TIME_IN_MILLISECONDS, stop_max_attempt_number=STANDARD_RETRY_MAX_ATTEMPT_COUNT) def create_alarm_model_uuid_name_with_retries(name, description, role_arn, severity, alarm_rule, alarm_event_actions, alarm_capabilities): alarm_model_name = name + get_timestamp() create_alarm_model_response = iotevents.create_alarm_model(alarmModelName=alarm_model_name, alarmModelDescription=description, roleArn=role_arn, severity=severity, alarmRule=alarm_rule, alarmCapabilities=alarm_capabilities, alarmEventActions=alarm_event_actions) logger.debug(f"Received the following create alarm model response {create_alarm_model_response}") return alarm_model_name, create_alarm_model_response['alarmModelArn'] def is_waiter_error(exception): return isinstance(exception, WaiterError) @retry(retry_on_exception=is_waiter_error,wait_fixed=WAIT_TIME_IN_MILLISECONDS, stop_max_attempt_number=STANDARD_RETRY_MAX_ATTEMPT_COUNT) def update_asset_model_with_alarm_model_arn(event, alarm_model_arn): asset_model_id = event['ResourceProperties']['assetModelId'] asset_model = iotsitewise.describe_asset_model(assetModelId=asset_model_id) for asset_model_composite_model in asset_model['assetModelCompositeModels']: if asset_model_composite_model['name'] == event['ResourceProperties']['assetModelCompositeModelName']: asset_model_composite_model['properties'].append( { "name": "AWS/ALARM_SOURCE", "dataType": "STRING", "unit": "none", "type": { "attribute": { "defaultValue": alarm_model_arn } } } ) try: iotsitewise.update_asset_model( assetModelId=asset_model['assetModelId'], assetModelName=asset_model['assetModelName'], assetModelDescription=asset_model['assetModelDescription'], assetModelProperties=asset_model['assetModelProperties'], assetModelHierarchies=asset_model['assetModelHierarchies'], assetModelCompositeModels=asset_model['assetModelCompositeModels'] ) logger.info(f"Update asset model: {asset_model['assetModelName']} with alarmModelArn {alarm_model_arn}") except Exception as e: logger.error(e) iotsitewise.get_waiter(waiter_name='asset_model_active').wait(assetModelId=asset_model_id) @retry(wait_fixed=WAIT_TIME_IN_MILLISECONDS, stop_max_attempt_number=STANDARD_RETRY_MAX_ATTEMPT_COUNT) def update_sitewise_iotevents_subscription(event): alarm_rule = event['ResourceProperties']['alarmRule'] input_property_id = alarm_rule['simpleRule']['inputProperty'] threshold_property_id = alarm_rule['simpleRule']['threshold'] asset_model_id = event['ResourceProperties']['assetModelId'] sitewise_subscription_role_arn = event['ResourceProperties']['sitewiseSubscriptionArn'] update_subscription(asset_model_id, input_property_id, sitewise_subscription_role_arn) update_subscription(asset_model_id, threshold_property_id, sitewise_subscription_role_arn) def update_subscription(asset_model_id, property_id, subscription_role_arn): try: iotsitewise.update_subscription( service='IOT_EVENTS', assetModelId=asset_model_id, propertyId=property_id, config={ "roleArn": subscription_role_arn }, sendCurrentValue=True ) logger.info(f"Created IOT_EVENTS subscription for AssetModel: {asset_model_id} and Property: {property_id}") except Exception as e: logger.error(e) @cfnResource.update def no_op(_, __): pass @cfnResource.delete def delete_alarm_model(event, context): logger.info('Delete IoT Events alarm model') logger.info(event) try: name = event['PhysicalResourceId'] iotevents.delete_alarm_model(alarmModelName=name) except Exception as e: logger.error(e) finally: return event.get('PhysicalResourceId', None) def get_timestamp(): return datetime.now(timezone.utc).strftime("%Y-%b-%d%H_%M_%S") def handler(event, context): cfnResource(event, context)