├── .cfnnag_global_suppress_list ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── general_question.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── THIRD_PARTY_LICENSES.txt ├── architecture.png ├── deployment ├── build-s3-dist.sh └── run-unit-tests.sh └── source ├── .eslintignore ├── .eslintrc.json ├── .prettierrc ├── infrastructure ├── .gitignore ├── .npmignore ├── bin │ └── machine-to-cloud-connectivity.ts ├── cdk.json ├── jest.config.js ├── lib │ ├── app-registry │ │ └── app-registry.ts │ ├── backend │ │ ├── api.ts │ │ └── connection-builder.ts │ ├── common-resource │ │ ├── cloudwatch-logs-policy.ts │ │ ├── logging-bucket.ts │ │ └── source-bucket.ts │ ├── custom-resource │ │ └── custom-resources.ts │ ├── data-flow │ │ ├── kinesis-data-stream.ts │ │ ├── sqs-message-consumer.ts │ │ └── timestream.ts │ ├── frontend │ │ ├── cloudfront.ts │ │ └── ui.ts │ ├── greengrass │ │ └── greengrass.ts │ └── machine-to-cloud-connectivity-stack.ts ├── package.json ├── test │ ├── api.test.ts │ ├── app-registry.test.ts │ ├── aspects.test.ts │ ├── cloudfront.test.ts │ ├── cloudwatch-logs-policy.test.ts │ ├── connection-builder.test.ts │ ├── custom-resources.test.ts │ ├── data-stream.test.ts │ ├── greengrass.test.ts │ ├── kinesis-data-stream.test.ts │ ├── logging-bucket.test.ts │ ├── machine-to-cloud-connectivity.test.ts │ ├── source-bucket.test.ts │ ├── sqs-message-consumer.test.ts │ ├── timestream.test.ts │ ├── ui.test.ts │ └── utils.test.ts ├── tsconfig.json └── utils │ ├── aspects.ts │ └── utils.ts ├── lambda ├── connection-builder │ ├── api-get-request.ts │ ├── api-post-request.ts │ ├── index.ts │ ├── jest.config.js │ ├── package.json │ ├── test │ │ ├── api-get-request.test.ts │ │ ├── api-post-connections-request.test.ts │ │ ├── api-post-greengrass-request.test.ts │ │ ├── invalid-request.test.ts │ │ ├── jest-environment-variables.ts │ │ └── mock.ts │ └── tsconfig.json ├── custom-resource │ ├── general-custom-resources │ │ └── index.ts │ ├── greengrass-custom-resources │ │ ├── index.ts │ │ └── script │ │ │ ├── m2c2-install.ps1 │ │ │ └── m2c2-install.sh │ ├── index.ts │ ├── jest.config.js │ ├── package.json │ ├── test │ │ ├── general-custom-resources.test.ts │ │ ├── greengrass-custom-resources.test.ts │ │ ├── invalid-inputs.test.ts │ │ ├── jest-environment-variables.ts │ │ ├── mock.ts │ │ ├── s3-bucket-delete.test.ts │ │ ├── timestream-db-delete.test.ts │ │ └── ui-custom-resource.test.ts │ ├── tsconfig.json │ └── ui-custom-resources │ │ └── index.ts ├── greengrass-deployer │ ├── index.ts │ ├── jest.config.js │ ├── package.json │ ├── test │ │ ├── connection-delete.test.ts │ │ ├── connection-deploy.test.ts │ │ ├── connection-update.test.ts │ │ ├── deployment-status.test.ts │ │ ├── invalid-inputs.test.ts │ │ ├── jest-environment-variables.ts │ │ ├── mock.ts │ │ └── no-send-anonymous-metrics.test.ts │ └── tsconfig.json ├── lib │ ├── aws-handlers │ │ ├── dynamodb-handler.ts │ │ ├── greengrass-v2-component-builder.ts │ │ ├── greengrass-v2-handler.ts │ │ ├── iot-handler.ts │ │ ├── iot-sitewise-handler.ts │ │ ├── lambda-handler.ts │ │ ├── s3-handler.ts │ │ ├── secretsManager-handler.ts │ │ └── timestream-handler.ts │ ├── errors.ts │ ├── jest.config.js │ ├── logger.ts │ ├── package.json │ ├── test │ │ ├── dynamodb-handler.test.ts │ │ ├── errors.test.ts │ │ ├── greengrass-v2-handler.test.ts │ │ ├── iot-handler.test.ts │ │ ├── iot-sitewise-handler.test.ts │ │ ├── jest-environment-variables.ts │ │ ├── lambda-handler.test.ts │ │ ├── logger.test.ts │ │ ├── mock.ts │ │ ├── s3-handler.test.ts │ │ ├── timestream-handler.test.ts │ │ ├── utils.test.ts │ │ ├── validations-connections.test.ts │ │ └── validations-greengrass-core-device.test.ts │ ├── tsconfig.json │ ├── types │ │ ├── connection-builder-types.ts │ │ ├── custom-resource-types.ts │ │ ├── dynamodb-handler-types.ts │ │ ├── greengrass-deployer-types.ts │ │ ├── greengrass-v2-handler-types.ts │ │ ├── index.ts │ │ ├── iot-handler-types.ts │ │ ├── iot-sitewise-handler-types.ts │ │ ├── message-consumer-types.ts │ │ ├── modbus-types.ts │ │ ├── s3-handler-types.ts │ │ ├── solution-common-types.ts │ │ ├── timestream-handler-types.ts │ │ └── utils-types.ts │ ├── utils.ts │ └── validations.ts ├── sqs-message-consumer │ ├── index.ts │ ├── jest.config.js │ ├── package.json │ ├── test │ │ ├── index.test.ts │ │ ├── jest-environment-variables.ts │ │ └── mock.ts │ └── tsconfig.json └── timestream-writer │ ├── index.ts │ ├── jest.config.js │ ├── package.json │ ├── test │ ├── index.test.ts │ ├── jest-environment-variables.ts │ └── mock.ts │ └── tsconfig.json ├── machine_connector ├── boilerplate │ ├── __init__.py │ ├── logging │ │ └── logger.py │ ├── messaging │ │ ├── __init__.py │ │ ├── announcements.py │ │ ├── message.py │ │ ├── message_batch.py │ │ └── message_sender.py │ └── tests │ │ ├── .coveragerc │ │ ├── __init__.py │ │ ├── test_logger.py │ │ ├── test_message_batch.py │ │ └── test_message_sender.py ├── m2c2_modbus_tcp_connector │ ├── __init__.py │ ├── config.py │ ├── m2c2_modbus_tcp_connector.py │ ├── messages.py │ ├── modbus_data_collection_controller.py │ ├── modbus_exception.py │ ├── modbus_message_handler.py │ ├── modbus_slave_config.py │ ├── pymodbus_client.py │ ├── requirements.txt │ └── tests │ │ ├── __init__.py │ │ ├── test_m2c2_modbus_tcp_connector.py │ │ ├── test_modbus_data_collection_controller.py │ │ ├── test_modbus_message_handler.py │ │ └── test_pymodbus_client.py ├── m2c2_opcda_connector │ ├── __init__.py │ ├── m2c2_opcda_connector.py │ ├── messages.py │ ├── requirements.txt │ ├── tests │ │ ├── .coveragerc │ │ ├── __init__.py │ │ ├── test_m2c2_opcda_connector.py │ │ └── test_message_validation.py │ └── validations │ │ ├── __init__.py │ │ ├── m2c2_msg_types.py │ │ └── message_validation.py ├── m2c2_osipi_connector │ ├── __init__.py │ ├── m2c2_osipi_connector.py │ ├── messages.py │ ├── pi_connector_sdk │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── enhanced_json_encoder.py │ │ ├── osi_pi_connector.py │ │ ├── pi_connection_config.py │ │ ├── pi_response.py │ │ └── time_helper.py │ ├── requirements.txt │ ├── tests │ │ ├── .coveragerc │ │ ├── __init__.py │ │ ├── test_m2c2_osipi_connector.py │ │ ├── test_message_validation.py │ │ ├── test_osi_pi_connector.py │ │ └── test_time_helper.py │ └── validations │ │ ├── __init__.py │ │ ├── m2c2_msg_types.py │ │ └── message_validation.py ├── m2c2_publisher │ ├── __init__.py │ ├── converters │ │ ├── __init__.py │ │ ├── common_converter.py │ │ ├── historian │ │ │ ├── __init__.py │ │ │ ├── historian_converter.py │ │ │ └── historian_message.py │ │ ├── iot_topic_converter.py │ │ ├── sitewise_converter.py │ │ ├── tag_converter.py │ │ └── timestream_converter.py │ ├── m2c2_publisher.py │ ├── payload_router.py │ ├── requirements.txt │ ├── targets │ │ ├── __init__.py │ │ ├── historian_target.py │ │ ├── iot_topic_target.py │ │ ├── kinesis_target.py │ │ └── sitewise_target.py │ └── tests │ │ ├── .coveragerc │ │ ├── __init__.py │ │ ├── test_common_converter.py │ │ ├── test_historian_converter.py │ │ ├── test_historian_target.py │ │ ├── test_iot_topic_converter.py │ │ ├── test_iot_topic_target.py │ │ ├── test_kinesis_target.py │ │ ├── test_payload_router.py │ │ ├── test_sitewise_converter.py │ │ ├── test_sitewise_target.py │ │ ├── test_tag_converter.py │ │ └── test_timestream_converter.py ├── requirements_dev.txt └── utils │ ├── __init__.py │ ├── client.py │ ├── constants.py │ ├── custom_exception.py │ ├── init_msg_metadata.py │ ├── pickle_checkpoint_manager.py │ ├── requirements.txt │ ├── stream_manager_helper.py │ ├── subscription_stream_handler.py │ └── tests │ ├── .coveragerc │ ├── __init__.py │ ├── test_client.py │ ├── test_init_msg_metadata.py │ ├── test_pickle_checkpoint_manager.py │ └── test_stream_manager_helper.py ├── package.json ├── ui ├── .env ├── package.json ├── public │ ├── index.html │ └── manifest.json ├── src │ ├── App.tsx │ ├── assets │ │ └── css │ │ │ └── style.scss │ ├── components │ │ ├── EmptyCol.tsx │ │ ├── EmptyRow.tsx │ │ ├── Header.tsx │ │ ├── Loading.tsx │ │ ├── MessageModal.tsx │ │ ├── PageNotFound.tsx │ │ └── __test__ │ │ │ ├── EmptyCol.test.tsx │ │ │ ├── EmptyRow.test.tsx │ │ │ ├── Header.test.tsx │ │ │ ├── Loading.test.tsx │ │ │ ├── MessageModal.test.tsx │ │ │ ├── PageNotFound.test.tsx │ │ │ └── __snapshots__ │ │ │ ├── EmptyCol.test.tsx.snap │ │ │ ├── EmptyRow.test.tsx.snap │ │ │ ├── Header.test.tsx.snap │ │ │ ├── Loading.test.tsx.snap │ │ │ ├── MessageModal.test.tsx.snap │ │ │ └── PageNotFound.test.tsx.snap │ ├── hooks │ │ ├── ConnectionHook.tsx │ │ └── GreengrassCoreDeviceHook.tsx │ ├── index.tsx │ ├── react-app-env.d.ts │ ├── util │ │ ├── __test__ │ │ │ ├── apis.test.ts │ │ │ ├── utils.buildConnectionDefinition.test.ts │ │ │ ├── utils.buildOpcDaTags.test.ts │ │ │ ├── utils.copyArray.test.ts │ │ │ ├── utils.copyObject.test.ts │ │ │ ├── utils.getAmplifyConfiguration.test.ts │ │ │ ├── utils.getConditionalValue.test.ts │ │ │ ├── utils.getErrorMessage.test.ts │ │ │ ├── utils.setValue.test.ts │ │ │ ├── utils.signOut.test.ts │ │ │ ├── validations.validateConnectionDefinition.test.ts │ │ │ └── validations.validateGreengrassCoreDeviceName.test.ts │ │ ├── apis.ts │ │ ├── lang │ │ │ └── en.json │ │ ├── types.ts │ │ ├── utils.ts │ │ └── validations.ts │ └── views │ │ ├── __test__ │ │ ├── ConnectionForm.test.tsx │ │ ├── ConnectionLogsModal.test.tsx │ │ ├── Dashboard.test.tsx │ │ ├── GreengrassCoreDeviceDashboard.test.tsx │ │ ├── GreengrassCoreDeviceForm.test.tsx │ │ ├── ModbusTcpForm.test.tsx │ │ ├── OpcDaForm.test.tsx │ │ ├── OpcUaForm.test.tsx │ │ ├── OsiPiForm.test.tsx │ │ └── __snapshots__ │ │ │ ├── ConnectionForm.test.tsx.snap │ │ │ ├── ConnectionLogsModal.test.tsx.snap │ │ │ ├── Dashboard.test.tsx.snap │ │ │ ├── GreengrassCoreDeviceForm.test.tsx.snap │ │ │ ├── ModbusTcpForm.test.tsx.snap │ │ │ ├── OpcDaForm.test.tsx.snap │ │ │ ├── OpcUaForm.test.tsx.snap │ │ │ └── OsiPiForm.test.tsx.snap │ │ ├── connection │ │ ├── ConnectionForm.tsx │ │ ├── ConnectionLogsModal.tsx │ │ ├── Dashboard.tsx │ │ ├── ModbusTcpForm.tsx │ │ ├── OpcDaForm.tsx │ │ ├── OpcUaForm.tsx │ │ ├── OsiPiForm.tsx │ │ └── connection-form-utils.ts │ │ └── greengrass │ │ ├── GreengrassCoreDeviceForm.tsx │ │ ├── GreengrassCoreDeviceInputForm.tsx │ │ ├── GreengrassCoreDevicesDashboard.tsx │ │ └── greengrass-core-device-form-utils.ts └── tsconfig.json └── utils └── requirements.txt /.cfnnag_global_suppress_list: -------------------------------------------------------------------------------- 1 | RulesToSuppress: 2 | - id: W76 3 | reason: SPCM higher than 25; all policies are required. 4 | - id: W89 5 | reason: Lambda functions in the solution does not require any VPC connection. 6 | - id: W92 7 | reason: Lambda functions in the solution mostly does not require reserved concurrency executions. 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | **To Reproduce** 14 | 15 | 16 | **Expected behavior** 17 | 18 | 19 | **Please complete the following information about the solution:** 20 | - [ ] Version: [e.g. v1.0.0] 21 | - [ ] Region: [e.g. us-east-1] 22 | - [ ] Was the solution modified from the version published on this repository? 23 | - [ ] If the answer to the previous question was yes, are the changes available on GitHub? 24 | - [ ] Have you checked your [service quotas](https://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) for the services this solution uses? 25 | - [ ] Were there any errors in the CloudWatch Logs? 26 | 27 | **Screenshots** 28 | 29 | 30 | **Additional context** 31 | 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this solution 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | **Describe the feature you'd like** 14 | 15 | 16 | **Additional context** 17 | 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general_question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: General question 3 | about: Ask a general question 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What is your question?** 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Issue #, if available:** 2 | 3 | 4 | 5 | 6 | **Description of changes:** 7 | 8 | 9 | 10 | **Checklist** 11 | - [ ] 👋 I have run the unit tests, and all unit tests have passed. 12 | - [ ] ⚠️ This pull request might incur a breaking change. 13 | 14 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | global-s3-assets/ 3 | regional-s3-assets/ 4 | open-source/ 5 | package/ 6 | source/test/ 7 | .venv/ 8 | __pycache__ 9 | .pytest_cache 10 | .coverage 11 | node_modules/ 12 | dist/ 13 | coverage/ 14 | aws-exports.js 15 | build/ 16 | .matt_deploy.env 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/machine-to-cloud-connectivity-framework/14c96676a8c0efc7d77ad5ebf823c54a3a18a32a/architecture.png -------------------------------------------------------------------------------- /source/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | build 5 | aws-exports.js 6 | cdk.out -------------------------------------------------------------------------------- /source/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:jsdoc/recommended", 10 | "plugin:prettier/recommended", 11 | "plugin:react/recommended" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": "latest", 16 | "project": "**/tsconfig.json", 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "@typescript-eslint", 21 | "header", 22 | "import", 23 | "react" 24 | ], 25 | "rules": { 26 | "@typescript-eslint/no-inferrable-types": [ 27 | "off", 28 | { 29 | "ignoreParameters": true, 30 | "ignoreProperties": true 31 | } 32 | ], 33 | "@typescript-eslint/no-useless-constructor": [ 34 | "off" 35 | ], 36 | "arrow-body-style": [ 37 | "warn", 38 | "as-needed" 39 | ], 40 | "prefer-arrow-callback": [ 41 | "warn" 42 | ], 43 | "no-inferrable-types": [ 44 | "off", 45 | "ignore-params" 46 | ], 47 | "no-unused-vars": [ 48 | "error", 49 | { 50 | "args": "none", 51 | "argsIgnorePattern": "^_", 52 | "varsIgnorePattern": "^[A-Z]" 53 | } 54 | ], 55 | "header/header": [ 56 | "error", 57 | "line", 58 | [ 59 | " Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.", 60 | " SPDX-License-Identifier: Apache-2.0" 61 | ], 62 | 2 63 | ], 64 | "jsdoc/require-param-type": [ 65 | "off" 66 | ], 67 | "jsdoc/require-returns-type": [ 68 | "off" 69 | ], 70 | "jsdoc/newline-after-description": [ 71 | "off" 72 | ], 73 | "react/react-in-jsx-scope": "off", 74 | "no-empty-function": "off", 75 | "@typescript-eslint/no-empty-function": [ 76 | "off" 77 | ] 78 | }, 79 | "settings": { 80 | "react": { 81 | "version": "17.0.2" 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /source/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSameLine": true, 4 | "bracketSpacing": true, 5 | "printWidth": 120, 6 | "singleQuote": true, 7 | "tabWidth": 2, 8 | "trailingComma": "none" 9 | } 10 | -------------------------------------------------------------------------------- /source/infrastructure/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | coverage/ -------------------------------------------------------------------------------- /source/infrastructure/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /source/infrastructure/bin/machine-to-cloud-connectivity.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { App, Aspects, DefaultStackSynthesizer } from 'aws-cdk-lib'; 5 | import { 6 | MachineToCloudConnectivityFrameworkProps, 7 | MachineToCloudConnectivityFrameworkStack 8 | } from '../lib/machine-to-cloud-connectivity-stack'; 9 | import { AwsSolutionsChecks } from 'cdk-nag'; 10 | import { AppRegistry } from '../lib/app-registry/app-registry'; 11 | import * as crypto from 'crypto'; 12 | 13 | /** 14 | * Gets the solution props from the environment variables. 15 | * @returns The solution props 16 | */ 17 | function getProps(): MachineToCloudConnectivityFrameworkProps { 18 | const { 19 | BUCKET_NAME_PLACEHOLDER, 20 | SOLUTION_NAME_PLACEHOLDER, 21 | VERSION_PLACEHOLDER, 22 | SHOULD_SEND_ANONYMOUS_METRICS, 23 | SHOULD_TEARDOWN_DATA_ON_DESTROY 24 | } = process.env; 25 | 26 | if (typeof BUCKET_NAME_PLACEHOLDER !== 'string' || BUCKET_NAME_PLACEHOLDER.trim() === '') { 27 | throw new Error('Missing required environment variable: BUCKET_NAME_PLACEHOLDER'); 28 | } 29 | 30 | if (typeof SOLUTION_NAME_PLACEHOLDER !== 'string' || SOLUTION_NAME_PLACEHOLDER.trim() === '') { 31 | throw new Error('Missing required environment variable: SOLUTION_NAME_PLACEHOLDER'); 32 | } 33 | 34 | if (typeof VERSION_PLACEHOLDER !== 'string' || VERSION_PLACEHOLDER.trim() === '') { 35 | throw new Error('Missing required environment variable: BUCKET_NAME_PLACEHOLDER'); 36 | } 37 | 38 | if (typeof SHOULD_SEND_ANONYMOUS_METRICS !== 'string' || SHOULD_SEND_ANONYMOUS_METRICS.trim() === '') { 39 | throw new Error('Missing required environment variable: SHOULD_SEND_ANONYMOUS_METRICS'); 40 | } 41 | 42 | if (typeof SHOULD_TEARDOWN_DATA_ON_DESTROY !== 'string' || SHOULD_TEARDOWN_DATA_ON_DESTROY.trim() === '') { 43 | throw new Error('Missing required environment variable: SHOULD_TEARDOWN_DATA_ON_DESTROY'); 44 | } 45 | 46 | const solutionBucketName = BUCKET_NAME_PLACEHOLDER; 47 | const solutionId = 'SO0070'; 48 | const solutionName = SOLUTION_NAME_PLACEHOLDER; 49 | const solutionVersion = VERSION_PLACEHOLDER; 50 | const shouldSendAnonymousMetrics = SHOULD_SEND_ANONYMOUS_METRICS; 51 | const shouldTeardownDataOnDestroy = SHOULD_TEARDOWN_DATA_ON_DESTROY; 52 | const description = `(${solutionId}) - ${solutionName}. Version ${solutionVersion}`; 53 | 54 | return { 55 | description, 56 | solutionBucketName, 57 | solutionId, 58 | solutionName, 59 | solutionVersion, 60 | shouldSendAnonymousMetrics, 61 | shouldTeardownDataOnDestroy 62 | }; 63 | } 64 | 65 | const appProps = getProps(); 66 | const app = new App(); 67 | const m2c2Stack = new MachineToCloudConnectivityFrameworkStack(app, 'Stack', { 68 | synthesizer: new DefaultStackSynthesizer({ 69 | generateBootstrapVersionRule: false 70 | }), 71 | ...appProps 72 | }); 73 | Aspects.of(app).add(new AwsSolutionsChecks()); 74 | const hash = crypto.createHash('sha256').update(m2c2Stack.stackName).digest('hex'); 75 | Aspects.of(m2c2Stack).add( 76 | new AppRegistry(m2c2Stack, `AppRegistry-${hash}`, { 77 | solutionID: appProps.solutionId 78 | }) 79 | ); 80 | -------------------------------------------------------------------------------- /source/infrastructure/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/machine-to-cloud-connectivity.ts", 3 | "context": { 4 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": false, 5 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /source/infrastructure/jest.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | preset: 'ts-jest', 6 | roots: ['/test'], 7 | testEnvironment: 'node', 8 | testMatch: ['**/*.test.ts'], 9 | transform: { 10 | '^.+\\.(ts|tsx)?$': 'ts-jest' 11 | }, 12 | coverageReporters: ['text', ['lcov', { projectRoot: '../../' }]] 13 | }; 14 | -------------------------------------------------------------------------------- /source/infrastructure/lib/common-resource/cloudwatch-logs-policy.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { ArnFormat, Stack, aws_iam as iam } from 'aws-cdk-lib'; 5 | import { Construct } from 'constructs'; 6 | 7 | /** 8 | * Creates a common CloudWatch Logs policy for Lambda functions. 9 | */ 10 | export class CloudwatchLogsPolicyConstruct extends Construct { 11 | public policy: iam.PolicyDocument; 12 | 13 | constructor(scope: Construct, id: string) { 14 | super(scope, id); 15 | 16 | this.policy = new iam.PolicyDocument({ 17 | statements: [ 18 | new iam.PolicyStatement({ 19 | effect: iam.Effect.ALLOW, 20 | actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], 21 | resources: [ 22 | Stack.of(this).formatArn({ 23 | service: 'logs', 24 | resource: 'log-group', 25 | resourceName: '/aws/lambda/*', 26 | arnFormat: ArnFormat.COLON_RESOURCE_NAME 27 | }) 28 | ] 29 | }) 30 | ] 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /source/infrastructure/lib/common-resource/logging-bucket.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { RemovalPolicy, aws_iam as iam, aws_s3 as s3 } from 'aws-cdk-lib'; 5 | import { NagSuppressions } from 'cdk-nag'; 6 | import { Construct } from 'constructs'; 7 | import { addCfnSuppressRules } from '../../utils/utils'; 8 | 9 | /** 10 | * Creates a common CloudWatch Logs policy for Lambda functions. 11 | */ 12 | export class LoggingBucketConstruct extends Construct { 13 | public s3LoggingBucket: s3.Bucket; 14 | 15 | constructor(scope: Construct, id: string) { 16 | super(scope, id); 17 | 18 | this.s3LoggingBucket = new s3.Bucket(this, 'LogBucket', { 19 | enforceSSL: true, 20 | objectOwnership: s3.ObjectOwnership.OBJECT_WRITER, 21 | encryption: s3.BucketEncryption.S3_MANAGED, 22 | removalPolicy: RemovalPolicy.RETAIN, 23 | blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL 24 | }); 25 | this.s3LoggingBucket.addToResourcePolicy( 26 | new iam.PolicyStatement({ 27 | actions: ['s3:*'], 28 | conditions: { 29 | Bool: { 'aws:SecureTransport': 'false' } 30 | }, 31 | effect: iam.Effect.DENY, 32 | principals: [new iam.AnyPrincipal()], 33 | resources: [this.s3LoggingBucket.bucketArn, this.s3LoggingBucket.arnForObjects('*')] 34 | }) 35 | ); 36 | addCfnSuppressRules(this.s3LoggingBucket, [ 37 | { id: 'W35', reason: 'This bucket is to store S3 logs, so it does not require access logs.' } 38 | ]); 39 | 40 | // cdk-nag suppressions 41 | NagSuppressions.addResourceSuppressions(this.s3LoggingBucket, [ 42 | { id: 'AwsSolutions-S1', reason: 'This bucket is to store S3 logs, so it does not require access logs.' }, 43 | { id: 'AwsSolutions-S2', reason: 'Public Access Blocking is handled by objectOwnership' } 44 | ]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /source/infrastructure/lib/common-resource/source-bucket.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { aws_s3 as s3 } from 'aws-cdk-lib'; 5 | import { Construct } from 'constructs'; 6 | 7 | export interface SourceBucketConstructProps { 8 | sourceCodeBucketName: string; 9 | } 10 | 11 | /** 12 | * Imports existing bucket containing source code 13 | */ 14 | export class SourceBucketConstruct extends Construct { 15 | public sourceCodeBucket: s3.IBucket; 16 | 17 | constructor(scope: Construct, id: string, props: SourceBucketConstructProps) { 18 | super(scope, id); 19 | 20 | this.sourceCodeBucket = s3.Bucket.fromBucketName(this, 'SourceCodeBucket', props.sourceCodeBucketName); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /source/infrastructure/lib/frontend/cloudfront.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { CustomResource, CfnCondition, CfnCustomResource, aws_cloudfront as cf, aws_s3 as s3 } from 'aws-cdk-lib'; 5 | import { CloudFrontToS3 } from '@aws-solutions-constructs/aws-cloudfront-s3'; 6 | import { NagSuppressions } from 'cdk-nag'; 7 | import { Construct } from 'constructs'; 8 | 9 | export interface CloudFrontConstructProps { 10 | readonly s3LoggingBucket: s3.IBucket; 11 | readonly customResourcesFunctionArn: string; 12 | readonly shouldTeardownData: CfnCondition; 13 | } 14 | 15 | /** 16 | * Creates a CloudFront distribution. 17 | */ 18 | export class CloudFrontConstruct extends Construct { 19 | public cloudFrontDomainName: string; 20 | public uiBucket: s3.Bucket; 21 | 22 | constructor(scope: Construct, id: string, props: CloudFrontConstructProps) { 23 | super(scope, id); 24 | 25 | const cloudFrontToS3 = new CloudFrontToS3(this, 'CloudFrontToS3', { 26 | bucketProps: { 27 | serverAccessLogsBucket: props.s3LoggingBucket, 28 | serverAccessLogsPrefix: 'ui-s3/' 29 | }, 30 | cloudFrontDistributionProps: { 31 | comment: 'Machine to Cloud Connectivity Framework Distribution', 32 | enableLogging: true, 33 | errorResponses: [ 34 | { httpStatus: 403, responseHttpStatus: 200, responsePagePath: '/index.html' }, 35 | { httpStatus: 404, responseHttpStatus: 200, responsePagePath: '/index.html' } 36 | ], 37 | logBucket: props.s3LoggingBucket, 38 | minimumProtocolVersion: cf.SecurityPolicyProtocol.TLS_V1_2_2019, 39 | logFilePrefix: 'ui-cf/' 40 | }, 41 | insertHttpSecurityHeaders: false 42 | }); 43 | this.cloudFrontDomainName = cloudFrontToS3.cloudFrontWebDistribution.domainName; 44 | this.uiBucket = cloudFrontToS3.s3Bucket; 45 | 46 | const teardownCloudfrontBucket = new CustomResource(this, 'TeardownCloudfrontBucket', { 47 | serviceToken: props.customResourcesFunctionArn, 48 | properties: { 49 | Resource: 'DeleteS3Bucket', 50 | BucketName: this.uiBucket.bucketName 51 | } 52 | }); 53 | const cfnTeardownCloudfrontBucket = teardownCloudfrontBucket.node.defaultChild; 54 | cfnTeardownCloudfrontBucket.cfnOptions.condition = props.shouldTeardownData; 55 | 56 | NagSuppressions.addResourceSuppressions( 57 | cloudFrontToS3, 58 | [ 59 | { id: 'AwsSolutions-CFR1', reason: 'The solution does not control geo restriction.' }, 60 | { id: 'AwsSolutions-CFR2', reason: 'No need to enable WAF.' }, 61 | { 62 | id: 'AwsSolutions-CFR4', 63 | reason: 'No contorl on the solution side as it is using the CloudFront default certificate.' 64 | } 65 | ], 66 | true 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /source/infrastructure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "machine-to-cloud-connectivity-infrastructure", 3 | "version": "4.2.3", 4 | "description": "Machine to Cloud Connectivity Framework Infrastructure", 5 | "author": { 6 | "name": "Amazon Web Services", 7 | "url": "https://aws.amazon.com/solutions" 8 | }, 9 | "license": "Apache-2.0", 10 | "bin": { 11 | "machine-to-cloud-connectivity": "bin/machine-to-cloud-connectivity.js" 12 | }, 13 | "scripts": { 14 | "clean": "rm -rf node_modules package-lock.json yarn.lock", 15 | "build": "tsc", 16 | "watch": "tsc -w", 17 | "test": "export overrideWarningsEnabled=false && jest --coverage", 18 | "cdk": "cdk" 19 | }, 20 | "devDependencies": { 21 | "@aws-cdk/assertions": "^1.204.0", 22 | "@aws-cdk/aws-servicecatalogappregistry-alpha": "^2.88.0-alpha.0", 23 | "@aws-solutions-constructs/aws-cloudfront-s3": "2.41.0", 24 | "@aws-solutions-constructs/aws-iot-sqs": "2.41.0", 25 | "@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3": "2.41.0", 26 | "@aws-solutions-constructs/aws-kinesisstreams-lambda": "2.41.0", 27 | "@aws-solutions-constructs/aws-lambda-dynamodb": "2.41.0", 28 | "@aws-solutions-constructs/aws-sqs-lambda": "2.41.0", 29 | "@types/jest": "^29.5.3", 30 | "@types/node": "^20.4.2", 31 | "@types/uuid": "^9.0.2", 32 | "aws-cdk": "2.88.0", 33 | "aws-cdk-lib": "2.88.0", 34 | "cdk-nag": "2.27.76", 35 | "constructs": "^10.2.69", 36 | "jest": "^29.6.1", 37 | "ts-jest": "^29.1.1", 38 | "ts-node": "^10.9.1", 39 | "typescript": "~5.1.6", 40 | "uuid": "^9.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /source/infrastructure/test/api.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Stack, aws_lambda as lambda, aws_s3 as s3 } from 'aws-cdk-lib'; 5 | import { ApiConstruct } from '../lib/backend/api'; 6 | 7 | test('M2C2 API test', () => { 8 | const stack = new Stack(); 9 | const connectionBuilderLambdaFunction = new lambda.Function(stack, 'TestConnectionBuilder', { 10 | code: lambda.Code.fromBucket( 11 | s3.Bucket.fromBucketName(stack, 'SourceCodeBucket', 'test-bucket'), 12 | 'connection-builder.zip' 13 | ), 14 | handler: 'connection-builder/index.handler', 15 | runtime: lambda.Runtime.NODEJS_18_X 16 | }); 17 | 18 | const api = new ApiConstruct(stack, 'TestApi', { connectionBuilderLambdaFunction, corsOrigin: '*' }); 19 | expect(api.apiEndpoint).toBeDefined(); 20 | expect(api.apiId).toBeDefined(); 21 | }); 22 | -------------------------------------------------------------------------------- /source/infrastructure/test/app-registry.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import * as cdk from 'aws-cdk-lib'; 5 | import * as crypto from 'crypto'; 6 | 7 | import { Capture, Template } from 'aws-cdk-lib/assertions'; 8 | 9 | import { AppRegistry } from '../lib/app-registry/app-registry'; 10 | 11 | describe('When a generic stack is registered with AppRegistry', () => { 12 | let template: Template; 13 | let app: cdk.App; 14 | let stack: cdk.Stack; 15 | const fakeSolutionId = 'SO999'; 16 | 17 | beforeAll(() => { 18 | app = new cdk.App(); 19 | stack = new cdk.Stack(app, 'TestStack'); 20 | 21 | const hash = crypto.createHash('sha256').update(stack.stackName).digest('hex'); 22 | cdk.Aspects.of(stack).add( 23 | new AppRegistry(stack, `AppRegistry-${hash}`, { 24 | solutionID: fakeSolutionId 25 | }) 26 | ); 27 | template = Template.fromStack(stack); 28 | }); 29 | 30 | it('Should create a ServiceCatalog Application', () => { 31 | template.resourceCountIs('AWS::ServiceCatalogAppRegistry::Application', 1); 32 | template.hasResourceProperties('AWS::ServiceCatalogAppRegistry::Application', { 33 | Name: { 34 | 'Fn::Join': ['-', ['App', { Ref: 'AWS::StackName' }, '%%APP_REG_NAME%%']] 35 | }, 36 | Description: 37 | 'Service Catalog application to track and manage all your resources for the solution %%SOLUTION_NAME%%', 38 | Tags: { 39 | 'Solutions:ApplicationType': 'AWS-Solutions', 40 | 'Solutions:SolutionID': fakeSolutionId, 41 | 'Solutions:SolutionName': '%%SOLUTION_NAME%%', 42 | 'Solutions:SolutionVersion': '%%VERSION%%' 43 | } 44 | }); 45 | }); 46 | 47 | it('Should have AttributeGroupAssociation', () => { 48 | const attGrpCapture = new Capture(); 49 | template.resourceCountIs('AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation', 1); 50 | template.hasResourceProperties('AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation', { 51 | Application: { 52 | 'Fn::GetAtt': [attGrpCapture, 'Id'] 53 | }, 54 | AttributeGroup: { 55 | 'Fn::GetAtt': [attGrpCapture, 'Id'] 56 | } 57 | }); 58 | attGrpCapture.next(); 59 | expect(template.toJSON()['Resources'][attGrpCapture.asString()]['Type']).toStrictEqual( 60 | 'AWS::ServiceCatalogAppRegistry::AttributeGroup' 61 | ); 62 | }); 63 | 64 | it('should have AttributeGroup', () => { 65 | template.resourceCountIs('AWS::ServiceCatalogAppRegistry::AttributeGroup', 1); 66 | template.hasResourceProperties('AWS::ServiceCatalogAppRegistry::AttributeGroup', { 67 | Attributes: { 68 | applicationType: 'AWS-Solutions', 69 | version: '%%VERSION%%', 70 | solutionID: fakeSolutionId, 71 | solutionName: '%%SOLUTION_NAME%%' 72 | } 73 | }); 74 | }); 75 | 76 | it('Should not have a ResourceAssociation for a nested stack', () => { 77 | template.resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /source/infrastructure/test/aspects.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Match, Template } from 'aws-cdk-lib/assertions'; 5 | import { Aspects, CfnCondition, Stack, aws_lambda as lambda } from 'aws-cdk-lib'; 6 | import { ConditionAspect } from '../utils/aspects'; 7 | 8 | describe('ConditionAspect', () => { 9 | test('Add condition to all resources', () => { 10 | const stack = new Stack(); 11 | const condition = new CfnCondition(stack, 'TestCondition'); 12 | const lambdaFunctions: lambda.Function[] = []; 13 | lambdaFunctions.push( 14 | new lambda.Function(stack, 'TestLambdaFunction', { 15 | code: lambda.Code.fromInline(`console.log('Hello world!');`), 16 | handler: 'index.handler', 17 | runtime: lambda.Runtime.NODEJS_18_X 18 | }) 19 | ); 20 | Aspects.of(stack).add(new ConditionAspect(condition)); 21 | 22 | Template.fromStack(stack).hasResource('AWS::Lambda::Function', { 23 | Condition: Match.stringLikeRegexp('TestCondition') 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /source/infrastructure/test/cloudfront.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { CfnCondition, Stack, aws_s3 as s3 } from 'aws-cdk-lib'; 6 | import { CloudFrontConstruct } from '../lib/frontend/cloudfront'; 7 | 8 | test('M2C2 Cloudfront test', () => { 9 | const stack = new Stack(); 10 | const s3LoggingBucket = new s3.Bucket(stack, 'TestLoggingBucket'); 11 | (s3LoggingBucket.node.defaultChild).overrideLogicalId('TestLoggingBucket'); 12 | const resourceBucket = new s3.Bucket(stack, 'TestGreengrassResourceBucket'); 13 | (resourceBucket.node.defaultChild).overrideLogicalId('TestGreengrassResourceBucket'); 14 | 15 | const cf = new CloudFrontConstruct(stack, 'TestCF', { 16 | s3LoggingBucket: s3LoggingBucket, 17 | customResourcesFunctionArn: 'test-arn', 18 | shouldTeardownData: new CfnCondition(stack, 'TestCondition') 19 | }); 20 | 21 | expect(cf.uiBucket).toBeDefined(); 22 | expect(cf.cloudFrontDomainName).toBeDefined(); 23 | Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { 24 | LoggingConfiguration: { 25 | DestinationBucketName: { 26 | Ref: 'TestLoggingBucket' 27 | }, 28 | LogFilePrefix: 'ui-s3/' 29 | } 30 | }); 31 | Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::Distribution', { 32 | DistributionConfig: { 33 | Logging: { 34 | Bucket: { 35 | 'Fn::GetAtt': ['TestLoggingBucket', 'RegionalDomainName'] 36 | }, 37 | Prefix: 'ui-cf/' 38 | } 39 | } 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /source/infrastructure/test/cloudwatch-logs-policy.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Stack } from 'aws-cdk-lib'; 5 | import { CloudwatchLogsPolicyConstruct } from '../lib/common-resource/cloudwatch-logs-policy'; 6 | 7 | test('M2C2 cloudwatch logs policy test', () => { 8 | const stack = new Stack(); 9 | const cloudwatchLogsPolicy = new CloudwatchLogsPolicyConstruct(stack, 'TestCloudWatchLogsPolicy'); 10 | 11 | expect(cloudwatchLogsPolicy.policy).toBeDefined(); 12 | }); 13 | -------------------------------------------------------------------------------- /source/infrastructure/test/connection-builder.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { Stack, aws_iam as iam, aws_s3 as s3 } from 'aws-cdk-lib'; 6 | import { ConnectionBuilderConstruct } from '../lib/backend/connection-builder'; 7 | 8 | test('M2C2 connection builder test', () => { 9 | const stack = new Stack(); 10 | const cloudWatchLogsPolicy = new iam.PolicyDocument({ 11 | statements: [ 12 | new iam.PolicyStatement({ 13 | effect: iam.Effect.ALLOW, 14 | resources: ['logs:*'], 15 | actions: ['*'] 16 | }) 17 | ] 18 | }); 19 | 20 | const greengrassResourceBucket = s3.Bucket.fromBucketName(stack, 'GreengrassResourceBucket', 'greengrass-bucket'); 21 | const iotCertificateArn = 'arn:of:certificate'; 22 | const iotEndpointAddress = 'https://iot.amazonaws.com'; 23 | const kinesisStreamName = 'test-kinesis-stream'; 24 | const kinesisStreamForTimestreamName = 'test-kinesis-stream-for-timestream'; 25 | const loggingLevel = 'ERROR'; 26 | const logsTableArn = 'arn:of:logs:dynamodb:table'; 27 | const logsTableName = 'test-logs-table'; 28 | const collectorId = 'test-collector-id'; 29 | const sendAnonymousUsage = 'Yes'; 30 | const solutionId = 'SO0070-Test'; 31 | const solutionVersion = 'v0.0.1-test'; 32 | const sourceCodeBucket = s3.Bucket.fromBucketName(stack, 'SourceCodeBucket', 'test-bucket-region'); 33 | const sourceCodePrefix = 'v0.0.1-test/machine-to-cloud-connectivity-framework'; 34 | const uuid = 'test-uuid'; 35 | 36 | const connectionBuilder = new ConnectionBuilderConstruct(stack, 'TestConnectionBuilder', { 37 | cloudWatchLogsPolicy, 38 | greengrassResourceBucket, 39 | iotCertificateArn, 40 | iotEndpointAddress, 41 | kinesisStreamName, 42 | kinesisStreamForTimestreamName, 43 | logsTableArn, 44 | logsTableName, 45 | collectorId, 46 | solutionConfig: { 47 | loggingLevel, 48 | sendAnonymousUsage, 49 | solutionId, 50 | solutionVersion, 51 | sourceCodeBucket, 52 | sourceCodePrefix, 53 | uuid 54 | } 55 | }); 56 | 57 | expect(connectionBuilder.connectionBuilderLambdaFunction).toBeDefined(); 58 | expect(connectionBuilder.connectionTableName).toBeDefined(); 59 | Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { 60 | KeySchema: [ 61 | { 62 | AttributeName: 'connectionName', 63 | KeyType: 'HASH' 64 | } 65 | ] 66 | }); 67 | Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { 68 | KeySchema: [ 69 | { 70 | AttributeName: 'name', 71 | KeyType: 'HASH' 72 | } 73 | ] 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /source/infrastructure/test/data-stream.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { Stack, CfnCondition, aws_s3 as s3 } from 'aws-cdk-lib'; 6 | import { KinesisDataStreamConstruct } from '../lib/data-flow/kinesis-data-stream'; 7 | 8 | test('M2C2 data stream test', () => { 9 | const stack = new Stack(); 10 | const s3LoggingBucket = new s3.Bucket(stack, 'TestLoggingBucket'); 11 | (s3LoggingBucket.node.defaultChild).overrideLogicalId('TestLoggingBucket'); 12 | const kinesisDataStream = new KinesisDataStreamConstruct(stack, 'TestKinesis', { 13 | s3LoggingBucket, 14 | customResourcesFunctionArn: '', 15 | shouldTeardownData: new CfnCondition(stack, 'TestCondition'), 16 | shouldCreateKinesisResources: new CfnCondition(stack, 'TestCreateKinesisCondition') 17 | }); 18 | 19 | expect(kinesisDataStream.kinesisStreamName).toBeDefined(); 20 | expect(kinesisDataStream.dataBucketName).toBeDefined(); 21 | Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { 22 | LoggingConfiguration: { 23 | DestinationBucketName: { 24 | Ref: 'TestLoggingBucket' 25 | }, 26 | LogFilePrefix: 'm2c2data/' 27 | } 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /source/infrastructure/test/greengrass.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { CfnCondition, Stack, aws_s3 as s3 } from 'aws-cdk-lib'; 5 | import { GreengrassConstruct } from '../lib/greengrass/greengrass'; 6 | 7 | test('M2C2 greengrass resource creation test', () => { 8 | const stack = new Stack(); 9 | const s3LoggingBucket = new s3.Bucket(stack, 'TestLoggingBucket'); 10 | (s3LoggingBucket.node.defaultChild).overrideLogicalId('TestLoggingBucket'); 11 | const greengrass = new GreengrassConstruct(stack, 'TestGreengrass', { 12 | kinesisStreamName: 'TestStream', 13 | s3LoggingBucket, 14 | solutionConfig: { 15 | solutionId: 'SO0070-Test', 16 | solutionVersion: 'v0.0.1-test', 17 | uuid: 'test-uuid' 18 | }, 19 | timestreamKinesisStreamArn: 'arn:of:timestream:kinesis:stream', 20 | customResourcesFunctionArn: 'test-arn', 21 | shouldTeardownData: new CfnCondition(stack, 'TestCondition') 22 | }); 23 | 24 | expect(greengrass.greengrassResourceBucket).toBeDefined(); 25 | expect(greengrass.iotCredentialsRoleArn).toBeDefined(); 26 | expect(greengrass.iotPolicyName).toBeDefined(); 27 | expect(greengrass.iotRoleAliasName).toBeDefined(); 28 | }); 29 | -------------------------------------------------------------------------------- /source/infrastructure/test/kinesis-data-stream.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { Stack, CfnCondition, aws_s3 as s3 } from 'aws-cdk-lib'; 6 | import { KinesisDataStreamConstruct } from '../lib/data-flow/kinesis-data-stream'; 7 | 8 | test('M2C2 data stream test', () => { 9 | const stack = new Stack(); 10 | const s3LoggingBucket = new s3.Bucket(stack, 'TestLoggingBucket'); 11 | (s3LoggingBucket.node.defaultChild).overrideLogicalId('TestLoggingBucket'); 12 | const dataStream = new KinesisDataStreamConstruct(stack, 'TestKinesisDataStream', { 13 | s3LoggingBucket, 14 | customResourcesFunctionArn: '', 15 | shouldTeardownData: new CfnCondition(stack, 'TestCondition'), 16 | shouldCreateKinesisResources: new CfnCondition(stack, 'TestCreateKinesisCondition') 17 | }); 18 | 19 | expect(dataStream.kinesisStreamName).toBeDefined(); 20 | expect(dataStream.dataBucketName).toBeDefined(); 21 | Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { 22 | LoggingConfiguration: { 23 | DestinationBucketName: { 24 | Ref: 'TestLoggingBucket' 25 | }, 26 | LogFilePrefix: 'm2c2data/' 27 | } 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /source/infrastructure/test/logging-bucket.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Stack } from 'aws-cdk-lib'; 5 | import { LoggingBucketConstruct } from '../lib/common-resource/logging-bucket'; 6 | 7 | test('M2C2 logging bucket test', () => { 8 | const stack = new Stack(); 9 | const loggingBucket = new LoggingBucketConstruct(stack, 'LoggingBucket'); 10 | 11 | expect(loggingBucket.s3LoggingBucket).toBeDefined(); 12 | }); 13 | -------------------------------------------------------------------------------- /source/infrastructure/test/machine-to-cloud-connectivity.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { App } from 'aws-cdk-lib'; 5 | import { MachineToCloudConnectivityFrameworkStack } from '../lib/machine-to-cloud-connectivity-stack'; 6 | jest.mock('uuid', () => ({ v4: () => '123456789' })); 7 | 8 | test('M2C2 stack test', () => { 9 | const solutionId = 'SO0070'; 10 | const solutionBucketName = 'test-bucket'; 11 | const solutionName = 'machine-to-cloud-connectivity-framework'; 12 | const solutionVersion = 'vTest'; 13 | const shouldTeardownDataOnDestroy = 'No'; 14 | const shouldSendAnonymousMetrics = 'Yes'; 15 | 16 | const app = new App(); 17 | const stack = new MachineToCloudConnectivityFrameworkStack(app, 'TestStack', { 18 | description: `(${solutionId}) - ${solutionName} Version ${solutionVersion}`, 19 | solutionBucketName, 20 | solutionId, 21 | solutionName, 22 | solutionVersion, 23 | shouldSendAnonymousMetrics, 24 | shouldTeardownDataOnDestroy 25 | }); 26 | 27 | expect(stack).toBeDefined(); 28 | }); 29 | -------------------------------------------------------------------------------- /source/infrastructure/test/source-bucket.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Stack } from 'aws-cdk-lib'; 5 | import { SourceBucketConstruct } from '../lib/common-resource/source-bucket'; 6 | 7 | test('M2C2 source bucket test', () => { 8 | const stack = new Stack(); 9 | const sourceBucket = new SourceBucketConstruct(stack, 'LoggingBucket', { 10 | sourceCodeBucketName: 'test-name' 11 | }); 12 | 13 | expect(sourceBucket.sourceCodeBucket).toBeDefined(); 14 | }); 15 | -------------------------------------------------------------------------------- /source/infrastructure/test/sqs-message-consumer.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { Stack, aws_s3 as s3 } from 'aws-cdk-lib'; 6 | import { SQSMessageConsumerConstruct } from '../lib/data-flow/sqs-message-consumer'; 7 | 8 | test('M2C2 SQS message consumer test', () => { 9 | const stack = new Stack(); 10 | const sqsMessageConsumer = new SQSMessageConsumerConstruct(stack, 'TestSQSMessageConsumer', { 11 | solutionConfig: { 12 | loggingLevel: 'ERROR', 13 | solutionId: 'SO0070-Test', 14 | solutionVersion: 'v0.0.1-test', 15 | sourceCodeBucket: s3.Bucket.fromBucketName(stack, 'SourceCodeBucket', 'test-bucket-region'), 16 | sourceCodePrefix: 'v0.0.1-test/machine-to-cloud-connectivity-framework' 17 | } 18 | }); 19 | 20 | expect(sqsMessageConsumer.logsTable).toBeDefined(); 21 | Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', { 22 | KeySchema: [ 23 | { 24 | AttributeName: 'connectionName', 25 | KeyType: 'HASH' 26 | }, 27 | { 28 | AttributeName: 'timestamp', 29 | KeyType: 'RANGE' 30 | } 31 | ], 32 | TimeToLiveSpecification: { 33 | AttributeName: 'ttl', 34 | Enabled: true 35 | } 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /source/infrastructure/test/ui.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { Stack, aws_s3 as s3 } from 'aws-cdk-lib'; 6 | import { UiConstruct } from '../lib/frontend/ui'; 7 | 8 | test('M2C2 UI test', () => { 9 | const stack = new Stack(); 10 | const s3LoggingBucket = new s3.Bucket(stack, 'TestLoggingBucket'); 11 | (s3LoggingBucket.node.defaultChild).overrideLogicalId('TestLoggingBucket'); 12 | const resourceBucket = new s3.Bucket(stack, 'TestGreengrassResourceBucket'); 13 | (resourceBucket.node.defaultChild).overrideLogicalId('TestGreengrassResourceBucket'); 14 | 15 | const ui = new UiConstruct(stack, 'TestUi', { 16 | apiId: 'mock-id', 17 | resourceBucket, 18 | userEmail: 'mockmail', 19 | cloudFrontDomainName: 'test-domain-name' 20 | }); 21 | 22 | expect(ui.identityPoolId).toBeDefined(); 23 | expect(ui.userPoolId).toBeDefined(); 24 | expect(ui.webClientId).toBeDefined(); 25 | Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { 26 | CorsConfiguration: { 27 | CorsRules: [ 28 | { 29 | AllowedHeaders: ['*'], 30 | AllowedMethods: ['GET'], 31 | AllowedOrigins: ['https://test-domain-name'], 32 | ExposedHeaders: ['ETag'] 33 | } 34 | ] 35 | } 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /source/infrastructure/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2018" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ], 25 | "resolveJsonModule": true 26 | }, 27 | "exclude": [ 28 | "cdk.out" 29 | ], 30 | "include": [ 31 | "**/*.ts", 32 | "jest.config.js" 33 | ], 34 | } -------------------------------------------------------------------------------- /source/infrastructure/utils/aspects.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { CfnCondition, CfnResource, IAspect } from 'aws-cdk-lib'; 5 | import { IConstruct } from 'constructs'; 6 | 7 | /** 8 | * CDK Aspect implementation to set up conditions to the entire Construct resources 9 | */ 10 | export class ConditionAspect implements IAspect { 11 | private readonly condition: CfnCondition; 12 | 13 | constructor(condition: CfnCondition) { 14 | this.condition = condition; 15 | } 16 | 17 | visit(node: IConstruct): void { 18 | const resource = node; 19 | if (resource.cfnOptions) { 20 | resource.cfnOptions.condition = this.condition; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /source/infrastructure/utils/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { CfnCondition, CfnOutput, CfnResource, Resource, Stack } from 'aws-cdk-lib'; 5 | 6 | interface CfnNagSuppressRule { 7 | id: string; 8 | reason: string; 9 | } 10 | 11 | /** 12 | * Adds CFN NAG suppress rules to the CDK resource. 13 | * @param resource The CDK resource 14 | * @param rules The CFN NAG suppress rules 15 | */ 16 | export function addCfnSuppressRules(resource: Resource | CfnResource, rules: CfnNagSuppressRule[]): void { 17 | if (resource instanceof Resource) { 18 | resource = resource.node.defaultChild; 19 | } 20 | 21 | const cfnNagMetadata = resource.getMetadata('cfn_nag'); 22 | 23 | if (cfnNagMetadata) { 24 | const existingRules = cfnNagMetadata.rules_to_suppress; 25 | 26 | if (Array.isArray(existingRules)) { 27 | for (const rule of existingRules) { 28 | if (typeof rules.find(newRule => newRule.id === rule.id) === 'undefined') { 29 | rules.push(rule); 30 | } 31 | } 32 | } 33 | } 34 | 35 | resource.addMetadata('cfn_nag', { 36 | rules_to_suppress: rules 37 | }); 38 | } 39 | 40 | interface AddOutputRequest { 41 | id: string; 42 | description: string; 43 | value: string; 44 | condition?: CfnCondition; 45 | } 46 | 47 | /** 48 | * Adds outputs to the CloudFormation template. 49 | * @param stack The CDK stack 50 | * @param outputRequests The CloudFormation outputs 51 | */ 52 | export function addOutputs(stack: Stack, outputRequests: AddOutputRequest[]): void { 53 | for (const request of outputRequests) { 54 | const { id, description, value, condition } = request; 55 | 56 | new CfnOutput(stack, id, { 57 | description, 58 | value, 59 | condition 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /source/lambda/connection-builder/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { handleGetRequest } from './api-get-request'; 5 | import { handlePostRequest } from './api-post-request'; 6 | import { LambdaError } from '../lib/errors'; 7 | import Logger, { LoggingLevel } from '../lib/logger'; 8 | import { APIGatewayRequest, APIGatewayResponse, APIResponseBodyType } from '../lib/types/connection-builder-types'; 9 | 10 | const { API_ENDPOINT, LOGGING_LEVEL } = process.env; 11 | const logger = new Logger('connection-builder', LOGGING_LEVEL); 12 | 13 | /** 14 | * The Lambda function deals with API requests and returns the response to the API Gateway. 15 | * @param event The request from the API Gateway 16 | * @returns The response to the API Gateway 17 | */ 18 | export async function handler(event: APIGatewayRequest): Promise { 19 | const response: APIGatewayResponse = { 20 | headers: { 21 | 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept', 22 | 'Access-Control-Allow-Methods': 'OPTIONS,POST,GET', 23 | 'Access-Control-Allow-Origin': '*' 24 | }, 25 | statusCode: 200, 26 | body: JSON.stringify({}) 27 | }; 28 | 29 | try { 30 | logger.log(LoggingLevel.INFO, `Request: ${JSON.stringify(event, null, 2)}`); 31 | 32 | const { body, headers, httpMethod, path, pathParameters, queryStringParameters, resource } = event; 33 | 34 | if (!headers || !headers.Host || headers.Host !== API_ENDPOINT) { 35 | throw new LambdaError({ 36 | message: 'Invalid Host header', 37 | name: 'ConnectionBuilderError', 38 | statusCode: 400 39 | }); 40 | } 41 | 42 | const queryStrings = queryStringParameters || {}; 43 | let result: APIResponseBodyType; 44 | 45 | switch (httpMethod) { 46 | case 'GET': 47 | result = await handleGetRequest({ 48 | path, 49 | pathParameters, 50 | queryStrings, 51 | resource 52 | }); 53 | 54 | break; 55 | case 'POST': 56 | result = await handlePostRequest({ 57 | body, 58 | resource 59 | }); 60 | 61 | break; 62 | default: 63 | throw new LambdaError({ 64 | message: `Not supported http method: ${httpMethod}`, 65 | name: 'ConnectionBuilderError', 66 | statusCode: 405 67 | }); 68 | } 69 | 70 | response.body = JSON.stringify(result); 71 | } catch (error) { 72 | logger.log(LoggingLevel.ERROR, 'Error occurred: ', error); 73 | 74 | /** 75 | * When an error happens, unless the error is controlled by `LambdaError`, 76 | * it sanitizes the error message to "Internal service error.". 77 | */ 78 | response.statusCode = error instanceof LambdaError ? error.statusCode : 500; 79 | response.body = JSON.stringify({ 80 | errorMessage: error instanceof LambdaError ? error.message : 'Internal service error.' 81 | }); 82 | } 83 | 84 | return response; 85 | } 86 | -------------------------------------------------------------------------------- /source/lambda/connection-builder/jest.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | preset: 'ts-jest', 6 | roots: ['/test'], 7 | transform: { 8 | '^.+\\.(ts|tsx)?$': 'ts-jest' 9 | }, 10 | setupFiles: ['./test/jest-environment-variables.ts'], 11 | collectCoverageFrom: ['**/*.ts', '!**/test/*.ts'], 12 | coverageReporters: ['text', ['lcov', { projectRoot: '../../' }]] 13 | }; 14 | -------------------------------------------------------------------------------- /source/lambda/connection-builder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connection-builder", 3 | "version": "4.2.3", 4 | "description": "The function creates a connection and deploys the connection into the Greengrass edge device.", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf node_modules dist coverage package-lock.json yarn.lock", 8 | "compile": "node_modules/typescript/bin/tsc --project tsconfig.json", 9 | "build": "yarn clean && yarn install && yarn compile", 10 | "copy-modules": "yarn install --production --ignore-scripts --prefer-offline && rsync -avrq ./node_modules ./dist", 11 | "package": "yarn build && yarn copy-modules && cd dist && zip -q -r9 package.zip * -x '**/test/*' && cd ..", 12 | "test": "jest --coverage --silent" 13 | }, 14 | "dependencies": { 15 | "@paralleldrive/cuid2": "^2.2.1", 16 | "aws-sdk": "2.1386.0", 17 | "axios": "~1.4.0" 18 | }, 19 | "devDependencies": { 20 | "@types/jest": "^29.5.3", 21 | "@types/node": "^20.4.5", 22 | "jest": "^29.6.1", 23 | "ts-jest": "^29.1.1", 24 | "ts-node": "^10.9.1", 25 | "typescript": "~5.1.6" 26 | }, 27 | "author": { 28 | "name": "Amazon Web Services", 29 | "url": "https://aws.amazon.com/solutions" 30 | }, 31 | "license": "Apache-2.0" 32 | } 33 | -------------------------------------------------------------------------------- /source/lambda/connection-builder/test/jest-environment-variables.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | process.env = { 5 | API_ENDPOINT: 'api.endpoint', 6 | GREENGRASS_RESOURCE_BUCKET: 'mock-greengrass-bucket', 7 | IOT_CERTIFICATE_ARN: 'arn:of:iot:certificate', 8 | LOGGING_LEVEL: 'ERROR', 9 | SEND_ANONYMOUS_METRIC: 'Yes', 10 | SOLUTION_UUID: 'mock-uuid' 11 | }; 12 | -------------------------------------------------------------------------------- /source/lambda/connection-builder/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "emitDecoratorMetadata": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "jest.config.js" 14 | ], 15 | "exclude": [ 16 | "node_modules", 17 | "package" 18 | ] 19 | } -------------------------------------------------------------------------------- /source/lambda/custom-resource/jest.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | preset: 'ts-jest', 6 | roots: ['/test'], 7 | transform: { 8 | '^.+\\.(ts|tsx)?$': 'ts-jest' 9 | }, 10 | setupFiles: ['./test/jest-environment-variables.ts'], 11 | collectCoverageFrom: ['**/*.ts', '!**/test/*.ts'], 12 | coverageReporters: ['text', ['lcov', { projectRoot: '../../' }]] 13 | }; 14 | -------------------------------------------------------------------------------- /source/lambda/custom-resource/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-resource", 3 | "version": "4.2.3", 4 | "description": "The solution's custom resource function", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf node_modules dist coverage package-lock.json yarn.lock", 8 | "compile": "node_modules/typescript/bin/tsc --project tsconfig.json", 9 | "build": "yarn clean && yarn install && yarn compile", 10 | "copy-modules": "yarn install --production --ignore-scripts --prefer-offline && rsync -avrq ./node_modules ./dist", 11 | "copy-script": "cp -rf ./greengrass-custom-resources/script ./dist/custom-resource/greengrass-custom-resources/", 12 | "package": "yarn build && yarn copy-modules && yarn copy-script && cd dist && zip -q -r9 package.zip * -x '**/test/*' && cd ..", 13 | "test": "jest --coverage --silent" 14 | }, 15 | "dependencies": { 16 | "@paralleldrive/cuid2": "^2.2.1", 17 | "aws-sdk": "2.1386.0", 18 | "axios": "~1.4.0", 19 | "uuid": "~9.0.0" 20 | }, 21 | "devDependencies": { 22 | "@types/jest": "^29.5.3", 23 | "@types/node": "^20.4.5", 24 | "@types/uuid": "^9.0.2", 25 | "jest": "^29.6.1", 26 | "ts-jest": "^29.1.1", 27 | "ts-node": "^10.9.1", 28 | "typescript": "~5.1.6" 29 | }, 30 | "engines": { 31 | "node": ">=18.0.0" 32 | }, 33 | "author": { 34 | "name": "Amazon Web Services", 35 | "url": "https://aws.amazon.com/solutions" 36 | }, 37 | "license": "Apache-2.0" 38 | } 39 | -------------------------------------------------------------------------------- /source/lambda/custom-resource/test/invalid-inputs.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { buildResponseBody, consoleErrorSpy, mockAxios, mockValues } from './mock'; 5 | import { handler } from '../index'; 6 | import { LambdaError } from '../../lib/errors'; 7 | import { StatusTypes } from '../../lib/types/custom-resource-types'; 8 | 9 | const { axiosConfig, context, event } = mockValues; 10 | 11 | beforeAll(() => { 12 | consoleErrorSpy.mockReset(); 13 | mockAxios.put.mockReset(); 14 | }); 15 | 16 | test('Test failure when Resource is not supported', async () => { 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | event.ResourceProperties.Resource = 'Invalid' as any; 19 | const errorMessage = `Not supported custom resource type: ${event.ResourceProperties.Resource}`; 20 | 21 | mockAxios.put.mockResolvedValueOnce({ status: 200 }); 22 | 23 | const response = await handler(event, context); 24 | const responseBody = buildResponseBody({ 25 | event, 26 | response, 27 | reason: errorMessage 28 | }); 29 | axiosConfig.headers['Content-Length'] = `${responseBody.length}`; 30 | 31 | expect(response).toEqual({ 32 | Status: StatusTypes.FAILED, 33 | Data: { 34 | Error: errorMessage 35 | } 36 | }); 37 | expect(consoleErrorSpy).toHaveBeenCalledTimes(1); 38 | expect(consoleErrorSpy).toHaveBeenCalledWith( 39 | '[custom-resource]', 40 | 'Error: ', 41 | new LambdaError({ 42 | message: errorMessage, 43 | name: 'NotSupportedCustomResourceType', 44 | statusCode: 400 45 | }) 46 | ); 47 | expect(mockAxios.put).toHaveBeenCalledTimes(1); 48 | expect(mockAxios.put).toHaveBeenCalledWith(event.ResponseURL, responseBody, axiosConfig); 49 | }); 50 | -------------------------------------------------------------------------------- /source/lambda/custom-resource/test/jest-environment-variables.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | process.env.AWS_REGION = 'mock-region-1'; 5 | process.env.LOGGING_LEVEL = 'WARN'; 6 | -------------------------------------------------------------------------------- /source/lambda/custom-resource/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "emitDecoratorMetadata": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "jest.config.js" 14 | ], 15 | "exclude": [ 16 | "node_modules", 17 | "package" 18 | ] 19 | } -------------------------------------------------------------------------------- /source/lambda/greengrass-deployer/jest.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | preset: 'ts-jest', 6 | roots: ['/test'], 7 | transform: { 8 | '^.+\\.(ts|tsx)?$': 'ts-jest' 9 | }, 10 | setupFiles: ['./test/jest-environment-variables.ts'], 11 | collectCoverageFrom: ['**/*.ts', '!**/test/*.ts'], 12 | coverageReporters: ['text', ['lcov', { projectRoot: '../../' }]], 13 | testTimeout: 20000 14 | }; 15 | -------------------------------------------------------------------------------- /source/lambda/greengrass-deployer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "greengrass-deployer", 3 | "version": "4.2.3", 4 | "description": "The function deploys the Greengrass group and restarting connections.", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf node_modules dist coverage package-lock.json yarn.lock", 8 | "compile": "node_modules/typescript/bin/tsc --project tsconfig.json", 9 | "build": "yarn clean && yarn install && yarn compile", 10 | "copy-modules": "yarn install --production --ignore-scripts --prefer-offline && rsync -avrq ./node_modules ./dist", 11 | "package": "yarn build && yarn copy-modules && cd dist && zip -q -r9 package.zip * -x '**/test/*' && cd ..", 12 | "test": "jest --coverage --silent" 13 | }, 14 | "dependencies": { 15 | "@paralleldrive/cuid2": "^2.2.1", 16 | "aws-sdk": "2.1386.0", 17 | "axios": "~1.4.0" 18 | }, 19 | "devDependencies": { 20 | "@types/jest": "^29.5.3", 21 | "@types/node": "^20.4.5", 22 | "jest": "^29.6.1", 23 | "ts-jest": "^29.1.1", 24 | "ts-node": "^10.9.1", 25 | "typescript": "~5.1.6" 26 | }, 27 | "author": { 28 | "name": "Amazon Web Services", 29 | "url": "https://aws.amazon.com/solutions" 30 | }, 31 | "license": "Apache-2.0" 32 | } 33 | -------------------------------------------------------------------------------- /source/lambda/greengrass-deployer/test/invalid-inputs.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { mockDynamoDbHandler, mockValues } from './mock'; 5 | import { handler } from '../index'; 6 | import { LambdaError } from '../../lib/errors'; 7 | import { ConnectionDefinition, MachineProtocol } from '../../lib/types/solution-common-types'; 8 | 9 | beforeEach(() => mockDynamoDbHandler.getGreengrassCoreDevice.mockReset()); 10 | 11 | test('Test invalid control', async () => { 12 | mockDynamoDbHandler.getGreengrassCoreDevice.mockResolvedValue({ 13 | iotSiteWiseGatewayId: 'mock-gateway-id', 14 | iotThingArn: 'arn:of:thing' 15 | }); 16 | 17 | const invalidControl = 'invalid'; 18 | const event: ConnectionDefinition = { 19 | connectionName: mockValues.connectionName, 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | control: invalidControl, 22 | greengrassCoreDeviceName: 'mock-core-device', 23 | protocol: MachineProtocol.OPCDA 24 | }; 25 | 26 | await expect(handler(event)).rejects.toEqual( 27 | new LambdaError({ 28 | message: `Unsupported connection control ${invalidControl}.`, 29 | name: 'GreengrassDeployerError' 30 | }) 31 | ); 32 | expect(mockDynamoDbHandler.getGreengrassCoreDevice).toHaveBeenCalledTimes(1); 33 | expect(mockDynamoDbHandler.getGreengrassCoreDevice).toHaveBeenCalledWith(event.greengrassCoreDeviceName); 34 | }); 35 | 36 | test('Test failure of getting Greengrass core device', async () => { 37 | mockDynamoDbHandler.getGreengrassCoreDevice.mockRejectedValue('Failure'); 38 | 39 | const invalidControl = 'invalid'; 40 | const event: ConnectionDefinition = { 41 | connectionName: mockValues.connectionName, 42 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 43 | control: invalidControl, 44 | greengrassCoreDeviceName: 'mock-core-device', 45 | protocol: MachineProtocol.OPCDA 46 | }; 47 | 48 | await expect(handler(event)).rejects.toEqual('Failure'); 49 | expect(mockDynamoDbHandler.getGreengrassCoreDevice).toHaveBeenCalledTimes(1); 50 | expect(mockDynamoDbHandler.getGreengrassCoreDevice).toHaveBeenCalledWith(event.greengrassCoreDeviceName); 51 | }); 52 | -------------------------------------------------------------------------------- /source/lambda/greengrass-deployer/test/jest-environment-variables.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | process.env.KINESIS_STREAM = 'mock-kinesis-stream'; 5 | process.env.LOGGING_LEVEL = 'ERROR'; 6 | process.env.SEND_ANONYMOUS_METRIC = 'Yes'; 7 | process.env.SOLUTION_UUID = 'mock-uuid'; 8 | -------------------------------------------------------------------------------- /source/lambda/greengrass-deployer/test/no-send-anonymous-metrics.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { 5 | mockDynamoDbHandler, 6 | mockGreengrassV2Handler, 7 | mockIoTHandler, 8 | mockIoTSiteWiseHandler, 9 | mockValues, 10 | sendAnonymousMetricsSpy, 11 | sleepSpy 12 | } from './mock'; 13 | import { handler } from '../index'; 14 | import { ConnectionControl, ConnectionDefinition, MachineProtocol } from '../../lib/types/solution-common-types'; 15 | 16 | const event: ConnectionDefinition = { 17 | connectionName: mockValues.connectionName, 18 | control: ConnectionControl.DELETE, 19 | protocol: MachineProtocol.OPCDA 20 | }; 21 | 22 | beforeAll(() => { 23 | process.env.SEND_ANONYMOUS_METRIC = 'No'; 24 | mockDynamoDbHandler.deleteConnection.mockReset(); 25 | mockDynamoDbHandler.getGreengrassCoreDevice.mockReset(); 26 | mockDynamoDbHandler.updateConnection.mockReset(); 27 | mockGreengrassV2Handler.createDeployment.mockReset(); 28 | mockGreengrassV2Handler.deleteComponent.mockReset(); 29 | mockGreengrassV2Handler.getDeployment.mockReset(); 30 | mockIoTHandler.publishIoTTopicMessage.mockReset(); 31 | mockIoTSiteWiseHandler.deleteGatewayCapabilityConfigurationSource.mockReset(); 32 | sendAnonymousMetricsSpy.mockReset(); 33 | sleepSpy.mockReset(); 34 | }); 35 | 36 | test('Test not sending anonymous metrics', async () => { 37 | mockDynamoDbHandler.getGreengrassCoreDevice.mockResolvedValue({ 38 | iotSiteWiseGatewayId: 'mock-gateway-id', 39 | iotThingArn: 'arn:of:thing' 40 | }); 41 | 42 | mockDynamoDbHandler.deleteConnection.mockResolvedValueOnce(undefined); 43 | mockDynamoDbHandler.updateConnection.mockResolvedValueOnce(undefined); 44 | mockGreengrassV2Handler.createDeployment.mockResolvedValueOnce({ deploymentId: mockValues.deploymentId }); 45 | mockGreengrassV2Handler.deleteComponent.mockResolvedValueOnce(undefined); 46 | mockGreengrassV2Handler.getDeployment.mockResolvedValueOnce({ deploymentStatus: 'COMPLETED' }); 47 | mockIoTHandler.publishIoTTopicMessage.mockResolvedValueOnce(undefined); 48 | sleepSpy.mockResolvedValueOnce(undefined); 49 | 50 | await handler(event); 51 | 52 | expect(sendAnonymousMetricsSpy).not.toHaveBeenCalled(); 53 | }); 54 | -------------------------------------------------------------------------------- /source/lambda/greengrass-deployer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "emitDecoratorMetadata": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "jest.config.js" 14 | ], 15 | "exclude": [ 16 | "node_modules", 17 | "package" 18 | ] 19 | } -------------------------------------------------------------------------------- /source/lambda/lib/aws-handlers/lambda-handler.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import Lambda from 'aws-sdk/clients/lambda'; 5 | import { LambdaError } from '../errors'; 6 | import Logger, { LoggingLevel } from '../logger'; 7 | import { ConnectionDefinition } from '../types/solution-common-types'; 8 | import { getAwsSdkOptions } from '../utils'; 9 | 10 | const { LOGGING_LEVEL, GREENGRASS_DEPLOYER_LAMBDA_FUNCTION } = process.env; 11 | const lambda = new Lambda(getAwsSdkOptions()); 12 | const logger = new Logger('LambdaHandler', LOGGING_LEVEL); 13 | 14 | /** 15 | * The Lambda handler to control Lambda functions 16 | */ 17 | export default class LambdaHandler { 18 | private readonly greengrassDeployerLambdaFunction: string; 19 | 20 | constructor() { 21 | this.greengrassDeployerLambdaFunction = GREENGRASS_DEPLOYER_LAMBDA_FUNCTION; 22 | } 23 | 24 | /** 25 | * Invokes the Greengrass deployer Lambda function. 26 | * @param payload The event payload to send to the Greengrass deployer Lambda function 27 | * @throws `LambdaHandlerError` when it fails to invoke the Greengrass deployer Lambda function 28 | */ 29 | public async invokeGreengrassDeployer(payload: ConnectionDefinition): Promise { 30 | try { 31 | const params: Lambda.InvocationRequest = { 32 | FunctionName: this.greengrassDeployerLambdaFunction, 33 | InvocationType: 'Event', 34 | Payload: JSON.stringify(payload) 35 | }; 36 | 37 | logger.log(LoggingLevel.DEBUG, `Invoking Greengrass deployer: ${JSON.stringify(params, null, 2)}`); 38 | 39 | await lambda.invoke(params).promise(); 40 | } catch (error) { 41 | logger.log(LoggingLevel.ERROR, '[invokeGreengrassDeployer] Error: ', error); 42 | 43 | throw new LambdaError({ 44 | message: `Failed to ${payload.control} the connection.`, 45 | name: 'LambdaHandlerError' 46 | }); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /source/lambda/lib/aws-handlers/secretsManager-handler.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import SecretsManager, { 5 | CreateSecretResponse, 6 | DeleteSecretResponse, 7 | UpdateSecretResponse 8 | } from 'aws-sdk/clients/secretsmanager'; 9 | import Logger, { LoggingLevel } from '../logger'; 10 | import { getAwsSdkOptions } from '../utils'; 11 | 12 | const { LOGGING_LEVEL } = process.env; 13 | const logger = new Logger('SecretsManagerHandler', LOGGING_LEVEL); 14 | 15 | const secretsManager = new SecretsManager(getAwsSdkOptions()); 16 | 17 | export default class SecretsManagerHandler { 18 | public async createSecret( 19 | secretId: string, 20 | secretValue: object 21 | ): Promise { 22 | logger.log(LoggingLevel.DEBUG, `Creating secret`); 23 | 24 | //If the same connector name was previously used, the secret might already exist. 25 | //Clear the delete flag if so and call update secret instead 26 | try { 27 | return await secretsManager 28 | .createSecret({ 29 | Name: secretId, 30 | SecretString: JSON.stringify(secretValue) 31 | }) 32 | .promise(); 33 | } catch (err) { 34 | if (err.message.toLowerCase().includes('scheduled for deletion')) { 35 | await secretsManager 36 | .restoreSecret({ 37 | SecretId: secretId 38 | }) 39 | .promise(); 40 | return await this.updateSecret(secretId, secretValue); 41 | } else { 42 | throw err; 43 | } 44 | } 45 | } 46 | 47 | public async updateSecret(secretId: string, secretValue: object): Promise { 48 | logger.log(LoggingLevel.DEBUG, `Updating secret`); 49 | 50 | try { 51 | return await secretsManager 52 | .updateSecret({ 53 | SecretId: secretId, 54 | SecretString: JSON.stringify(secretValue) 55 | }) 56 | .promise(); 57 | } catch (err) { 58 | if (err.code === 'ResourceNotFoundException') { 59 | return await this.createSecret(secretId, secretValue); 60 | } else if (err.message.toLowerCase().includes('scheduled for deletion')) { 61 | await secretsManager 62 | .restoreSecret({ 63 | SecretId: secretId 64 | }) 65 | .promise(); 66 | return await this.updateSecret(secretId, secretValue); 67 | } else { 68 | throw err; 69 | } 70 | } 71 | } 72 | 73 | public async deleteSecret(secretId: string): Promise { 74 | logger.log(LoggingLevel.DEBUG, `Deleting secret`); 75 | 76 | try { 77 | return await secretsManager 78 | .deleteSecret({ 79 | SecretId: secretId 80 | }) 81 | .promise(); 82 | } catch { 83 | //handle no secret or secret already deleted 84 | logger.log(LoggingLevel.DEBUG, `Delete failed. Possible there is no secret or the secret was previously deleted`); 85 | return undefined; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /source/lambda/lib/aws-handlers/timestream-handler.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import TimestreamWrite, { ListTablesResponse } from 'aws-sdk/clients/timestreamwrite'; 5 | import Logger, { LoggingLevel } from '../logger'; 6 | import { getAwsSdkOptions } from '../utils'; 7 | import { 8 | ListTablesRequest, 9 | DeleteTableRequest, 10 | DeleteDatabaseRequest, 11 | WriteRecordsRequest 12 | } from '../types/timestream-handler-types'; 13 | 14 | const { LOGGING_LEVEL } = process.env; 15 | const timestreamWrite = new TimestreamWrite(getAwsSdkOptions()); 16 | const logger = new Logger('TimestreamHandler', LOGGING_LEVEL); 17 | 18 | /** 19 | * The Timestream handler to control Timestream actions 20 | */ 21 | export default class TimestreamHandler { 22 | /** 23 | * Writes records in Timestream database and table. 24 | * @param params The required params to do a write records 25 | */ 26 | public async write(params: WriteRecordsRequest): Promise { 27 | logger.log(LoggingLevel.DEBUG, `Writing records into Timestream: ${params.databaseName}/${params.tableName}`); 28 | 29 | const writeRecordsRequest: TimestreamWrite.WriteRecordsRequest = { 30 | DatabaseName: params.databaseName, 31 | Records: params.records, 32 | TableName: params.tableName 33 | }; 34 | 35 | await timestreamWrite.writeRecords(writeRecordsRequest).promise(); 36 | } 37 | 38 | async listTables(params: ListTablesRequest): Promise { 39 | const listTablesRequest: TimestreamWrite.Types.ListTablesRequest = { 40 | DatabaseName: params.databaseName 41 | }; 42 | 43 | return await timestreamWrite.listTables(listTablesRequest).promise(); 44 | } 45 | 46 | async deleteTable(params: DeleteTableRequest): Promise { 47 | const deleteTableRequest: TimestreamWrite.Types.DeleteTableRequest = { 48 | DatabaseName: params.databaseName, 49 | TableName: params.tableName 50 | }; 51 | console.log(`Deleting table ${params.tableName}`); 52 | 53 | await timestreamWrite.deleteTable(deleteTableRequest).promise(); 54 | } 55 | 56 | async deleteDatabase(params: DeleteDatabaseRequest): Promise { 57 | const deleteDbParams = { 58 | DatabaseName: params.databaseName 59 | }; 60 | console.log(`Deleting database ${params.databaseName}`); 61 | 62 | await timestreamWrite.deleteDatabase(deleteDbParams).promise(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /source/lambda/lib/errors.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | interface ErrorInput { 5 | message: string; 6 | name?: string; 7 | statusCode?: number; 8 | } 9 | 10 | /** 11 | * `LambdaError` to throw when an error happens on the Lambda functions 12 | * @param error The error message (required), name (optional), and status code (optional). 13 | */ 14 | export class LambdaError extends Error { 15 | public readonly statusCode: number; 16 | constructor(error: ErrorInput) { 17 | super(error.message); 18 | this.name = error.name || 'LambdaError'; 19 | this.statusCode = error.statusCode || 500; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /source/lambda/lib/jest.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | preset: 'ts-jest', 6 | roots: ['/test'], 7 | transform: { 8 | '^.+\\.(ts|tsx)?$': 'ts-jest' 9 | }, 10 | setupFiles: ['./test/jest-environment-variables.ts'], 11 | collectCoverageFrom: ['**/*.ts', '!**/test/*.ts'], 12 | coverageReporters: ['text', ['lcov', { projectRoot: '../../' }]], 13 | testTimeout: 20000 14 | }; 15 | -------------------------------------------------------------------------------- /source/lambda/lib/logger.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export enum LoggingLevel { 5 | ERROR = 1, 6 | WARN = 2, 7 | INFO = 3, 8 | DEBUG = 4, 9 | VERBOSE = 5 10 | } 11 | 12 | /** 13 | * Logger class 14 | */ 15 | export default class Logger { 16 | private readonly name: string; 17 | private readonly loggingLevel: LoggingLevel; 18 | 19 | /** 20 | * Sets up the default properties. 21 | * @param name The logger name which will be shown in the log. 22 | * @param loggingLevel The logging level to show the minimum logs. 23 | */ 24 | constructor(name: string, loggingLevel: string | LoggingLevel) { 25 | this.name = name; 26 | 27 | if (typeof loggingLevel === 'string' || !loggingLevel) { 28 | this.loggingLevel = LoggingLevel[loggingLevel] || LoggingLevel.ERROR; 29 | } else { 30 | this.loggingLevel = loggingLevel; 31 | } 32 | } 33 | 34 | /** 35 | * Logs when the logging level is lower than the default logging level. 36 | * @param loggingLevel The logging level of the log 37 | * @param messages The log messages 38 | */ 39 | public log(loggingLevel: LoggingLevel, ...messages: unknown[]): void { 40 | if (loggingLevel <= this.loggingLevel) { 41 | this._log(loggingLevel, ...messages); 42 | } 43 | } 44 | 45 | /** 46 | * Logs based on the logging level. 47 | * @param loggingLevel The logging level of the log 48 | * @param messages The log messages 49 | */ 50 | private _log(loggingLevel: LoggingLevel, ...messages: unknown[]): void { 51 | switch (loggingLevel) { 52 | case LoggingLevel.VERBOSE: 53 | case LoggingLevel.DEBUG: 54 | console.debug(`[${this.name}]`, ...messages); 55 | break; 56 | case LoggingLevel.INFO: 57 | console.info(`[${this.name}]`, ...messages); 58 | break; 59 | case LoggingLevel.WARN: 60 | console.warn(`[${this.name}]`, ...messages); 61 | break; 62 | default: 63 | console.error(`[${this.name}]`, ...messages); 64 | break; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /source/lambda/lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib", 3 | "version": "4.2.3", 4 | "description": "The Lambda functions common libraries", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf node_modules dist coverage package-lock.json yarn.lock", 8 | "test": "jest --coverage --silent" 9 | }, 10 | "dependencies": { 11 | "@paralleldrive/cuid2": "^2.2.1", 12 | "aws-sdk": "2.1386.0", 13 | "axios": "~1.4.0" 14 | }, 15 | "devDependencies": { 16 | "@types/jest": "^29.5.3", 17 | "@types/node": "^20.4.5", 18 | "jest": "^29.6.1", 19 | "ts-jest": "^29.1.1", 20 | "ts-node": "^10.9.1", 21 | "typescript": "~5.1.6" 22 | }, 23 | "engines": { 24 | "node": ">=18.0.0" 25 | }, 26 | "author": { 27 | "name": "Amazon Web Services", 28 | "url": "https://aws.amazon.com/solutions" 29 | }, 30 | "license": "Apache-2.0" 31 | } 32 | -------------------------------------------------------------------------------- /source/lambda/lib/test/errors.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // PREPARE 5 | import { LambdaError } from '../errors'; 6 | 7 | test('Test to throw LambdaError with the default parameters', () => { 8 | try { 9 | throw new LambdaError({ 10 | message: 'An error happened.' 11 | }); 12 | } catch (error) { 13 | expect(error.message).toEqual('An error happened.'); 14 | expect(error.name).toEqual('LambdaError'); 15 | expect(error.statusCode).toEqual(500); 16 | } 17 | }); 18 | 19 | test('Test to throw LambdaError with the customized name', () => { 20 | try { 21 | throw new LambdaError({ 22 | message: 'An error happened.', 23 | name: 'CustomError' 24 | }); 25 | } catch (error) { 26 | expect(error.message).toEqual('An error happened.'); 27 | expect(error.name).toEqual('CustomError'); 28 | expect(error.statusCode).toEqual(500); 29 | } 30 | }); 31 | 32 | test('Test to throw LambdaError with the customized status code', () => { 33 | try { 34 | throw new LambdaError({ 35 | message: 'Not found.', 36 | statusCode: 404 37 | }); 38 | } catch (error) { 39 | expect(error.message).toEqual('Not found.'); 40 | expect(error.name).toEqual('LambdaError'); 41 | expect(error.statusCode).toEqual(404); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /source/lambda/lib/test/jest-environment-variables.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | process.env = { 5 | ARTIFACT_BUCKET: 'mock-artifact-bucket', 6 | CONNECTION_DYNAMODB_TABLE: 'connection-table', 7 | GREENGRASS_CORE_DEVICES_DYNAMODB_TABLE: 'greengrass-core-devices-table', 8 | GREENGRASS_DEPLOYER_LAMBDA_FUNCTION: 'mock-greengrass-deployer', 9 | KINESIS_STREAM: 'mock-kinesis-stream', 10 | LAMBDA_ROLE: 'mock-lambda-role', 11 | LOGGING_LEVEL: 'ERROR', 12 | LOGS_DYNAMODB_TABLE: 'logs-table', 13 | PAGE_SIZE: '2', 14 | SOLUTION_ID: 'SO0070', 15 | SOLUTION_VERSION: 'vTest' 16 | }; 17 | -------------------------------------------------------------------------------- /source/lambda/lib/test/lambda-handler.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { consoleErrorSpy, mockAwsLambda } from './mock'; 5 | import { LambdaError } from '../errors'; 6 | import LambdaHandler from '../aws-handlers/lambda-handler'; 7 | 8 | const lambdaHandler = new LambdaHandler(); 9 | 10 | describe('Unit tests of invokeGreengrassDeployer() function', () => { 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | const payload: any = { 13 | control: 'mock' 14 | }; 15 | 16 | beforeEach(() => { 17 | mockAwsLambda.invoke.mockReset(); 18 | consoleErrorSpy.mockReset(); 19 | }); 20 | 21 | test('Test success to invoke the Greengrass deployer Lambda function', async () => { 22 | mockAwsLambda.invoke.mockImplementationOnce(() => ({ 23 | promise() { 24 | return Promise.resolve(); 25 | } 26 | })); 27 | 28 | await lambdaHandler.invokeGreengrassDeployer(payload); 29 | expect(mockAwsLambda.invoke).toHaveBeenCalledTimes(1); 30 | expect(mockAwsLambda.invoke).toHaveBeenCalledWith({ 31 | FunctionName: process.env.GREENGRASS_DEPLOYER_LAMBDA_FUNCTION, 32 | InvocationType: 'Event', 33 | Payload: JSON.stringify(payload) 34 | }); 35 | expect(consoleErrorSpy).not.toHaveBeenCalled(); 36 | }); 37 | 38 | test('Test failure to invoke the Greengrass deployer Lambda function', async () => { 39 | mockAwsLambda.invoke.mockImplementationOnce(() => ({ 40 | promise() { 41 | return Promise.reject('Failure'); 42 | } 43 | })); 44 | 45 | try { 46 | await lambdaHandler.invokeGreengrassDeployer(payload); 47 | } catch (error) { 48 | expect(mockAwsLambda.invoke).toHaveBeenCalledTimes(1); 49 | expect(mockAwsLambda.invoke).toHaveBeenCalledWith({ 50 | FunctionName: process.env.GREENGRASS_DEPLOYER_LAMBDA_FUNCTION, 51 | InvocationType: 'Event', 52 | Payload: JSON.stringify(payload) 53 | }); 54 | expect(error).toEqual( 55 | new LambdaError({ 56 | message: `Failed to ${payload.control} the connection.`, 57 | name: 'LambdaHandlerError' 58 | }) 59 | ); 60 | expect(consoleErrorSpy).toHaveBeenCalledTimes(1); 61 | expect(consoleErrorSpy).toHaveBeenCalledWith('[LambdaHandler]', '[invokeGreengrassDeployer] Error: ', 'Failure'); 62 | } 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /source/lambda/lib/test/mock.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export const mockAwsDynamoDB = { 5 | delete: jest.fn(), 6 | get: jest.fn(), 7 | put: jest.fn(), 8 | query: jest.fn(), 9 | scan: jest.fn(), 10 | update: jest.fn() 11 | }; 12 | jest.mock('aws-sdk/clients/dynamodb', () => ({ 13 | DocumentClient: jest.fn(() => ({ ...mockAwsDynamoDB })) 14 | })); 15 | 16 | export const mockAwsGreengrassV2 = { 17 | createComponentVersion: jest.fn(), 18 | createDeployment: jest.fn(), 19 | deleteComponent: jest.fn(), 20 | deleteCoreDevice: jest.fn(), 21 | getDeployment: jest.fn(), 22 | listComponents: jest.fn(), 23 | listCoreDevices: jest.fn(), 24 | listDeployments: jest.fn() 25 | }; 26 | jest.mock('aws-sdk/clients/greengrassv2', () => jest.fn(() => ({ ...mockAwsGreengrassV2 }))); 27 | 28 | export const mockAwsIoT = { 29 | attachThingPrincipal: jest.fn(), 30 | createKeysAndCertificate: jest.fn(), 31 | createRoleAlias: jest.fn(), 32 | createThing: jest.fn(), 33 | deleteCertificate: jest.fn(), 34 | deleteRoleAlias: jest.fn(), 35 | deleteThing: jest.fn(), 36 | describeEndpoint: jest.fn(), 37 | describeThing: jest.fn(), 38 | detachThingPrincipal: jest.fn(), 39 | listPrincipalThings: jest.fn(), 40 | updateCertificate: jest.fn() 41 | }; 42 | jest.mock('aws-sdk/clients/iot', () => jest.fn(() => mockAwsIoT)); 43 | 44 | export const mockAwsIoTData = { 45 | publish: jest.fn() 46 | }; 47 | jest.mock('aws-sdk/clients/iotdata', () => jest.fn(() => mockAwsIoTData)); 48 | 49 | export const mockAwsIoTSiteWise = { 50 | createGateway: jest.fn(), 51 | deleteGateway: jest.fn(), 52 | describeGatewayCapabilityConfiguration: jest.fn(), 53 | listGateways: jest.fn(), 54 | updateGatewayCapabilityConfiguration: jest.fn() 55 | }; 56 | jest.mock('aws-sdk/clients/iotsitewise', () => jest.fn(() => ({ ...mockAwsIoTSiteWise }))); 57 | 58 | export const mockAwsLambda = { 59 | invoke: jest.fn() 60 | }; 61 | jest.mock('aws-sdk/clients/lambda', () => jest.fn(() => ({ ...mockAwsLambda }))); 62 | 63 | export const mockAwsS3 = { 64 | copyObject: jest.fn(), 65 | deleteObject: jest.fn(), 66 | getObject: jest.fn(), 67 | getSignedUrlPromise: jest.fn(), 68 | putObject: jest.fn(), 69 | deleteObjects: jest.fn(), 70 | deleteBucket: jest.fn(), 71 | listObjectVersions: jest.fn() 72 | }; 73 | jest.mock('aws-sdk/clients/s3', () => jest.fn(() => ({ ...mockAwsS3 }))); 74 | 75 | export const mockAwsTimestreamWrite = { 76 | writeRecords: jest.fn(), 77 | listTables: jest.fn(), 78 | deleteTable: jest.fn(), 79 | deleteDatabase: jest.fn() 80 | }; 81 | jest.mock('aws-sdk/clients/timestreamwrite', () => jest.fn(() => ({ ...mockAwsTimestreamWrite }))); 82 | 83 | export const mockAxios = jest.fn(); 84 | jest.mock('axios', () => ({ post: mockAxios })); 85 | 86 | export const UPPER_ALPHA_NUMERIC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 87 | export const mockCuid2 = jest.fn(); 88 | jest.mock('@paralleldrive/cuid2', () => ({ init: mockCuid2 })); 89 | 90 | export const consoleErrorSpy = jest.spyOn(console, 'error'); 91 | -------------------------------------------------------------------------------- /source/lambda/lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "emitDecoratorMetadata": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "jest.config.js" 14 | ], 15 | "exclude": [ 16 | "node_modules", 17 | "package" 18 | ] 19 | } -------------------------------------------------------------------------------- /source/lambda/lib/types/connection-builder-types.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { 5 | CreatedBy, 6 | GetConnectionResponse, 7 | GetConnectionsResponse, 8 | GetGreengrassCoreDevicesResponse, 9 | GetLogsResponse 10 | } from './dynamodb-handler-types'; 11 | import { ListGreengrassCoreDevicesResponse } from './greengrass-v2-handler-types'; 12 | import { ConnectionControl } from './solution-common-types'; 13 | 14 | type StringJson = Record; 15 | type StringArrayJson = Record; 16 | export type APIResponseBodyType = 17 | | GetConnectionsResponse 18 | | GetConnectionResponse 19 | | GetGreengrassCoreDevicesResponse 20 | | GetLogsResponse 21 | | ListGreengrassCoreDevicesResponse 22 | | ProcessConnectionResponse 23 | | ProcessGreengrassCoreDeviceResponse 24 | | Record; 25 | 26 | export interface APIGatewayRequest { 27 | resource: string; 28 | path: string; 29 | httpMethod: string; 30 | headers: StringJson; 31 | multiValueHeaders: StringArrayJson; 32 | queryStringParameters: StringJson; 33 | multiValueQueryStringParameters: StringArrayJson; 34 | pathParameters: StringJson; 35 | stageVariables: StringJson; 36 | requestContext: Record; 37 | body: string; 38 | isBase64Encoded: string; 39 | } 40 | 41 | export interface APIGatewayResponse { 42 | statusCode: number; 43 | body: string; 44 | headers?: StringJson; 45 | multiValueHeaders?: StringArrayJson; 46 | isBase64Encoded?: boolean; 47 | } 48 | 49 | export interface ProcessConnectionResponse { 50 | connectionName: string; 51 | control: ConnectionControl; 52 | message: string; 53 | } 54 | 55 | export interface GetApiRequestInput { 56 | path: string; 57 | pathParameters: StringJson; 58 | queryStrings: StringJson; 59 | resource: string; 60 | } 61 | 62 | export interface PostApiRequestInput { 63 | body: string; 64 | resource: string; 65 | } 66 | 67 | export enum GreengrassCoreDeviceControl { 68 | CREATE = 'create', 69 | DELETE = 'delete' 70 | } 71 | 72 | export enum GreengrassCoreDeviceOsPlatform { 73 | LINUX = 'linux', 74 | WINDOWS = 'windows' 75 | } 76 | 77 | export enum GreengrassCoreDeviceEventTypes { 78 | CREATE = 'CreateGreengrassCoreDevice', 79 | DELETE = 'DeleteGreengrassCoreDevice' 80 | } 81 | 82 | export interface PostGreengrassRequestBodyInput { 83 | name: string; 84 | control: GreengrassCoreDeviceControl; 85 | createdBy: CreatedBy; 86 | osPlatform: GreengrassCoreDeviceOsPlatform; 87 | } 88 | 89 | export interface ProcessGreengrassCoreDeviceResponse { 90 | name: string; 91 | message: string; 92 | } 93 | -------------------------------------------------------------------------------- /source/lambda/lib/types/greengrass-deployer-types.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { DynamoDBHandlerTypes, SolutionCommonTypes } from '.'; 5 | import { ConnectionControl, MachineProtocol } from './solution-common-types'; 6 | 7 | export interface MetricData { 8 | EventType: ConnectionControl; 9 | protocol: MachineProtocol; 10 | interval?: number; 11 | iterations?: number; 12 | numberOfLists?: number; 13 | numberOfTags?: number; 14 | } 15 | 16 | export interface UpdateOpcUaConfigurationRequest { 17 | currentConfiguration: DynamoDBHandlerTypes.GetConnectionResponse; 18 | currentControl: SolutionCommonTypes.ConnectionControl; 19 | gatewayId: string; 20 | newConfiguration: SolutionCommonTypes.ConnectionDefinition; 21 | } 22 | 23 | export interface DeleteComponentRequest { 24 | connectionName: string; 25 | gatewayId: string; 26 | protocol: MachineProtocol; 27 | } 28 | -------------------------------------------------------------------------------- /source/lambda/lib/types/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export * as ConnectionBuilderTypes from './connection-builder-types'; 5 | export * as CustomResourceTypes from './custom-resource-types'; 6 | export * as DynamoDBHandlerTypes from './dynamodb-handler-types'; 7 | export * as GreengrassDeployerTypes from './greengrass-deployer-types'; 8 | export * as GreengrassV2HandlerTypes from './greengrass-v2-handler-types'; 9 | export * as IoTHandlerTypes from './iot-handler-types'; 10 | export * as IoTSiteWiseHandlerTypes from './iot-sitewise-handler-types'; 11 | export * as MessageConsumerTypes from './message-consumer-types'; 12 | export * as S3HandlerTypes from './s3-handler-types'; 13 | export * as SolutionCommonTypes from './solution-common-types'; 14 | export * as UtilsTypes from './utils-types'; 15 | export * as TimestreamHandlerTypes from './timestream-handler-types'; 16 | -------------------------------------------------------------------------------- /source/lambda/lib/types/iot-handler-types.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * Below ones are the supported IoT endpoint types as of February 2022. 6 | * {@link https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Iot.html#describeEndpoint-property} 7 | */ 8 | export type IoTEndpointType = 'iot:Data' | 'iot:Data-ATS' | 'iot:CredentialProvider' | 'iot:Jobs'; 9 | 10 | export enum IoTMessageTypes { 11 | JOB = 'job', 12 | INFO = 'info', 13 | ERROR = 'error' 14 | } 15 | 16 | export interface PublishIoTTopicMessageRequest { 17 | connectionName: string; 18 | type: IoTMessageTypes; 19 | data: unknown; 20 | } 21 | -------------------------------------------------------------------------------- /source/lambda/lib/types/iot-sitewise-handler-types.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * The below types restricts the values only for the solution only. 6 | * This can be expended to support other values in the future. 7 | */ 8 | type CertificateTrustType = 'TrustAny'; 9 | type DefinitionRootPathType = '/'; 10 | type DefinitionType = 'OpcUaRootPath'; 11 | type DestinationType = 'StreamManager'; 12 | type IdentityProviderType = 'Anonymous'; 13 | type MeasurementDataStringPrefixType = ''; 14 | type MessageSecurityMode = 'NONE'; 15 | type NodeFilterRulesActionType = 'INCLUDE'; 16 | type SecurityPolicyType = 'NONE'; 17 | 18 | export type AddGatewayCapacityConfigurationRequest = Omit; 19 | export type GatewayIdAndConfiguration = Pick; 20 | export type GatewayIdAndServerName = Pick; 21 | export type GatewayIdAndSource = Pick; 22 | 23 | interface IoTSiteWiseRequestParameters { 24 | configuration: string; 25 | connectionName: string; 26 | gatewayId: string; 27 | serverName: string; 28 | source: CapabilityConfigurationSource; 29 | machineIp: string; 30 | port?: number; 31 | } 32 | 33 | export interface GetDefaultSourceRequest { 34 | connectionName: string; 35 | endpointUri: string; 36 | name: string; 37 | } 38 | 39 | export interface CapabilityConfigurationSource { 40 | destination: Destination; 41 | name: string; 42 | endpoint: Endpoint; 43 | measurementDataStreamPrefix: MeasurementDataStringPrefixType; 44 | } 45 | 46 | interface CertificateTrust { 47 | type: CertificateTrustType; 48 | } 49 | 50 | interface IdentityProvider { 51 | type: IdentityProviderType; 52 | } 53 | 54 | interface NodeFilterRulesDefinition { 55 | type: DefinitionType; 56 | rootPath: DefinitionRootPathType; 57 | } 58 | 59 | interface NodeFilterRule { 60 | action: NodeFilterRulesActionType; 61 | definition: NodeFilterRulesDefinition; 62 | } 63 | 64 | interface Endpoint { 65 | certificateTrust: CertificateTrust; 66 | endpointUri: string; 67 | identityProvider: IdentityProvider; 68 | messageSecurityMode: MessageSecurityMode; 69 | nodeFilterRules: NodeFilterRule[]; 70 | securityPolicy: SecurityPolicyType; 71 | } 72 | 73 | interface Destination { 74 | streamBufferSize: number; 75 | streamName: string; 76 | type: DestinationType; 77 | } 78 | 79 | export interface ListGateway { 80 | gatewayId: string; 81 | coreDeviceThingName: string; 82 | } 83 | 84 | export interface ListGatewayResponse { 85 | gateways: ListGateway[]; 86 | } 87 | -------------------------------------------------------------------------------- /source/lambda/lib/types/message-consumer-types.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { LogType } from './solution-common-types'; 5 | 6 | export interface EventMessage { 7 | Records: SqsRecord[]; 8 | } 9 | 10 | export interface SqsRecord { 11 | messageId: string; 12 | receiptHandle: string; 13 | body: string; 14 | attributes: { 15 | ApproximateReceiveCount: string; 16 | SentTimestamp: string; 17 | SenderId: string; 18 | ApproximateFirstReceiveTimestamp: string; 19 | }; 20 | messageAttributes: Record; 21 | md5OfBody: string; 22 | eventSource: string; 23 | eventSourceARN: string; 24 | awsRegion: string; 25 | } 26 | 27 | export interface RecordDefaultBody { 28 | connectionName: string; 29 | logType: LogType; 30 | timestamp: number; 31 | message: string; 32 | } 33 | 34 | export interface RecordBody extends RecordDefaultBody { 35 | siteName: string; 36 | area: string; 37 | process: string; 38 | machineName: string; 39 | } 40 | 41 | export interface ItemBody extends RecordDefaultBody { 42 | ttl: number; 43 | } 44 | 45 | export interface BatchPutRequest { 46 | PutRequest: { 47 | Item: ItemBody; 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /source/lambda/lib/types/modbus-types.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export interface ModbusTcpDefinition { 5 | host: string; 6 | hostPort: number; 7 | hostTag: string; 8 | modbusSecondariesConfig: ModbusTcpSecondaryDefinition[]; 9 | } 10 | 11 | export interface ModbusTcpSecondaryDefinition { 12 | secondaryAddress: string; 13 | frequencyInSeconds: number; 14 | commandConfig: ModbusTcpSecondaryCommandConfig; 15 | } 16 | 17 | export interface ModbusTcpSecondaryCommandConfig { 18 | readCoils?: ModbusTcpSecondaryCommandReadCoilsConfig; 19 | readDiscreteInputs?: ModbusTcpSecondaryCommandReadDiscreteInputsConfig; 20 | readHoldingRegisters?: ModbusTcpSecondaryCommandReadHoldingRegistersConfig; 21 | readInputRegisters?: ModbusTcpSecondaryCommandReadInputRegistersConfig; 22 | } 23 | 24 | export interface ModbusTcpSecondaryCommandIndividualConfig { 25 | enabled: boolean; 26 | address: string; 27 | count?: number; 28 | } 29 | 30 | export type ModbusTcpSecondaryCommandReadCoilsConfig = ModbusTcpSecondaryCommandIndividualConfig; 31 | export type ModbusTcpSecondaryCommandReadDiscreteInputsConfig = ModbusTcpSecondaryCommandIndividualConfig; 32 | export type ModbusTcpSecondaryCommandReadHoldingRegistersConfig = ModbusTcpSecondaryCommandIndividualConfig; 33 | export type ModbusTcpSecondaryCommandReadInputRegistersConfig = ModbusTcpSecondaryCommandIndividualConfig; 34 | -------------------------------------------------------------------------------- /source/lambda/lib/types/s3-handler-types.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export interface GetObjectRequest { 5 | bucket: string; 6 | key: string; 7 | } 8 | 9 | export interface CopyObjectRequest { 10 | destinationBucket: string; 11 | destinationKey: string; 12 | sourceBucketKey: string; 13 | } 14 | 15 | export interface PutObjectRequest { 16 | body: Buffer | string; 17 | contentType: string; 18 | destinationBucket: string; 19 | destinationKey: string; 20 | } 21 | 22 | export interface GetSignedUrlRequest { 23 | bucket: string; 24 | expires: number; 25 | key: string; 26 | operation: string; 27 | } 28 | 29 | export interface DeleteObjectRequest { 30 | sourceBucket: string; 31 | sourceKey: string; 32 | } 33 | 34 | export interface DeleteObjectsRequest { 35 | bucketName: string; 36 | keys: DeleteObjectKey[]; 37 | } 38 | 39 | interface DeleteObjectKey { 40 | Key: string; 41 | VersionId?: string; 42 | } 43 | 44 | export interface DeleteBucketRequest { 45 | bucketName: string; 46 | } 47 | 48 | export interface ListObjectVersionsRequest { 49 | bucketName: string; 50 | } 51 | -------------------------------------------------------------------------------- /source/lambda/lib/types/solution-common-types.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { CapabilityConfigurationSource } from './iot-sitewise-handler-types'; 5 | import { ModbusTcpDefinition } from './modbus-types'; 6 | 7 | export enum ConnectionControl { 8 | DEPLOY = 'deploy', 9 | START = 'start', 10 | STOP = 'stop', 11 | UPDATE = 'update', 12 | DELETE = 'delete', 13 | PUSH = 'push', 14 | PULL = 'pull', 15 | FAIL = 'fail' 16 | } 17 | 18 | export enum MachineProtocol { 19 | OPCDA = 'opcda', 20 | OPCUA = 'opcua', 21 | OSIPI = 'osipi', 22 | MODBUSTCP = 'modbustcp' 23 | } 24 | 25 | export enum LogType { 26 | INFO = 'info', 27 | ERROR = 'error' 28 | } 29 | 30 | export enum OsiPiAuthMode { 31 | ANONYMOUS = 'Anonymous', 32 | BASIC = 'BASIC' 33 | //TODO: Figure out how to support Kerberos auth 34 | // KERBEROS = 'KERBEROS' 35 | } 36 | 37 | /** 38 | * The connection definition to control a connection. This will be sent through the API Gateway body. 39 | * Refer to https://docs.aws.amazon.com/solutions/latest/machine-to-cloud-connectivity-framework/api-specification.html 40 | * @interface ConnectionDefinition 41 | */ 42 | export interface ConnectionDefinition { 43 | connectionName: string; 44 | control: ConnectionControl; 45 | protocol: MachineProtocol; 46 | area?: string; 47 | gatewayId?: string; 48 | greengrassCoreDeviceName?: string; 49 | machineName?: string; 50 | logLevel?: string; 51 | opcDa?: OpcDaDefinition; 52 | opcUa?: OpcUaDefinition; 53 | osiPi?: OsiPiDefinition; 54 | modbusTcp?: ModbusTcpDefinition; 55 | process?: string; 56 | sendDataToIoTSiteWise?: boolean; 57 | sendDataToIoTTopic?: boolean; 58 | sendDataToKinesisDataStreams?: boolean; 59 | sendDataToTimestream?: boolean; 60 | sendDataToHistorian?: boolean; 61 | siteName?: string; 62 | historianKinesisDatastreamName?: string; 63 | osPlatform?: string; 64 | } 65 | 66 | export interface CommonDefinition { 67 | machineIp: string; 68 | serverName: string; 69 | } 70 | 71 | export interface OpcDaDefinition extends CommonDefinition { 72 | interval: number; 73 | iterations: number; 74 | listTags?: string[]; 75 | tags?: string[]; 76 | } 77 | 78 | export interface OpcUaDefinition extends CommonDefinition { 79 | port?: number; 80 | source?: CapabilityConfigurationSource; 81 | } 82 | 83 | export interface OsiPiDefinition { 84 | apiUrl: string; 85 | serverName: string; 86 | authMode: OsiPiAuthMode; 87 | verifySSL: boolean; 88 | username?: string; 89 | password?: string; 90 | credentialSecretArn?: string; 91 | tags: string[]; 92 | requestFrequency: number; 93 | catchupFrequency: number; 94 | maxRequestDuration: number; 95 | queryOffset: number; 96 | } 97 | -------------------------------------------------------------------------------- /source/lambda/lib/types/timestream-handler-types.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import TimestreamWrite from 'aws-sdk/clients/timestreamwrite'; 5 | 6 | export interface WriteRecordsRequest { 7 | databaseName: string; 8 | tableName: string; 9 | records: TimestreamWrite.Records; 10 | } 11 | 12 | export interface ListTablesRequest { 13 | databaseName: string; 14 | } 15 | 16 | export interface DeleteTableRequest { 17 | databaseName: string; 18 | tableName: string; 19 | } 20 | 21 | export interface DeleteDatabaseRequest { 22 | databaseName: string; 23 | } 24 | -------------------------------------------------------------------------------- /source/lambda/lib/types/utils-types.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { GreengrassCoreDeviceEventTypes } from './connection-builder-types'; 5 | import { StackEventTypes } from './custom-resource-types'; 6 | import { CreatedBy } from './dynamodb-handler-types'; 7 | import { ConnectionControl, MachineProtocol } from './solution-common-types'; 8 | 9 | export type AnonymousMetricData = DefaultMetricData | ApiEventMetricData | StackEventMetricData; 10 | export type AwsSdkOptions = Record; 11 | 12 | interface DefaultMetricData { 13 | EventType: ConnectionControl | GreengrassCoreDeviceEventTypes | StackEventTypes; 14 | } 15 | 16 | export interface ApiEventMetricData extends DefaultMetricData { 17 | createdBy?: CreatedBy; 18 | interval?: number; 19 | iterations?: number; 20 | numberOfLists?: number; 21 | numberOfTags?: number; 22 | protocol?: MachineProtocol; 23 | } 24 | 25 | export interface StackEventMetricData extends DefaultMetricData { 26 | ExistingKinesisStream: boolean; 27 | ExistingTimestreamDatabase: boolean; 28 | Region: string; 29 | } 30 | -------------------------------------------------------------------------------- /source/lambda/sqs-message-consumer/jest.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | roots: ['/test'], 6 | preset: 'ts-jest', 7 | transform: { 8 | '^.+\\.(ts|tsx)?$': 'ts-jest' 9 | }, 10 | setupFiles: ['./test/jest-environment-variables.ts'], 11 | collectCoverageFrom: ['**/*.ts', '!**/test/*.ts'], 12 | coverageReporters: ['text', ['lcov', { projectRoot: '../../' }]] 13 | }; 14 | -------------------------------------------------------------------------------- /source/lambda/sqs-message-consumer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sqs-message-consumer", 3 | "version": "4.2.3", 4 | "description": "It consumes the SQS queue messages from IoT topic and stores the logs into the DynamoDB table.", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf node_modules dist coverage package-lock.json yarn.lock", 8 | "compile": "node_modules/typescript/bin/tsc --project tsconfig.json", 9 | "build": "yarn clean && yarn install && yarn compile", 10 | "copy-modules": "yarn install --production --ignore-scripts --prefer-offline && rsync -avrq ./node_modules ./dist", 11 | "package": "yarn build && yarn copy-modules && cd dist && zip -q -r9 package.zip * -x '**/test/*' && cd ..", 12 | "test": "jest --coverage --silent" 13 | }, 14 | "dependencies": { 15 | "@paralleldrive/cuid2": "^2.2.1", 16 | "aws-sdk": "2.1386.0", 17 | "axios": "~1.4.0" 18 | }, 19 | "devDependencies": { 20 | "@types/jest": "^29.5.3", 21 | "@types/node": "^20.4.5", 22 | "jest": "^29.6.1", 23 | "ts-jest": "^29.1.1", 24 | "ts-node": "^10.9.1", 25 | "typescript": "~5.1.6" 26 | }, 27 | "engines": { 28 | "node": ">=18.0.0" 29 | }, 30 | "author": { 31 | "name": "Amazon Web Services", 32 | "url": "https://aws.amazon.com/solutions" 33 | }, 34 | "license": "Apache-2.0" 35 | } 36 | -------------------------------------------------------------------------------- /source/lambda/sqs-message-consumer/test/jest-environment-variables.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | process.env.LOGGING_LEVEL = 'WARN'; 5 | process.env.LOGS_DYNAMODB_TABLE = 'mock-logs-table'; 6 | -------------------------------------------------------------------------------- /source/lambda/sqs-message-consumer/test/mock.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export const mockBatchWrite = jest.fn(); 5 | jest.mock('aws-sdk/clients/dynamodb', () => ({ 6 | DocumentClient: jest.fn(() => ({ batchWrite: mockBatchWrite })) 7 | })); 8 | 9 | jest.mock('../../lib/utils', () => ({ 10 | getAwsSdkOptions: jest.fn().mockReturnValue({}) 11 | })); 12 | 13 | export const consoleWarnSpy = jest.spyOn(console, 'warn'); 14 | export const consoleErrorSpy = jest.spyOn(console, 'error'); 15 | -------------------------------------------------------------------------------- /source/lambda/sqs-message-consumer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "emitDecoratorMetadata": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "jest.config.js" 14 | ], 15 | "exclude": [ 16 | "node_modules", 17 | "package" 18 | ] 19 | } -------------------------------------------------------------------------------- /source/lambda/timestream-writer/jest.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | roots: ['/test'], 6 | preset: 'ts-jest', 7 | transform: { 8 | '^.+\\.(ts|tsx)?$': 'ts-jest' 9 | }, 10 | setupFiles: ['./test/jest-environment-variables.ts'], 11 | collectCoverageFrom: ['**/*.ts', '!**/test/*.ts'], 12 | coverageReporters: ['text', ['lcov', { projectRoot: '../../' }]] 13 | }; 14 | -------------------------------------------------------------------------------- /source/lambda/timestream-writer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timestream-writer", 3 | "version": "4.2.3", 4 | "description": "It consumes the Kinesis Data Stream data and store the data into Amazon Timestream table.", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf node_modules dist coverage package-lock.json yarn.lock", 8 | "compile": "node_modules/typescript/bin/tsc --project tsconfig.json", 9 | "build": "yarn clean && yarn install && yarn compile", 10 | "copy-modules": "yarn install --production --ignore-scripts --prefer-offline && rsync -avrq ./node_modules ./dist", 11 | "package": "yarn build && yarn copy-modules && cd dist && zip -q -r9 package.zip * -x '**/test/*' && cd ..", 12 | "test": "jest --coverage --silent" 13 | }, 14 | "dependencies": { 15 | "@paralleldrive/cuid2": "^2.2.1", 16 | "aws-sdk": "2.1386.0", 17 | "axios": "~1.4.0", 18 | "is-typedarray": "^1.0.0" 19 | }, 20 | "devDependencies": { 21 | "@types/jest": "^29.5.3", 22 | "@types/node": "^20.4.5", 23 | "jest": "^29.6.1", 24 | "ts-jest": "^29.1.1", 25 | "ts-node": "^10.9.1", 26 | "typescript": "~5.1.6" 27 | }, 28 | "engines": { 29 | "node": ">=18.0.0" 30 | }, 31 | "author": { 32 | "name": "Amazon Web Services", 33 | "url": "https://aws.amazon.com/solutions" 34 | }, 35 | "license": "Apache-2.0" 36 | } 37 | -------------------------------------------------------------------------------- /source/lambda/timestream-writer/test/jest-environment-variables.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | process.env.LOGGING_LEVEL = 'ERROR'; 5 | process.env.TIMESTREAM_DATABASE = 'mock-timestream-database'; 6 | process.env.TIMESTREAM_TABLE = 'mock-timestream-table'; 7 | -------------------------------------------------------------------------------- /source/lambda/timestream-writer/test/mock.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export const mockTimestreamHandler = { 5 | write: jest.fn() 6 | }; 7 | jest.mock('../../lib/aws-handlers/timestream-handler', () => jest.fn(() => ({ ...mockTimestreamHandler }))); 8 | 9 | export const consoleErrorSpy = jest.spyOn(console, 'error'); 10 | -------------------------------------------------------------------------------- /source/lambda/timestream-writer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "emitDecoratorMetadata": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "jest.config.js" 14 | ], 15 | "exclude": [ 16 | "node_modules", 17 | "package" 18 | ] 19 | } -------------------------------------------------------------------------------- /source/machine_connector/boilerplate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/machine-to-cloud-connectivity-framework/14c96676a8c0efc7d77ad5ebf823c54a3a18a32a/source/machine_connector/boilerplate/__init__.py -------------------------------------------------------------------------------- /source/machine_connector/boilerplate/logging/logger.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import os 5 | import logging 6 | import sys 7 | 8 | 9 | def get_logger(class_name: str): 10 | LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") 11 | try: 12 | print(f"setting log level to: {LOG_LEVEL}") 13 | logging.basicConfig(stream=sys.stdout, level=LOG_LEVEL) 14 | except Exception as err: 15 | print("Setting log level failed...using default log level") 16 | print(err) 17 | logging.basicConfig(stream=sys.stdout, level="INFO") 18 | logger = logging.getLogger(class_name) 19 | logger.info(f"Using Log Level: {LOG_LEVEL}") 20 | return logger 21 | -------------------------------------------------------------------------------- /source/machine_connector/boilerplate/messaging/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/machine-to-cloud-connectivity-framework/14c96676a8c0efc7d77ad5ebf823c54a3a18a32a/source/machine_connector/boilerplate/messaging/__init__.py -------------------------------------------------------------------------------- /source/machine_connector/boilerplate/messaging/announcements.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # Info messages 5 | INF_MSG_CONNECTION_STARTED = "Connection started." 6 | INF_MSG_CONNECTION_STOPPED = "Connection stopped." 7 | INF_MSG_CONNECTION_UPDATED = "Connection updated." 8 | INF_MSG_SERVER_NAME = "Available server: {}" 9 | INF_MSG_PUBLISH_DATA_TO_TOPIC = "Publishing data to topic %s: %s" 10 | 11 | # Error messages 12 | ERR_MSG_FAIL_SERVER_NAME = "Failed to retrieve available server(s): {}" 13 | ERR_MSG_FAIL_TO_CONNECT = "Unable to connect to the server." 14 | ERR_MSG_LOST_CONNECTION_STOPPED = "Unable to read server: {}" 15 | ERR_MSG_FAIL_UNKNOWN_CONTROL = "Unknown control request: {}" 16 | ERR_MSG_FAIL_LAST_COMMAND_STOP = "Connection '{}' has already been stopped." 17 | ERR_MSG_FAIL_LAST_COMMAND_START = "A version of the requested '{}' is already running. Please stop it before starting it again." 18 | ERR_MSG_NO_CONNECTION_FILE = "Request was not successful. Connection '{}' has not been started." 19 | ERR_MSG_SCHEMA_MESSAGE_NOT_DICT = "Message validation error. Message is not dictionary: '{}'" 20 | ERR_MSG_SCHEMA_EMPTY_MESSAGES = "Message validation error. No data in messages: '{}'" 21 | ERR_MSG_SCHEMA_MISSING_KEY = "Message validation error. Missing key in message '{}'" 22 | ERR_MSG_SCHEMA_DATE_CORRUPTED = "Message validation error. Datestamp is malformed in message '{}'" 23 | ERR_NAME_NOT_ALIAS = "Message validation error. The `name` value within each data point message must be the same string as `alias`: '{}'" 24 | ERR_MISSING_KEYS = "Message validation error. The following keys are missing from the message: '{}'" 25 | ERR_MSG_VALIDATION = "An error occurred validating message data: '{}'" 26 | -------------------------------------------------------------------------------- /source/machine_connector/boilerplate/messaging/message.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import dateutil.parser as parser 5 | 6 | from utils.custom_exception import ValidationException 7 | 8 | 9 | class Message: 10 | 11 | def __init__(self, value: any, quality: str, timestamp: str) -> None: 12 | self.value = value 13 | self.quality = quality 14 | self.timestamp = timestamp 15 | 16 | def validate(self) -> None: 17 | if self.value is None or isinstance(self.quality, str) == False or isinstance(self.timestamp, str) == False: 18 | self._raise_validation_error( 19 | Exception(f"Attribute(s) invalid - value: {self.value}, quality: {self.quality}, timestamp: {self.timestamp}")) 20 | self._validate_timestamp() 21 | 22 | def _raise_validation_error(self, e: Exception) -> None: 23 | raise ValidationException("Could not validate message", e) 24 | 25 | def _validate_timestamp(self) -> None: 26 | try: 27 | parser.parse(self.timestamp) 28 | except Exception as e: 29 | self._raise_validation_error(e) 30 | -------------------------------------------------------------------------------- /source/machine_connector/boilerplate/messaging/message_batch.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import os 5 | 6 | from boilerplate.messaging.message import Message 7 | from utils.custom_exception import ValidationException 8 | 9 | # Site name from component environment variables 10 | SITE_NAME = os.getenv("SITE_NAME") 11 | # Area from component environment variables 12 | AREA = os.getenv("AREA") 13 | # Process from component environment variables 14 | PROCESS = os.getenv("PROCESS") 15 | # Machine name from component environment variables 16 | MACHINE_NAME = os.getenv("MACHINE_NAME") 17 | 18 | 19 | class MessageBatch: 20 | 21 | def __init__(self, tag: str, messages: 'list[Message]', source_id: str) -> None: 22 | self.alias = f"{SITE_NAME}/{AREA}/{PROCESS}/{MACHINE_NAME}/{tag}" 23 | self.messages = self._get_messages_as_dict(messages) 24 | self.sourceId = source_id 25 | self.validate(tag, messages) 26 | 27 | def validate(self, tag: str, messages: 'list[Message]') -> None: 28 | if (isinstance(self.alias, str) == False 29 | or isinstance(tag, str) == False 30 | or len(messages) == 0 31 | or isinstance(self.sourceId, str) == False): 32 | self._raise_validation_error() 33 | 34 | for message in messages: 35 | message.validate() 36 | 37 | def _get_messages_as_dict(self, messages: 'list[Message]'): 38 | messages_as_dict = [] 39 | for message in messages: 40 | message_dict = message.__dict__ 41 | message_dict['name'] = self.alias 42 | messages_as_dict.append(message_dict) 43 | return messages_as_dict 44 | 45 | def _raise_validation_error(self) -> None: 46 | raise ValidationException("Could not validate message batch") 47 | -------------------------------------------------------------------------------- /source/machine_connector/boilerplate/tests/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = */tests/*,*/utils/* -------------------------------------------------------------------------------- /source/machine_connector/boilerplate/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/machine-to-cloud-connectivity-framework/14c96676a8c0efc7d77ad5ebf823c54a3a18a32a/source/machine_connector/boilerplate/tests/__init__.py -------------------------------------------------------------------------------- /source/machine_connector/boilerplate/tests/test_logger.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import json 5 | 6 | from unittest import mock, TestCase 7 | from boilerplate.logging.logger import get_logger 8 | 9 | 10 | class TestLogger(TestCase): 11 | 12 | def test_get_logger(self): 13 | # Arrange and Act 14 | logger = get_logger("test-class-name") 15 | 16 | # Assert 17 | logger.info("test-log") 18 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_modbus_tcp_connector/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/machine-to-cloud-connectivity-framework/14c96676a8c0efc7d77ad5ebf823c54a3a18a32a/source/machine_connector/m2c2_modbus_tcp_connector/__init__.py -------------------------------------------------------------------------------- /source/machine_connector/m2c2_modbus_tcp_connector/config.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | lock = False # flag used to prevent concurrency 5 | control = "" # connection control variables monitored by the thread 6 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_modbus_tcp_connector/messages.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # Info messages 5 | INF_MSG_CONNECTION_STARTED = "Connection started." 6 | INF_MSG_CONNECTION_STOPPED = "Connection stopped." 7 | INF_MSG_CONNECTION_UPDATED = "Connection updated." 8 | INF_MSG_CONFIGURATION = "Modbus configuration: {}" 9 | INF_MSG_PUBLISH_DATA_TO_TOPIC = "Publishing data to topic %s: %s" 10 | 11 | # Error messages 12 | ERR_MSG_FAIL_SERVER_NAME = "Failed to retrieve available server(s): {}" 13 | ERR_MSG_FAIL_TO_CONNECT = "Unable to connect to the server." 14 | ERR_MSG_LOST_CONNECTION_STOPPED = "Unable to read from secondary: {}" 15 | ERR_MSG_FAIL_UNKNOWN_CONTROL = "Unknown control request: {}" 16 | ERR_MSG_FAIL_LAST_COMMAND_STOP = "Connection '{}' has already been stopped." 17 | ERR_MSG_FAIL_LAST_COMMAND_START = "A version of the requested '{}' is already running. Please stop it before starting it again." 18 | ERR_MSG_NO_CONNECTION_FILE = "Request was not successful. Connection '{}' has not been started." 19 | ERR_MSG_SCHEMA_MESSAGE_NOT_DICT = "Message validation error. Message is not dictionary: '{}'" 20 | ERR_MSG_SCHEMA_EMPTY_MESSAGES = "Message validation error. No data in messages: '{}'" 21 | ERR_MSG_SCHEMA_MISSING_KEY = "Message validation error. Missing key in message '{}'" 22 | ERR_MSG_SCHEMA_DATE_CORRUPTED = "Message validation error. Datestamp is malformed in message '{}'" 23 | ERR_NAME_NOT_ALIAS = "Message validation error. The `name` value within each data point message must be the same string as `alias`: '{}'" 24 | ERR_MISSING_KEYS = "Message validation error. The following keys are missing from the message: '{}'" 25 | ERR_MSG_VALIDATION = "An error occurred validating message data: '{}'" 26 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_modbus_tcp_connector/modbus_exception.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | class ModbusException(Exception): 5 | pass 6 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_modbus_tcp_connector/requirements.txt: -------------------------------------------------------------------------------- 1 | twisted[serial]>=20.3.0 2 | pymodbus==3.0.0.dev4 3 | pyserial>=3.5 4 | pyserial-asyncio==0.6 5 | greengrasssdk==1.6.1 6 | backoff==2.2.1 7 | awsiotsdk==1.18.0 8 | dateutil==1.4 -------------------------------------------------------------------------------- /source/machine_connector/m2c2_modbus_tcp_connector/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/machine-to-cloud-connectivity-framework/14c96676a8c0efc7d77ad5ebf823c54a3a18a32a/source/machine_connector/m2c2_modbus_tcp_connector/tests/__init__.py -------------------------------------------------------------------------------- /source/machine_connector/m2c2_modbus_tcp_connector/tests/test_m2c2_modbus_tcp_connector.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from unittest import mock, TestCase 5 | 6 | 7 | class TestM2C2ModbusTCPConnector(TestCase): 8 | 9 | def test_main(self): 10 | # arrange 11 | with mock.patch("utils.AWSEndpointClient.__init__", return_value=None) as mock_endpoint_client: 12 | import m2c2_modbus_tcp_connector.m2c2_modbus_tcp_connector as connector 13 | self.connector = connector 14 | self.connector.connector_client = mock_endpoint_client.MagicMock() 15 | self.connector.connector_client.publish_message_to_iot_topic = mock.MagicMock() 16 | self.connector.connector_client.stop_client = mock.MagicMock() 17 | self.connector.connector_client.start_client = mock.MagicMock() 18 | self.connector.connector_client.read_local_connection_configuration = mock.MagicMock() 19 | self.connector.connector_client.write_local_connection_configuration_file = mock.MagicMock() 20 | 21 | # act 22 | connector.main() 23 | 24 | # assert 25 | self.connector.connector_client.read_local_connection_configuration.assert_called() 26 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_opcda_connector/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/machine-to-cloud-connectivity-framework/14c96676a8c0efc7d77ad5ebf823c54a3a18a32a/source/machine_connector/m2c2_opcda_connector/__init__.py -------------------------------------------------------------------------------- /source/machine_connector/m2c2_opcda_connector/messages.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # Info messages 5 | INF_MSG_CONNECTION_STARTED = "Connection started." 6 | INF_MSG_CONNECTION_STOPPED = "Connection stopped." 7 | INF_MSG_CONNECTION_UPDATED = "Connection updated." 8 | INF_MSG_SERVER_NAME = "Available server: {}" 9 | INF_MSG_PUBLISH_DATA_TO_TOPIC = "Publishing data to topic %s: %s" 10 | 11 | # Error messages 12 | ERR_MSG_FAIL_SERVER_NAME = "Failed to retrieve available server(s): {}" 13 | ERR_MSG_FAIL_TO_CONNECT = "Unable to connect to the server." 14 | ERR_MSG_LOST_CONNECTION_STOPPED = "Unable to read server: {}" 15 | ERR_MSG_FAIL_UNKNOWN_CONTROL = "Unknown control request: {}" 16 | ERR_MSG_FAIL_LAST_COMMAND_STOP = "Connection '{}' has already been stopped." 17 | ERR_MSG_FAIL_LAST_COMMAND_START = "A version of the requested '{}' is already running. Please stop it before starting it again." 18 | ERR_MSG_NO_CONNECTION_FILE = "Request was not successful. Connection '{}' has not been started." 19 | ERR_MSG_SCHEMA_MESSAGE_NOT_DICT = "Message validation error. Message is not dictionary: '{}'" 20 | ERR_MSG_SCHEMA_EMPTY_MESSAGES = "Message validation error. No data in messages: '{}'" 21 | ERR_MSG_SCHEMA_MISSING_KEY = "Message validation error. Missing key in message '{}'" 22 | ERR_MSG_SCHEMA_DATE_CORRUPTED = "Message validation error. Datestamp is malformed in message '{}'" 23 | ERR_NAME_NOT_ALIAS = "Message validation error. The `name` value within each data point message must be the same string as `alias`: '{}'" 24 | ERR_MISSING_KEYS = "Message validation error. The following keys are missing from the message: '{}'" 25 | ERR_MSG_VALIDATION = "An error occurred validating message data: '{}'" 26 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_opcda_connector/requirements.txt: -------------------------------------------------------------------------------- 1 | greengrasssdk==1.6.1 2 | Pyro4==4.81 3 | OpenOPC-Python3x==1.3.1 4 | python-dateutil==2.8.1 5 | backoff==2.2.1 6 | awsiotsdk==1.18.0 -------------------------------------------------------------------------------- /source/machine_connector/m2c2_opcda_connector/tests/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = */tests/*,*/utils/* -------------------------------------------------------------------------------- /source/machine_connector/m2c2_opcda_connector/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/machine-to-cloud-connectivity-framework/14c96676a8c0efc7d77ad5ebf823c54a3a18a32a/source/machine_connector/m2c2_opcda_connector/tests/__init__.py -------------------------------------------------------------------------------- /source/machine_connector/m2c2_opcda_connector/validations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/machine-to-cloud-connectivity-framework/14c96676a8c0efc7d77ad5ebf823c54a3a18a32a/source/machine_connector/m2c2_opcda_connector/validations/__init__.py -------------------------------------------------------------------------------- /source/machine_connector/m2c2_opcda_connector/validations/m2c2_msg_types.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | class OPCDAMsgValidations: 5 | def __init__(self): 6 | """ 7 | This class establishes the expected configuration and validation 8 | of types used by the OPC DA collector and 9 | expected format for the publisher. 10 | """ 11 | pass 12 | 13 | def payload_required_keys(self) -> list: 14 | return ["alias", "messages"] 15 | 16 | def messages_required_keys(self) -> list: 17 | return ["name", "timestamp", "value", "quality"] 18 | 19 | def payload_validations(self) -> dict: 20 | return { 21 | "alias": lambda x: isinstance(x, str), 22 | "messages": lambda x: isinstance(x, list) 23 | } 24 | 25 | def quality_validations(self) -> list: 26 | return [ 27 | 'Good', 28 | 'GOOD', 29 | 'Bad', 30 | 'BAD', 31 | 'Uncertain', 32 | 'UNCERTAIN' 33 | ] 34 | 35 | def msgs_validations(self) -> dict: 36 | return { 37 | "name": lambda x: isinstance(x, str), 38 | "timestamp": lambda x: isinstance(x, str), 39 | "value": lambda x: x != None, 40 | "quality": lambda x: isinstance(x, str) and x in self.quality_validations() 41 | } 42 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_osipi_connector/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/machine-to-cloud-connectivity-framework/14c96676a8c0efc7d77ad5ebf823c54a3a18a32a/source/machine_connector/m2c2_osipi_connector/__init__.py -------------------------------------------------------------------------------- /source/machine_connector/m2c2_osipi_connector/messages.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # Info messages 5 | INF_MSG_CONNECTION_STARTED = "Connection started." 6 | INF_MSG_CONNECTION_STOPPED = "Connection stopped." 7 | INF_MSG_CONNECTION_UPDATED = "Connection updated." 8 | INF_MSG_SERVER_NAME = "Available server: {}" 9 | INF_MSG_PUBLISH_DATA_TO_TOPIC = "Publishing data to topic %s: %s" 10 | 11 | # Error messages 12 | ERR_MSG_FAIL_SERVER_NAME = "Failed to retrieve available server(s): {}" 13 | ERR_MSG_FAIL_TO_CONNECT = "Unable to connect to the server." 14 | ERR_MSG_LOST_CONNECTION_STOPPED = "Unable to read server: {}" 15 | ERR_MSG_FAIL_UNKNOWN_CONTROL = "Unknown control request: {}" 16 | ERR_MSG_FAIL_LAST_COMMAND_STOP = "Connection '{}' has already been stopped." 17 | ERR_MSG_FAIL_LAST_COMMAND_START = "A version of the requested '{}' is already running. Please stop it before starting it again." 18 | ERR_MSG_NO_CONNECTION_FILE = "Request was not successful. Connection '{}' has not been started." 19 | ERR_MSG_SCHEMA_MESSAGE_NOT_DICT = "Message validation error. Message is not dictionary: '{}'" 20 | ERR_MSG_SCHEMA_EMPTY_MESSAGES = "Message validation error. No data in messages: '{}'" 21 | ERR_MSG_SCHEMA_MISSING_KEY = "Message validation error. Missing key in message '{}'" 22 | ERR_MSG_SCHEMA_DATE_CORRUPTED = "Message validation error. Datestamp is malformed in message '{}'" 23 | ERR_NAME_NOT_ALIAS = "Message validation error. The `name` value within each data point message must be the same string as `alias`: '{}'" 24 | ERR_MISSING_KEYS = "Message validation error. The following keys are missing from the message: '{}'" 25 | ERR_MSG_VALIDATION = "An error occurred validating message data: '{}'" 26 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_osipi_connector/pi_connector_sdk/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/machine-to-cloud-connectivity-framework/14c96676a8c0efc7d77ad5ebf823c54a3a18a32a/source/machine_connector/m2c2_osipi_connector/pi_connector_sdk/__init__.py -------------------------------------------------------------------------------- /source/machine_connector/m2c2_osipi_connector/pi_connector_sdk/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | MAX_RETRIES = 5 5 | DEFAULT_POINTS_START_INDEX = 0 6 | DEFAULT_POINTS_MAX_COUNT = 100 7 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_osipi_connector/pi_connector_sdk/enhanced_json_encoder.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import dataclasses 5 | import json 6 | 7 | 8 | class EnhancedJSONEncoder(json.JSONEncoder): 9 | def default(self, o): 10 | if dataclasses.is_dataclass(o): 11 | return dataclasses.asdict(o) 12 | return super().default(o) 13 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_osipi_connector/pi_connector_sdk/pi_connection_config.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from dataclasses import dataclass, field 5 | from typing import Union 6 | 7 | 8 | @dataclass 9 | class PiAuthParam(): 10 | 11 | def __init__(self): 12 | self.username: Union[str, None] = None 13 | self.password: Union[str, None] = None 14 | 15 | 16 | @dataclass 17 | class PiServerConnection: 18 | api_url: Union[str, None] = None 19 | server_name: Union[str, None] = None 20 | auth_mode: Union[str, None] = None 21 | verify_ssl: bool = True 22 | auth_param: PiAuthParam = field(default_factory=PiAuthParam) 23 | 24 | 25 | @dataclass 26 | class PiQueryConfig(): 27 | tag_names: 'list[str]' = field(default_factory=list) 28 | req_frequency_sec: float = 5 29 | catchup_req_frequency_sec: float = 0.1 30 | max_req_duration_sec: float = 60 # Might need to tweak based on data volume 31 | query_offset_from_now_sec: float = 0 32 | 33 | 34 | @dataclass 35 | class LocalStorageConfig(): 36 | 37 | storage_directory: Union[str, None] = None 38 | enabled: bool = False 39 | 40 | 41 | @dataclass 42 | class PiConnectionConfig: 43 | 44 | server_connection: PiServerConnection = field( 45 | default_factory=PiServerConnection) 46 | query_config: PiQueryConfig = field(default_factory=PiQueryConfig) 47 | local_storage_config: LocalStorageConfig = field( 48 | default_factory=LocalStorageConfig) 49 | log_level: str = "INFO" 50 | time_log_file: str = './data/timelog.txt' # TODO: MAKE DYNAMIC? 51 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_osipi_connector/pi_connector_sdk/pi_response.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from dataclasses import dataclass 5 | 6 | 7 | @dataclass 8 | class PiResponse: 9 | 10 | path: str 11 | name: str 12 | records: 'list[dict]' 13 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_osipi_connector/pi_connector_sdk/time_helper.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from datetime import datetime, timedelta, timezone 5 | from os import makedirs 6 | from os.path import exists, abspath 7 | from pathlib import Path 8 | from typing import Union 9 | 10 | 11 | class TimeHelper: 12 | def __init__(self, time_log_file: str): 13 | self.time_log_file = time_log_file 14 | 15 | def write_datetime_to_time_log(self, d_time) -> None: 16 | 17 | if not exists(self.time_log_file): 18 | log_dir = Path(self.time_log_file).parent 19 | makedirs(log_dir, exist_ok=True) 20 | 21 | with open(self.time_log_file, "w+") as f: 22 | timestamp = str(d_time) 23 | f.write(timestamp) 24 | 25 | def _get_time_from_time_log(self) -> Union[datetime, None]: 26 | 27 | if exists(self.time_log_file): 28 | with open(self.time_log_file, "r") as file: 29 | last_accessed_time_str = file.read() 30 | else: 31 | return None 32 | 33 | try: 34 | return datetime.fromisoformat(last_accessed_time_str) 35 | except Exception: 36 | # If file is empty or time invalid, return none so that parent function can take action 37 | return None 38 | 39 | def get_calculated_time_range(self, max_req_duration_sec: float, query_offset_from_now_sec: float = 0): 40 | 41 | start_time = self._get_time_from_time_log() 42 | 43 | if start_time is None: 44 | start_time = _get_current_utc_datetime( 45 | ) - timedelta(seconds=query_offset_from_now_sec + 1) 46 | self.write_datetime_to_time_log(start_time) 47 | 48 | end_time = _get_current_utc_datetime() - timedelta(seconds=query_offset_from_now_sec) 49 | 50 | time_diff = end_time - start_time 51 | time_diff_sec = time_diff.total_seconds() 52 | 53 | # Note: This can occur if the queryOffsetFromNow has changed. Need to handle getting times back in sync 54 | if (time_diff_sec < 0): 55 | start_time = end_time - timedelta(seconds=1) 56 | time_diff = end_time - start_time 57 | time_diff_sec = time_diff.total_seconds() 58 | 59 | is_offset_from_latest_request_query = False 60 | 61 | if time_diff_sec > max_req_duration_sec: 62 | end_time = start_time + timedelta(seconds=max_req_duration_sec) 63 | is_offset_from_latest_request_query = True 64 | 65 | return start_time, end_time, is_offset_from_latest_request_query 66 | 67 | 68 | def _get_current_utc_datetime(): 69 | return datetime.now(tz=timezone.utc) 70 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_osipi_connector/requirements.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/dcbark01/PI-Web-API-Client-Python.git@b620f72f2d2551632f406df44bd409f5cc305055 2 | 3 | requests_ntlm == 1.2.0 4 | greengrasssdk==1.6.1 5 | backoff==2.2.1 6 | awsiotsdk==1.17.0 -------------------------------------------------------------------------------- /source/machine_connector/m2c2_osipi_connector/tests/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = */tests/*,*/utils/* -------------------------------------------------------------------------------- /source/machine_connector/m2c2_osipi_connector/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/machine-to-cloud-connectivity-framework/14c96676a8c0efc7d77ad5ebf823c54a3a18a32a/source/machine_connector/m2c2_osipi_connector/tests/__init__.py -------------------------------------------------------------------------------- /source/machine_connector/m2c2_osipi_connector/validations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/machine-to-cloud-connectivity-framework/14c96676a8c0efc7d77ad5ebf823c54a3a18a32a/source/machine_connector/m2c2_osipi_connector/validations/__init__.py -------------------------------------------------------------------------------- /source/machine_connector/m2c2_osipi_connector/validations/m2c2_msg_types.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | class OsiPiMsgValidations: 5 | def __init__(self): 6 | """ 7 | This class establishes the expected configuration and validation 8 | of types used by the OSI PI collector and 9 | expected format for the publisher. 10 | """ 11 | pass 12 | 13 | def payload_required_keys(self) -> list: 14 | return ["alias", "messages"] 15 | 16 | def messages_required_keys(self) -> list: 17 | return ["name", "timestamp", "value", "quality"] 18 | 19 | def payload_validations(self) -> dict: 20 | return { 21 | "alias": lambda x: isinstance(x, str), 22 | "messages": lambda x: isinstance(x, list) 23 | } 24 | 25 | def quality_validations(self) -> list: 26 | return [ 27 | 'Good', 28 | 'GOOD', 29 | 'Bad', 30 | 'BAD', 31 | 'Uncertain', 32 | 'UNCERTAIN' 33 | ] 34 | 35 | def msgs_validations(self) -> dict: 36 | return { 37 | "name": lambda x: isinstance(x, str), 38 | "timestamp": lambda x: isinstance(x, str), 39 | "value": lambda x: x != None, 40 | "quality": lambda x: isinstance(x, str) and x in self.quality_validations() 41 | } 42 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/machine-to-cloud-connectivity-framework/14c96676a8c0efc7d77ad5ebf823c54a3a18a32a/source/machine_connector/m2c2_publisher/__init__.py -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/converters/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from .common_converter import CommonConverter 5 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/converters/common_converter.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import logging 5 | 6 | from utils.custom_exception import ConverterException 7 | 8 | 9 | class CommonConverter: 10 | def __init__(self, hierarchy): 11 | self.site_name = hierarchy["site_name"] 12 | self.area = hierarchy["area"] 13 | self.process = hierarchy["process"] 14 | self.machine_name = hierarchy["machine_name"] 15 | 16 | self.logger = logging.getLogger(self.__class__.__name__) 17 | self.logger.setLevel(logging.INFO) 18 | 19 | def add_metadata(self, payload: dict, tag: str) -> dict: 20 | """ 21 | This adds metadata to identify the source and tag of the payload 22 | """ 23 | try: 24 | self.key_list = [ 25 | "site_name", 26 | "area", 27 | "process", 28 | "machine_name", 29 | "tag" 30 | ] 31 | self.value_list = [ 32 | self.site_name, 33 | self.area, 34 | self.process, 35 | self.machine_name, 36 | tag 37 | ] 38 | self.metadata_dict = dict(zip(self.key_list, self.value_list)) 39 | payload.update(self.metadata_dict) 40 | return (payload) 41 | except Exception as err: 42 | self.logger.error( 43 | "An error has occurred in the common converter: {}".format(err)) 44 | raise ConverterException(err) 45 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/converters/historian/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/machine-to-cloud-connectivity-framework/14c96676a8c0efc7d77ad5ebf823c54a3a18a32a/source/machine_connector/m2c2_publisher/converters/historian/__init__.py -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/converters/historian/historian_converter.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import time 5 | from converters.historian.historian_message import HistorianMessage 6 | from boilerplate.logging.logger import get_logger 7 | 8 | 9 | class HistorianConverter: 10 | 11 | def __init__(self, source_id: str, collector_id: str): 12 | self.source_id = source_id 13 | self.collector_id = collector_id 14 | 15 | self.logger = get_logger(self.__class__.__name__) 16 | 17 | def convert_payload(self, payload: dict) -> list: 18 | self.logger.info(f"Converting payload for historian: {payload}") 19 | 20 | try: 21 | historian_messages = [] 22 | for message in payload['messages']: 23 | 24 | measurement_id = payload["tag"] 25 | # historian expects time in epoch form, not using timestamp coming from source 26 | timestamp = round(time.time() * 1000) 27 | quality = message['quality'] 28 | value = message['value'] 29 | 30 | historian_message = HistorianMessage(self.source_id, self.collector_id, 31 | measurement_id, timestamp, value, quality) 32 | 33 | historian_message_dict = historian_message.__dict__ 34 | historian_message_dict['@type'] = 'data' 35 | 36 | historian_messages.append( 37 | historian_message_dict 38 | ) 39 | 40 | self.logger.info( 41 | f"Total length of messages converted for historian: {len(historian_messages)}") 42 | 43 | return historian_messages 44 | 45 | except Exception as e: 46 | self.logger.error(f"Error converting payload: {e}") 47 | raise e 48 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/converters/historian/historian_message.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | class HistorianMessage: 5 | 6 | def __init__(self, source_id: str, collector_id: str, measurement_id: str, timestamp: str, value: any, quality: str): 7 | self.sourceId = source_id 8 | self.collectorId = collector_id 9 | self.measurementId = measurement_id 10 | self.measureName = measurement_id 11 | self.timestamp = timestamp 12 | self.value = value 13 | self.measureQuality = quality 14 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/converters/iot_topic_converter.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import logging 5 | 6 | from utils.custom_exception import ConverterException 7 | 8 | 9 | class IoTTopicConverter: 10 | 11 | def __init__(self, connection_name: str, protocol: str): 12 | self.connection_name = connection_name 13 | self.protocol = protocol 14 | 15 | self.logger = logging.getLogger(self.__class__.__name__) 16 | self.logger.setLevel(logging.INFO) 17 | 18 | def topic_converter(self, payload): 19 | try: 20 | iot_topic = "m2c2/data/{connection_name}/{machine_name}/{tag}".format( 21 | connection_name=self.connection_name, 22 | **payload 23 | ) 24 | return iot_topic 25 | except Exception as err: 26 | error_msg = "There was an error when trying to create the IoT topic: '{}'".format( 27 | err 28 | ) 29 | self.logger.error(error_msg) 30 | raise ConverterException(error_msg) 31 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/converters/tag_converter.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import logging 5 | 6 | 7 | class TagConverter: 8 | def __init__(self, protocol): 9 | self.protocol = protocol 10 | 11 | self.logger = logging.getLogger(self.__class__.__name__) 12 | self.logger.setLevel(logging.INFO) 13 | 14 | def convert_opcua_tag(self, payload): 15 | """ 16 | Converting the OPC-UA alias, representing the telemetry tag 17 | Replacing '/' with '_' and "." with "-" in tag 18 | """ 19 | self.payload = payload 20 | tag = self.payload["alias"].replace(".", "-").replace("/", "_") 21 | return tag 22 | 23 | def convert_opcda_tag(self, payload): 24 | """ 25 | Using the alias to pull out the tag for OPC-DA 26 | """ 27 | tag = payload["alias"].split('/')[-1] 28 | return tag 29 | 30 | def convert_osipi_tag(self, payload): 31 | """ 32 | Converting the OSI PI name, representing the telemetry tag 33 | Replacing '/' with '_' and "." with "-" in tag 34 | """ 35 | self.payload = payload 36 | tag = self.payload["alias"].replace(".", "-").replace("/", "_") 37 | return tag 38 | 39 | def convert_modbustcp_tag(self, payload): 40 | """ 41 | Using the alias to pull out the tag for modbustcp, tag is 42 | last part of alias designated as "(user custom tag)_(modbus command)_(secondary address)" 43 | """ 44 | tag = payload["alias"].split('/')[-1] 45 | return tag 46 | 47 | def retrieve_tag(self, payload): 48 | if self.protocol == "opcua": 49 | self.tag = self.convert_opcua_tag(payload) 50 | if self.protocol == "opcda": 51 | self.tag = self.convert_opcda_tag(payload) 52 | if self.protocol == "osipi": 53 | self.tag = self.convert_osipi_tag(payload) 54 | if self.protocol == "modbustcp": 55 | self.tag = self.convert_modbustcp_tag(payload) 56 | return self.tag 57 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/converters/timestream_converter.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import logging 5 | 6 | from dateutil import parser 7 | from utils.custom_exception import ConverterException 8 | 9 | 10 | class TimestreamConverter: 11 | def __init__(self) -> None: 12 | self.logger = logging.getLogger(self.__class__.__name__) 13 | self.logger.setLevel(logging.INFO) 14 | 15 | def convert_timestream_format(self, payload: dict) -> list: 16 | """ 17 | Converts the below solution format to below Timestream format: 18 | [ 19 | { 20 | "areaName": str, 21 | "machineName": str, 22 | "process": str, 23 | "quality": "Good" | "GOOD" | "Bad" | "BAD" | "Uncertain" | "UNCERTAIN", 24 | "site": str, 25 | "tag": str, 26 | "timestamp": Unix epoch time in ms, 27 | "value": various values 28 | } 29 | ] 30 | 31 | :param payload: The payload that the solution sends 32 | :return: The Kinesis records for the Timestream 33 | """ 34 | 35 | try: 36 | messages = payload.get("messages") 37 | metadata = { 38 | "site": payload.get("site_name"), 39 | "area": payload.get("area"), 40 | "process": payload.get("process"), 41 | "machine": payload.get("machine_name"), 42 | "tag": payload.get("tag") 43 | } 44 | records = [] 45 | 46 | for message in messages: 47 | records.append({ 48 | **metadata, 49 | "quality": message.get("quality"), 50 | "timestamp": parser.parse(message.get("timestamp")).timestamp() * 1000, 51 | "value": message.get("value"), 52 | }) 53 | 54 | return records 55 | except Exception as err: 56 | error_message = f"There was an issue converting the payload to solution format: {err}" 57 | self.logger.error(error_message) 58 | raise ConverterException(error_message) 59 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/requirements.txt: -------------------------------------------------------------------------------- 1 | greengrasssdk==1.6.1 2 | python-dateutil==2.8.1 3 | backoff==2.2.1 4 | awsiotsdk==1.18.0 -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/targets/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from .iot_topic_target import IoTTopicTarget 5 | from .kinesis_target import KinesisTarget 6 | from .sitewise_target import SiteWiseTarget 7 | from .historian_target import HistorianTarget 8 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/targets/iot_topic_target.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import logging 5 | 6 | from converters import common_converter, sitewise_converter, tag_converter, iot_topic_converter 7 | from utils.custom_exception import ConverterException 8 | from utils import AWSEndpointClient 9 | 10 | 11 | class IoTTopicTarget: 12 | def __init__(self, connection_name: str, protocol: str, hierarchy: dict): 13 | self.connection_name = connection_name 14 | self.protocol = protocol 15 | self.hierarchy = hierarchy 16 | self.tag_client = tag_converter.TagConverter(self.protocol) 17 | self.converter_client = common_converter.CommonConverter( 18 | self.hierarchy) 19 | self.sitewise_converter = sitewise_converter.SiteWiseConverter() 20 | self.topic_client = iot_topic_converter.IoTTopicConverter( 21 | self.connection_name, self.protocol) 22 | self.connector_client = AWSEndpointClient() 23 | 24 | self.logger = logging.getLogger(self.__class__.__name__) 25 | self.logger.setLevel(logging.INFO) 26 | 27 | def send_to_iot(self, payload: dict): 28 | try: 29 | self.payload = payload 30 | 31 | if self.protocol == "opcua": 32 | self.payload = self.sitewise_converter.convert_sitewise_format( 33 | payload 34 | ) 35 | 36 | self.tag = self.tag_client.retrieve_tag( 37 | self.payload 38 | ) 39 | self.payload = self.converter_client.add_metadata( 40 | self.payload, 41 | self.tag 42 | ) 43 | self.topic = self.topic_client.topic_converter(self.payload) 44 | self.connector_client.publish_message_to_iot_topic( 45 | self.topic, 46 | self.payload 47 | ) 48 | except ConverterException as err: 49 | raise err 50 | except Exception as err: 51 | self.logger.error( 52 | f"Failed to publish telemetry data to the IoT topic. Error: {err}") 53 | raise err 54 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/targets/sitewise_target.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import logging 5 | 6 | from converters.sitewise_converter import SiteWiseConverter 7 | from utils.stream_manager_helper import StreamManagerHelperClient 8 | 9 | 10 | class SiteWiseTarget: 11 | def __init__(self, protocol: str, sitewise_stream: str): 12 | self.protocol = protocol 13 | self.sitewise_stream = sitewise_stream 14 | self.sitewise_converter = SiteWiseConverter() 15 | self.sm_helper_client = StreamManagerHelperClient() 16 | 17 | self.logger = logging.getLogger(self.__class__.__name__) 18 | self.logger.setLevel(logging.INFO) 19 | 20 | def send_to_sitewise(self, payload: dict): 21 | try: 22 | self.payload = payload 23 | if self.protocol != "opcua": 24 | self.payload = self.sitewise_converter.sw_required_format( 25 | self.payload) 26 | self.sm_helper_client.write_to_stream( 27 | self.sitewise_stream, self.payload) 28 | except Exception as err: 29 | self.logger.error(f"Error raised when writing to SiteWise: {err}") 30 | raise err 31 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/tests/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = */tests/*,*/utils/* -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/machine-to-cloud-connectivity-framework/14c96676a8c0efc7d77ad5ebf823c54a3a18a32a/source/machine_connector/m2c2_publisher/tests/__init__.py -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/tests/test_historian_converter.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import unittest 5 | 6 | from converters.historian.historian_converter import HistorianConverter 7 | 8 | 9 | class TestHistorianConverter(unittest.TestCase): 10 | def setUp(self): 11 | pass 12 | 13 | def test_convert_payload(self): 14 | # Arrange 15 | historian_converter = HistorianConverter( 16 | "test-source-id", "test-collector-id") 17 | payload = { 18 | "tag": "test-tag", 19 | "messages": [ 20 | { 21 | "quality": "GOOD", 22 | "value": 100 23 | } 24 | ] 25 | } 26 | 27 | # Act 28 | converted_payload = historian_converter.convert_payload(payload) 29 | 30 | # Assert 31 | self.assertEqual(len(converted_payload), 1) 32 | self.assertEqual(converted_payload[0]['measurementId'], "test-tag") 33 | self.assertEqual(converted_payload[0]['sourceId'], "test-source-id") 34 | self.assertEqual(converted_payload[0] 35 | ['collectorId'], "test-collector-id") 36 | self.assertEqual(converted_payload[0]['value'], 100) 37 | self.assertEqual(converted_payload[0]['measureQuality'], "GOOD") 38 | self.assertEqual(converted_payload[0]['measureName'], "test-tag") 39 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/tests/test_iot_topic_converter.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import unittest 5 | 6 | from converters.iot_topic_converter import IoTTopicConverter 7 | 8 | 9 | class TestTopicConverter(unittest.TestCase): 10 | def setUp(self): 11 | self.connection_name = "test_job" 12 | self.protocol = "opcda" 13 | self.site_name = "test_site" 14 | self.area = "test_area" 15 | self.process = "test_process" 16 | self.machine_name = "test_machine_name" 17 | self.tag = "test_tag" 18 | self.payload = { 19 | "site_name": self.site_name, 20 | "area": self.area, 21 | "process": self.process, 22 | "machine_name": self.machine_name, 23 | "tag": self.tag 24 | } 25 | self.client = IoTTopicConverter(self.connection_name, self.protocol) 26 | 27 | def test_topic_converter(self): 28 | self.topic = self.client.topic_converter(self.payload) 29 | self.assertEqual( 30 | self.topic, f"m2c2/data/{self.connection_name}/{self.machine_name}/{self.tag}") 31 | 32 | def test_incomplete_payload(self): 33 | self.payload = { 34 | "site_name": self.site_name, 35 | "area": self.area 36 | } 37 | with self.assertRaises(Exception): 38 | self.client.topic_converter(self.payload) 39 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/tests/test_sitewise_target.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import unittest 5 | 6 | from unittest import mock 7 | from targets.sitewise_target import SiteWiseTarget 8 | 9 | 10 | @mock.patch("utils.stream_manager_helper.StreamManagerHelperClient.__init__", return_value=None) 11 | class TestSiteWiseTarget(unittest.TestCase): 12 | def setUp(self): 13 | self.sitewise_stream = "SiteWise_Stream" 14 | self.alias = "London/floor 1/packaging/messabout 2/Random-UInt4" 15 | self.value = 123 16 | self.sitewise_payload = { 17 | "propertyAlias": self.alias, 18 | "propertyValues": [ 19 | { 20 | "value": {"integerValue": self.value}, 21 | "timestamp": { 22 | "timeInSeconds": 1622733261, 23 | "offsetInNanos": 247000000 24 | }, 25 | "quality": "GOOD" 26 | } 27 | ] 28 | } 29 | self.opcda_payload = { 30 | "alias": self.alias, 31 | "messages": [ 32 | { 33 | "value": self.value, 34 | "quality": "Good", 35 | "timestamp": "2021-06-03 15:14:21.247000+00:00", 36 | "name": self.alias 37 | } 38 | ] 39 | } 40 | 41 | def test_send_opcda_data(self, mock_stream_manager_helper): 42 | sitewise_target = SiteWiseTarget("opcda", self.sitewise_stream) 43 | mock_sm_helper_client = mock_stream_manager_helper.MagicMock() 44 | sitewise_target.sm_helper_client = mock_sm_helper_client 45 | sitewise_target.sm_helper_client.write_to_stream = mock_stream_manager_helper.MagicMock() 46 | 47 | sitewise_target.send_to_sitewise(self.opcda_payload) 48 | self.assertDictEqual(sitewise_target.payload, self.sitewise_payload) 49 | 50 | def test_send_opcua_data(self, mock_stream_manager_helper): 51 | sitewise_target = SiteWiseTarget("opcua", self.sitewise_stream) 52 | mock_sm_helper_client = mock_stream_manager_helper.MagicMock() 53 | sitewise_target.sm_helper_client = mock_sm_helper_client 54 | sitewise_target.sm_helper_client.write_to_stream = mock_stream_manager_helper.MagicMock() 55 | 56 | sitewise_target.send_to_sitewise(self.sitewise_payload) 57 | self.assertDictEqual(sitewise_target.payload, self.sitewise_payload) 58 | 59 | def test_wrong_opcda_payload(self, mock_stream_manager_helper): 60 | sitewise_target = SiteWiseTarget("opcda", self.sitewise_stream) 61 | 62 | with self.assertRaises(Exception): 63 | sitewise_target.send_to_sitewise({}) 64 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/tests/test_tag_converter.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import unittest 5 | 6 | from converters.tag_converter import TagConverter 7 | 8 | 9 | class TestTagConverter(unittest.TestCase): 10 | def setUp(self): 11 | self.protocol_opcua = "opcua" 12 | self.protocol_opcda = "opcda" 13 | self.opcua_alias = "/RealTime/TestMetric" 14 | self.opcda_alias = "/Site/Area/Process/Machine/TestMetric" 15 | 16 | def test_tag_converter_opcua(self): 17 | self.opcua_client = TagConverter(self.protocol_opcua) 18 | self.payload = {"alias": self.opcua_alias} 19 | self.tag = self.opcua_client.retrieve_tag(self.payload) 20 | self.assertEqual(self.tag, "_RealTime_TestMetric") 21 | 22 | def test_tag_converter_opcda(self): 23 | self.opcda_client = TagConverter(self.protocol_opcda) 24 | self.payload = {"alias": self.opcda_alias} 25 | self.tag = self.opcda_client.retrieve_tag(self.payload) 26 | self.assertEqual(self.tag, "TestMetric") 27 | -------------------------------------------------------------------------------- /source/machine_connector/m2c2_publisher/tests/test_timestream_converter.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import unittest 5 | 6 | from dateutil import parser 7 | from converters.timestream_converter import TimestreamConverter 8 | from utils.custom_exception import ConverterException 9 | 10 | 11 | class TestTimestreamConverter(unittest.TestCase): 12 | def setUp(self): 13 | self.timestream_converter = TimestreamConverter() 14 | self.hierarchy = { 15 | "site_name": "site", 16 | "area": "area", 17 | "process": "process", 18 | "machine_name": "machine", 19 | "tag": "MockTag", 20 | } 21 | self.timestamp = "2022-03-14 12:34:56.789000+00:00" 22 | 23 | def test_convert_timestream_format(self): 24 | payload = { 25 | "alias": "/site/area/process/machine/MockTag", 26 | "messages": [ 27 | { 28 | "name": "/site/area/process/machine/MockTag", 29 | "quality": "Good", 30 | "value": "mock", 31 | "timestamp": self.timestamp 32 | } 33 | ], 34 | **self.hierarchy 35 | } 36 | kinesis_records = self.timestream_converter.convert_timestream_format( 37 | payload 38 | ) 39 | self.assertListEqual(kinesis_records, [{ 40 | "site": payload.get("site_name"), 41 | "area": payload.get("area"), 42 | "process": payload.get("process"), 43 | "machine": payload.get("machine_name"), 44 | "tag": payload.get("tag"), 45 | "quality": "Good", 46 | "timestamp": parser.parse(self.timestamp).timestamp() * 1000, 47 | "value": "mock" 48 | }]) 49 | 50 | def test_convert_timestream_format_error(self): 51 | with self.assertRaises(ConverterException): 52 | self.timestream_converter.convert_timestream_format({ 53 | "messages": [{ 54 | "timestamp": "invalid_timestamp_format" 55 | }] 56 | }) 57 | -------------------------------------------------------------------------------- /source/machine_connector/requirements_dev.txt: -------------------------------------------------------------------------------- 1 | backoff==2.2.1 2 | pytest==7.2.0 3 | pytest-cov==4.1.0 4 | pytest-mock==3.11.1 5 | python-dateutil==2.8.1 6 | awsiotsdk==1.18.0 7 | greengrasssdk==1.6.1 8 | Pyro4==4.81 9 | OpenOPC-Python3x==1.3.1 10 | 11 | git+https://github.com/dcbark01/PI-Web-API-Client-Python.git@b620f72f2d2551632f406df44bd409f5cc305055 12 | 13 | requests_ntlm == 1.2.0 -------------------------------------------------------------------------------- /source/machine_connector/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from .client import AWSEndpointClient 5 | from .pickle_checkpoint_manager import PickleCheckpointManager 6 | from .stream_manager_helper import StreamManagerHelperClient 7 | from .init_msg_metadata import InitMessage 8 | 9 | __version__ = "4.2.3" 10 | -------------------------------------------------------------------------------- /source/machine_connector/utils/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | WORK_BASE_DIR = "/greengrass/v2/work" 5 | -------------------------------------------------------------------------------- /source/machine_connector/utils/custom_exception.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | class FileException(Exception): 5 | """Machine to Cloud Connectivity utils file exception""" 6 | pass 7 | 8 | 9 | class ConnectorException(Exception): 10 | """Machine to Cloud Connectivity Framework connector exception""" 11 | pass 12 | 13 | 14 | class ConverterException(Exception): 15 | """Machine to Cloud Connectivity Framework converter exception""" 16 | pass 17 | 18 | 19 | class StreamManagerHelperException(Exception): 20 | """Machine to Cloud Connectivity Framework stream manager helper exception""" 21 | pass 22 | 23 | 24 | class ValidationException(Exception): 25 | """Machine to Cloud Connectivity Framework validation exception""" 26 | pass 27 | 28 | 29 | class PublisherException(Exception): 30 | """Machine to Cloud Connectivity Framework publisher exception""" 31 | pass 32 | -------------------------------------------------------------------------------- /source/machine_connector/utils/init_msg_metadata.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import os 5 | 6 | 7 | class InitMessage: 8 | 9 | def __init__(self): 10 | # Site name from Greengrass Lambda Environment variables 11 | self.SITE_NAME = os.environ["SITE_NAME"] 12 | # Area from Greengrass Lambda Environment variables 13 | self.AREA = os.environ["AREA"] 14 | # Process from Greengrass Lambda Environment variables 15 | self.PROCESS = os.environ["PROCESS"] 16 | # Machine name from Greengrass Lambda Environment variables 17 | self.MACHINE_NAME = os.environ["MACHINE_NAME"] 18 | 19 | def init_user_message(self) -> dict: 20 | self.user_message = {} 21 | self.user_message["siteName"] = self.SITE_NAME 22 | self.user_message["area"] = self.AREA 23 | self.user_message["process"] = self.PROCESS 24 | self.user_message["machineName"] = self.MACHINE_NAME 25 | return (self.user_message) 26 | -------------------------------------------------------------------------------- /source/machine_connector/utils/requirements.txt: -------------------------------------------------------------------------------- 1 | backoff==2.2.1 2 | awsiotsdk==1.18.0 3 | greengrasssdk==1.6.1 -------------------------------------------------------------------------------- /source/machine_connector/utils/subscription_stream_handler.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import json 5 | import logging 6 | import awsiot.greengrasscoreipc.client as client 7 | 8 | from typing import Callable 9 | from awsiot.greengrasscoreipc.model import IoTCoreMessage 10 | 11 | 12 | class SubscriptionStreamHandler(client.SubscribeToIoTCoreStreamHandler): 13 | """ 14 | IoT Core stream handler class. This handles message from the MQTT topic from the cloud. 15 | 16 | For more information, refer to 17 | https://docs.aws.amazon.com/greengrass/v2/developerguide/ipc-iot-core-mqtt.html#ipc-operation-subscribetoiotcore 18 | """ 19 | 20 | def __init__(self, message_handler_callback: Callable[[dict], None]): 21 | """ 22 | :param message_handler_callback: The callback method when a message comes to the MQTT topic 23 | """ 24 | 25 | super().__init__() 26 | self.logger = logging.getLogger(self.__class__.__name__) 27 | self.logger.setLevel(logging.INFO) 28 | self.message_handler_callback = message_handler_callback 29 | 30 | def on_stream_event(self, event: IoTCoreMessage) -> None: 31 | """ 32 | Handles the message. Since the message is going to be `bytes`, it converts the message to dict. 33 | After that, it calls the message callback method with the dict. 34 | 35 | :param event: The event message from the cloud 36 | """ 37 | 38 | message = json.loads(str(event.message.payload, "utf-8")) 39 | self.message_handler_callback(message) 40 | 41 | def on_stream_error(self, error: Exception) -> bool: 42 | """ 43 | Handles the stream error. 44 | Return True to close stream, False to keep the stream open. 45 | 46 | :param error: The error 47 | """ 48 | 49 | self.logger.error( 50 | f"Error occurred on the subscription stream: {error}" 51 | ) 52 | return True 53 | 54 | def on_stream_closed(self) -> None: 55 | """ 56 | Handles close the stream. Basically, it does nothing by default. 57 | """ 58 | pass 59 | -------------------------------------------------------------------------------- /source/machine_connector/utils/tests/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = */tests/* -------------------------------------------------------------------------------- /source/machine_connector/utils/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/machine-to-cloud-connectivity-framework/14c96676a8c0efc7d77ad5ebf823c54a3a18a32a/source/machine_connector/utils/tests/__init__.py -------------------------------------------------------------------------------- /source/machine_connector/utils/tests/test_init_msg_metadata.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import os 5 | from unittest import mock 6 | 7 | 8 | @mock.patch.dict(os.environ, {"SITE_NAME": "test_site", "AREA": "test_area", "PROCESS": "test_process", "MACHINE_NAME": "test_machine_name"}) 9 | def test_init_user_message_init(mocker): 10 | mocker.patch("pickle_checkpoint_manager.PickleCheckpointManager") 11 | from init_msg_metadata import InitMessage 12 | msg_metadata_client = InitMessage() 13 | assert msg_metadata_client.SITE_NAME == "test_site" 14 | assert msg_metadata_client.AREA == "test_area" 15 | assert msg_metadata_client.PROCESS == "test_process" 16 | assert msg_metadata_client.MACHINE_NAME == "test_machine_name" 17 | 18 | 19 | @mock.patch.dict(os.environ, {"SITE_NAME": "test_site", "AREA": "test_area", "PROCESS": "test_process", "MACHINE_NAME": "test_machine_name"}) 20 | def test_init_user_message_usrmsg(mocker): 21 | mocker.patch("pickle_checkpoint_manager.PickleCheckpointManager") 22 | from init_msg_metadata import InitMessage 23 | msg_metadata_client = InitMessage() 24 | test_user_message = msg_metadata_client.init_user_message() 25 | expected_user_message = { 26 | "siteName": "test_site", 27 | "area": "test_area", 28 | "process": "test_process", 29 | "machineName": "test_machine_name" 30 | } 31 | assert "siteName" in test_user_message 32 | assert "area" in test_user_message 33 | assert "process" in test_user_message 34 | assert "machineName" in test_user_message 35 | assert test_user_message == expected_user_message 36 | assert test_user_message["siteName"] == expected_user_message["siteName"] 37 | assert test_user_message["area"] == expected_user_message["area"] 38 | assert test_user_message["process"] == expected_user_message["process"] 39 | assert test_user_message["machineName"] == expected_user_message["machineName"] 40 | -------------------------------------------------------------------------------- /source/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "machine-to-cloud-connectivity-framework-source", 3 | "version": "4.2.3", 4 | "description": "Machine to Cloud Connectivity Framework ESLint and prettier", 5 | "private": true, 6 | "license": "Apache-2.0", 7 | "author": { 8 | "name": "Amazon Web Services", 9 | "url": "https://aws.amazon.com/solutions" 10 | }, 11 | "scripts": { 12 | "clean": "rm -rf node_modules yarn.lock package-lock.json", 13 | "clean:install": "yarn clean && yarn install", 14 | "lint": "yarn run eslint ./", 15 | "clean:lint": "yarn clean:install && yarn lint" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^20.4.5", 19 | "@typescript-eslint/eslint-plugin": "^6.2.0", 20 | "@typescript-eslint/parser": "^6.2.0", 21 | "eslint": "^8.45.0", 22 | "eslint-config-prettier": "^9.0.0", 23 | "eslint-config-react-app": "^7.0.1", 24 | "eslint-plugin-header": "^3.1.1", 25 | "eslint-plugin-import": "^2.27.5", 26 | "eslint-plugin-jsdoc": "^46.4.4", 27 | "eslint-plugin-node": "^11.1.0", 28 | "eslint-plugin-prettier": "^5.0.0", 29 | "eslint-plugin-react": "^7.33.0", 30 | "eslint-plugin-react-hooks": "^4.6.0", 31 | "prettier": "^3.0.0", 32 | "typescript": "~5.1.6" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /source/ui/.env: -------------------------------------------------------------------------------- 1 | INLINE_RUNTIME_CHUNK=false 2 | GENERATE_SOURCEMAP=false 3 | DISABLE_ESLINT_PLUGIN=true 4 | -------------------------------------------------------------------------------- /source/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "machine-to-cloud-connectivity-framework-ui", 3 | "license": "Apache-2.0", 4 | "description": "Machine to Cloud Connectivity Framework UI", 5 | "author": { 6 | "name": "Amazon Web Services", 7 | "url": "https://aws.amazon.com/solutions" 8 | }, 9 | "version": "4.2.3", 10 | "private": true, 11 | "devDependencies": { 12 | "@aws-amplify/api": "4.0.40", 13 | "@aws-amplify/auth": "4.5.4", 14 | "@aws-amplify/core": "4.5.4", 15 | "@aws-amplify/storage": "4.4.23", 16 | "@aws-amplify/ui-react": "2.17.1", 17 | "@testing-library/jest-dom": "^5.16.3", 18 | "@testing-library/react": "^12.1.4", 19 | "@testing-library/user-event": "^13.5.0", 20 | "@types/jest": "^27.4.1", 21 | "@types/react-dom": "^17.0.14", 22 | "@types/react-router-bootstrap": "^0.24.5", 23 | "@types/react-router-dom": "^5.3.3", 24 | "aws-amplify": "4.3.22", 25 | "bootstrap": "~4.6.0", 26 | "bootstrap-icons": "^1.8.1", 27 | "react": "~17.0.2", 28 | "react-bootstrap": "~1.6.4", 29 | "react-dom": "~17.0.2", 30 | "react-router-bootstrap": "~0.26.1", 31 | "react-router-dom": "~6.2.2", 32 | "react-scripts": "~5.0.0", 33 | "sass": "~1.49.9", 34 | "typescript": "~4.6.3" 35 | }, 36 | "resolutions": { 37 | "@types/react": "17.0.65", 38 | "fast-xml-parser": "4.2.6", 39 | "nth-check": "2.1.1", 40 | "crypto-js": "4.2.0" 41 | }, 42 | "scripts": { 43 | "start": "react-scripts start", 44 | "clean": "rm -rf node_modules && rm -rf build", 45 | "build:react": "react-scripts build", 46 | "build": "yarn clean && yarn install && yarn build:react", 47 | "test": "react-scripts test --coverage --passWithNoTests --watchAll=false --silent", 48 | "eject": "react-scripts eject" 49 | }, 50 | "browserslist": { 51 | "production": [ 52 | ">0.2%", 53 | "not dead", 54 | "not op_mini all" 55 | ], 56 | "development": [ 57 | "last 1 chrome version", 58 | "last 1 firefox version", 59 | "last 1 safari version" 60 | ] 61 | }, 62 | "jest": { 63 | "collectCoverageFrom": [ 64 | "src/**/*.{ts,tsx}", 65 | "!src/react-app-env.d.ts", 66 | "!src/index.tsx" 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /source/ui/public/index.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | Machine to Cloud Connectivity Framework 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /source/ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "M2C2", 3 | "name": "Machine to Cloud Connectivity Framework", 4 | "icons": [], 5 | "start_url": ".", 6 | "display": "standalone", 7 | "theme_color": "#000000", 8 | "background_color": "#ffffff" 9 | } -------------------------------------------------------------------------------- /source/ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Amplify } from 'aws-amplify'; 5 | import { withAuthenticator } from '@aws-amplify/ui-react'; 6 | import { BrowserRouter, Route, Routes } from 'react-router-dom'; 7 | import Header from './components/Header'; 8 | import PageNotFound from './components/PageNotFound'; 9 | import { AmplifyConfigurationInput } from './util/types'; 10 | import { getAmplifyConfiguration } from './util/utils'; 11 | import Dashboard from './views/connection/Dashboard'; 12 | import ConnectionForm from './views/connection/ConnectionForm'; 13 | import GreengrassCoreDevicesDashboard from './views/greengrass/GreengrassCoreDevicesDashboard'; 14 | import GreengrassCoreDeviceForm from './views/greengrass/GreengrassCoreDeviceForm'; 15 | 16 | // Amplify configuration 17 | type UiWindow = Window & typeof globalThis & { config: AmplifyConfigurationInput }; 18 | const config: AmplifyConfigurationInput = (window as UiWindow).config; 19 | Amplify.Logger.LOG_LEVEL = config.loggingLevel; 20 | Amplify.configure(getAmplifyConfiguration(config)); 21 | 22 | /** 23 | * The default application 24 | * @returns Amplify Authenticator with Main and Footer 25 | */ 26 | function App(): React.JSX.Element { 27 | return ( 28 |
29 | 30 |
31 | 32 | } /> 33 | } /> 34 | } /> 35 | } /> 36 | } /> 37 | } /> 38 | 39 | 40 |
41 | ); 42 | } 43 | 44 | export default withAuthenticator(App); 45 | -------------------------------------------------------------------------------- /source/ui/src/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * General CSS 6 | */ 7 | html * { 8 | font-family: "Amazon Ember", "Helvetica Neue", Arial, Helvetica, sans-serif !important; 9 | } 10 | 11 | body { 12 | -webkit-font-smoothing: antialiased; 13 | } 14 | 15 | .container { 16 | max-width: 95% !important; 17 | padding: 15px 0 0 0 ; 18 | margin-right: auto; 19 | margin-left: auto; 20 | } 21 | 22 | .btn-primary { 23 | background-color: #FF9900 !important; 24 | border-color: #FF9900 !important; 25 | } 26 | 27 | .btn-link { 28 | color: #007EB9 !important; 29 | } 30 | 31 | .btn-small { 32 | font-size: 14px !important; 33 | } 34 | 35 | .uppercase-text { 36 | text-transform: uppercase; 37 | } 38 | 39 | .red-text { 40 | color: red; 41 | } 42 | 43 | .empty-p { 44 | margin: auto; 45 | } 46 | 47 | .grid { 48 | display: grid; 49 | } 50 | 51 | /** 52 | * Dashboard CSS 53 | */ 54 | .btn-manage-connection { 55 | padding-top: 0px; 56 | padding-left: 0px; 57 | padding-bottom: 0px; 58 | padding-right: 0px; 59 | } 60 | 61 | .btn-manage-connection-icon { 62 | font-size: 20px !important; 63 | color: #212529; 64 | } 65 | 66 | .dropdown-button { 67 | height: 31px; 68 | align-items: center; 69 | } 70 | 71 | .dropdown-toggle::after { 72 | content: none !important; 73 | } 74 | 75 | .text-align-center { 76 | text-align: center; 77 | } -------------------------------------------------------------------------------- /source/ui/src/components/EmptyCol.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * EmptyCol returns an empty col. 6 | * @returns An empty col 7 | */ 8 | export default function EmptyCol(): React.JSX.Element { 9 | return  ; 10 | } 11 | -------------------------------------------------------------------------------- /source/ui/src/components/EmptyRow.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import Row from 'react-bootstrap/Row'; 5 | import Col from 'react-bootstrap/Col'; 6 | import EmptyCol from './EmptyCol'; 7 | 8 | /** 9 | * EmptyRow returns an empty row. 10 | * @returns An empty row 11 | */ 12 | export default function EmptyRow(): React.JSX.Element { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /source/ui/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { I18n } from '@aws-amplify/core'; 5 | import Button from 'react-bootstrap/Button'; 6 | import Nav from 'react-bootstrap/Nav'; 7 | import Navbar from 'react-bootstrap/Navbar'; 8 | import { Link } from 'react-router-dom'; 9 | import { signOut } from '../util/utils'; 10 | 11 | /** 12 | * Renders the header of the UI. 13 | * @returns The header 14 | */ 15 | export default function Header(): React.JSX.Element { 16 | return ( 17 |
18 | 19 | {I18n.get('application')} 20 | 21 | 22 | 30 | 33 | 34 | 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /source/ui/src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import ProgressBar from 'react-bootstrap/ProgressBar'; 5 | import Spinner from 'react-bootstrap/Spinner'; 6 | import EmptyCol from './EmptyCol'; 7 | 8 | type LoadingProp = { 9 | loading: boolean; 10 | }; 11 | 12 | /** 13 | * Renders loading bar. 14 | * @param props The loading property 15 | * @returns Loading bar 16 | */ 17 | export function LoadingProgressBar(props: LoadingProp): React.JSX.Element { 18 | if (props.loading) return ; 19 | else return <>; 20 | } 21 | 22 | /** 23 | * Renders loading spinner. 24 | * @param props The loading property 25 | * @returns Loading spinner 26 | */ 27 | export function LoadingSpinner(props: LoadingProp): React.JSX.Element { 28 | if (props.loading) { 29 | return ( 30 | <> 31 |