diff --git a/.doc_gen/metadata/iot-data_metadata.yaml b/.doc_gen/metadata/iot-data_metadata.yaml index f8469e86bcd..5343e895e05 100644 --- a/.doc_gen/metadata/iot-data_metadata.yaml +++ b/.doc_gen/metadata/iot-data_metadata.yaml @@ -1,6 +1,14 @@ # zexi 0.4.0 iot-data-plane_GetThingShadow: languages: + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.GetThingShadow Kotlin: versions: - sdk_version: 1 @@ -30,6 +38,14 @@ iot-data-plane_GetThingShadow: iot-data-plane: {GetThingShadow} iot-data-plane_UpdateThingShadow: languages: + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.UpdateThingShadow Kotlin: versions: - sdk_version: 1 diff --git a/.doc_gen/metadata/iot_metadata.yaml b/.doc_gen/metadata/iot_metadata.yaml index 6339c137a6f..ba89253bd68 100644 --- a/.doc_gen/metadata/iot_metadata.yaml +++ b/.doc_gen/metadata/iot_metadata.yaml @@ -22,6 +22,14 @@ iot_Hello: - description: snippet_tags: - iot.java2.hello_iot.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.Hello C++: versions: - sdk_version: 1 @@ -55,6 +63,14 @@ iot_DescribeEndpoint: - description: snippet_tags: - iot.java2.describe.endpoint.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.DescribeEndpoint Rust: versions: - sdk_version: 1 @@ -75,6 +91,14 @@ iot_DescribeEndpoint: iot: {DescribeEndpoint} iot_ListThings: languages: + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.ListThings Rust: versions: - sdk_version: 1 @@ -104,6 +128,14 @@ iot_ListCertificates: - description: snippet_tags: - iot.java2.list.certs.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.ListCertificates C++: versions: - sdk_version: 1 @@ -133,6 +165,14 @@ iot_CreateKeysAndCertificate: - description: snippet_tags: - iot.java2.create.cert.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.CreateKeysAndCertificate C++: versions: - sdk_version: 1 @@ -162,6 +202,14 @@ iot_DeleteCertificate: - description: snippet_tags: - iot.java2.delete.cert.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.DeleteCertificate C++: versions: - sdk_version: 1 @@ -191,6 +239,14 @@ iot_SearchIndex: - description: snippet_tags: - iot.java2.search.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.SearchIndex C++: versions: - sdk_version: 1 @@ -232,6 +288,14 @@ iot_DeleteThing: - description: snippet_tags: - iot.java2.delete.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.DeleteThing C++: versions: - sdk_version: 1 @@ -290,6 +354,14 @@ iot_AttachThingPrincipal: - description: snippet_tags: - iot.java2.attach.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.AttachThingPrincipal C++: versions: - sdk_version: 1 @@ -319,6 +391,14 @@ iot_DetachThingPrincipal: - description: snippet_tags: - iot.java2.detach.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.DetachThingPrincipal C++: versions: - sdk_version: 1 @@ -340,14 +420,14 @@ iot_UpdateThing: - description: snippet_tags: - iot.kotlin.update.thing.main - Java: + .NET: versions: - - sdk_version: 2 - github: javav2/example_code/iot + - sdk_version: 4 + github: dotnetv4/IoT excerpts: - description: snippet_tags: - - iot.java2.update.shadow.thing.main + - iot.dotnetv4.UpdateThing C++: versions: - sdk_version: 1 @@ -377,6 +457,14 @@ iot_CreateTopicRule: - description: snippet_tags: - iot.java2.create.rule.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.CreateTopicRule C++: versions: - sdk_version: 1 @@ -418,6 +506,14 @@ iot_CreateThing: - description: snippet_tags: - iot.java2.create.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.CreateThing C++: versions: - sdk_version: 1 @@ -435,7 +531,7 @@ iot_Scenario: - Update an &IoT; Thing with Attributes. - Return a unique endpoint. - List your &IoT; certificates. - - Create an &IoT; shadow. + - Update an &IoT; shadow. - Write out state information. - Creates a rule. - List your rules. @@ -464,6 +560,17 @@ iot_Scenario: - description: A wrapper class for &IoT; SDK methods. snippet_tags: - iot.java2.scenario.actions.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: Run an interactive scenario demonstrating &IoT; features. + snippet_tags: + - iot.dotnetv4.IoTScenario + - description: A wrapper class for &IoT; SDK methods. + snippet_tags: + - iot.dotnetv4.IoTWrapper C++: versions: - sdk_version: 1 diff --git a/cpp/example_code/iot/README.md b/cpp/example_code/iot/README.md index 9497e020860..efdfa9986ae 100644 --- a/cpp/example_code/iot/README.md +++ b/cpp/example_code/iot/README.md @@ -106,7 +106,7 @@ This example shows you how to do the following: - Update an AWS IoT Thing with Attributes. - Return a unique endpoint. - List your AWS IoT certificates. -- Create an AWS IoT shadow. +- Update an AWS IoT shadow. - Write out state information. - Creates a rule. - List your rules. diff --git a/dotnetv4/DotNetV4Examples.sln b/dotnetv4/DotNetV4Examples.sln index eb4a4fe4569..f05efed7816 100644 --- a/dotnetv4/DotNetV4Examples.sln +++ b/dotnetv4/DotNetV4Examples.sln @@ -161,6 +161,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedshiftBasics", "Redshift\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedshiftTests", "Redshift\Tests\RedshiftTests.csproj", "{1DB37AC5-18FE-4535-816C-827B4D3DCB96}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IoT", "IoT", "{AF6DCE90-C605-41AE-A37C-770E2D654E9F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IoTTests", "IoT\Tests\IoTTests.csproj", "{D25392A3-A6F2-6BA8-9478-B47D192BC2D1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IoTBasics", "IoT\Scenarios\IoTBasics.csproj", "{D739A0A7-8A33-1CC5-31AC-15B2497BD130}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IoTActions", "IoT\Actions\IoTActions.csproj", "{0FA755B3-F5B4-F551-504E-5EE86DBF1719}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -795,6 +803,42 @@ Global {1DB37AC5-18FE-4535-816C-827B4D3DCB96}.Release|x64.Build.0 = Release|Any CPU {1DB37AC5-18FE-4535-816C-827B4D3DCB96}.Release|x86.ActiveCfg = Release|Any CPU {1DB37AC5-18FE-4535-816C-827B4D3DCB96}.Release|x86.Build.0 = Release|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Debug|x64.ActiveCfg = Debug|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Debug|x64.Build.0 = Debug|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Debug|x86.ActiveCfg = Debug|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Debug|x86.Build.0 = Debug|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Release|Any CPU.Build.0 = Release|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Release|x64.ActiveCfg = Release|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Release|x64.Build.0 = Release|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Release|x86.ActiveCfg = Release|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Release|x86.Build.0 = Release|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Debug|x64.ActiveCfg = Debug|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Debug|x64.Build.0 = Debug|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Debug|x86.ActiveCfg = Debug|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Debug|x86.Build.0 = Debug|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Release|Any CPU.Build.0 = Release|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Release|x64.ActiveCfg = Release|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Release|x64.Build.0 = Release|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Release|x86.ActiveCfg = Release|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Release|x86.Build.0 = Release|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Debug|x64.ActiveCfg = Debug|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Debug|x64.Build.0 = Debug|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Debug|x86.ActiveCfg = Debug|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Debug|x86.Build.0 = Debug|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Release|Any CPU.Build.0 = Release|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Release|x64.ActiveCfg = Release|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Release|x64.Build.0 = Release|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Release|x86.ActiveCfg = Release|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -863,6 +907,9 @@ Global {4E74F2DB-3BA5-4390-8FBF-C58F57601671} = {BC1690DE-FD9E-72EA-CAED-A2B9A3D6B335} {A30F8E57-AF18-40EC-B130-140585246CC7} = {BC1690DE-FD9E-72EA-CAED-A2B9A3D6B335} {1DB37AC5-18FE-4535-816C-827B4D3DCB96} = {BC1690DE-FD9E-72EA-CAED-A2B9A3D6B335} + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1} = {AF6DCE90-C605-41AE-A37C-770E2D654E9F} + {D739A0A7-8A33-1CC5-31AC-15B2497BD130} = {AF6DCE90-C605-41AE-A37C-770E2D654E9F} + {0FA755B3-F5B4-F551-504E-5EE86DBF1719} = {AF6DCE90-C605-41AE-A37C-770E2D654E9F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {08502818-E8E1-4A91-A51C-4C8C8D4FF9CA} diff --git a/dotnetv4/IoT/Actions/HelloIoT.cs b/dotnetv4/IoT/Actions/HelloIoT.cs new file mode 100644 index 00000000000..7f9cfd02f53 --- /dev/null +++ b/dotnetv4/IoT/Actions/HelloIoT.cs @@ -0,0 +1,93 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.IoT; +using Amazon.IoT.Model; + +namespace IoTActions; + +// snippet-start:[iot.dotnetv4.Hello] +/// +/// Hello AWS IoT example. +/// +public class HelloIoT +{ + /// + /// Main method to run the Hello IoT example. + /// + /// Command line arguments. + /// A Task object. + public static async Task Main(string[] args) + { + var iotClient = new AmazonIoTClient(); + + try + { + Console.WriteLine("Hello AWS IoT! Let's list your IoT Things:"); + Console.WriteLine(new string('-', 80)); + + // Use pages of 10. + var request = new ListThingsRequest() + { + MaxResults = 10 + }; + var response = await iotClient.ListThingsAsync(request); + + // Since there is not a built-in paginator, use the NextMarker to paginate. + bool hasMoreResults = true; + + var things = new List(); + while (hasMoreResults) + { + things.AddRange(response.Things); + + // If NextMarker is not null, there are more results. Get the next page of results. + if (!String.IsNullOrEmpty(response.NextMarker)) + { + request.Marker = response.NextMarker; + response = await iotClient.ListThingsAsync(request); + } + else + hasMoreResults = false; + } + + if (things is { Count: > 0 }) + { + Console.WriteLine($"Found {things.Count} IoT Things:"); + foreach (var thing in things) + { + Console.WriteLine($"- Thing Name: {thing.ThingName}"); + Console.WriteLine($" Thing ARN: {thing.ThingArn}"); + Console.WriteLine($" Thing Type: {thing.ThingTypeName ?? "No type specified"}"); + Console.WriteLine($" Version: {thing.Version}"); + + if (thing.Attributes?.Count > 0) + { + Console.WriteLine(" Attributes:"); + foreach (var attr in thing.Attributes) + { + Console.WriteLine($" {attr.Key}: {attr.Value}"); + } + } + Console.WriteLine(); + } + } + else + { + Console.WriteLine("No IoT Things found in your account."); + Console.WriteLine("You can create IoT Things using the IoT Basics scenario example."); + } + + Console.WriteLine("Hello IoT completed successfully."); + } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + Console.WriteLine($"Request throttled, please try again later: {ex.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"Couldn't list Things. Here's why: {ex.Message}"); + } + } +} +// snippet-end:[iot.dotnetv4.Hello] \ No newline at end of file diff --git a/dotnetv4/IoT/Actions/IoTActions.csproj b/dotnetv4/IoT/Actions/IoTActions.csproj new file mode 100644 index 00000000000..8a0ab2e6143 --- /dev/null +++ b/dotnetv4/IoT/Actions/IoTActions.csproj @@ -0,0 +1,24 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/dotnetv4/IoT/Actions/IoTWrapper.cs b/dotnetv4/IoT/Actions/IoTWrapper.cs new file mode 100644 index 00000000000..2fe0ee8f90f --- /dev/null +++ b/dotnetv4/IoT/Actions/IoTWrapper.cs @@ -0,0 +1,682 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.IoT; +using Amazon.IoT.Model; +using Amazon.IotData; +using Amazon.IotData.Model; +using Microsoft.Extensions.Logging; + +namespace IoTActions; + +// snippet-start:[iot.dotnetv4.IoTWrapper] +/// +/// Wrapper methods to use Amazon IoT Core with .NET. +/// +public class IoTWrapper +{ + private readonly IAmazonIoT _amazonIoT; + private readonly IAmazonIotData _amazonIotData; + private readonly ILogger _logger; + + /// + /// Constructor for the IoT wrapper. + /// + /// The injected IoT client. + /// The injected IoT Data client. + /// The injected logger. + public IoTWrapper(IAmazonIoT amazonIoT, IAmazonIotData amazonIotData, ILogger logger) + { + _amazonIoT = amazonIoT; + _amazonIotData = amazonIotData; + _logger = logger; + } + + // snippet-start:[iot.dotnetv4.CreateThing] + /// + /// Creates an AWS IoT Thing. + /// + /// The name of the Thing to create. + /// The ARN of the Thing created, or null if creation failed. + public async Task CreateThingAsync(string thingName) + { + try + { + var request = new CreateThingRequest + { + ThingName = thingName + }; + + var response = await _amazonIoT.CreateThingAsync(request); + _logger.LogInformation($"Created Thing {thingName} with ARN {response.ThingArn}"); + return response.ThingArn; + } + catch (Amazon.IoT.Model.ResourceAlreadyExistsException ex) + { + _logger.LogWarning($"Thing {thingName} already exists: {ex.Message}"); + return null; + } + catch (Exception ex) + { + _logger.LogError($"Couldn't create Thing {thingName}. Here's why: {ex.Message}"); + return null; + } + } + // snippet-end:[iot.dotnetv4.CreateThing] + + // snippet-start:[iot.dotnetv4.CreateKeysAndCertificate] + /// + /// Creates a device certificate for AWS IoT. + /// + /// The certificate details including ARN and certificate PEM, or null if creation failed. + public async Task<(string CertificateArn, string CertificatePem, string CertificateId)?> CreateKeysAndCertificateAsync() + { + try + { + var request = new CreateKeysAndCertificateRequest + { + SetAsActive = true + }; + + var response = await _amazonIoT.CreateKeysAndCertificateAsync(request); + _logger.LogInformation($"Created certificate with ARN {response.CertificateArn}"); + return (response.CertificateArn, response.CertificatePem, response.CertificateId); + } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return null; + } + catch (Exception ex) + { + _logger.LogError($"Couldn't create certificate. Here's why: {ex.Message}"); + return null; + } + } + // snippet-end:[iot.dotnetv4.CreateKeysAndCertificate] + + // snippet-start:[iot.dotnetv4.AttachThingPrincipal] + /// + /// Attaches a certificate to an IoT Thing. + /// + /// The name of the Thing. + /// The ARN of the certificate to attach. + /// True if successful, false otherwise. + public async Task AttachThingPrincipalAsync(string thingName, string certificateArn) + { + try + { + var request = new AttachThingPrincipalRequest + { + ThingName = thingName, + Principal = certificateArn + }; + + await _amazonIoT.AttachThingPrincipalAsync(request); + _logger.LogInformation($"Attached certificate {certificateArn} to Thing {thingName}"); + return true; + } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot attach certificate - resource not found: {ex.Message}"); + return false; + } + catch (Exception ex) + { + _logger.LogError($"Couldn't attach certificate to Thing. Here's why: {ex.Message}"); + return false; + } + } + // snippet-end:[iot.dotnetv4.AttachThingPrincipal] + + // snippet-start:[iot.dotnetv4.UpdateThing] + /// + /// Updates an IoT Thing with attributes. + /// + /// The name of the Thing to update. + /// Dictionary of attributes to add. + /// True if successful, false otherwise. + public async Task UpdateThingAsync(string thingName, Dictionary attributes) + { + try + { + var request = new UpdateThingRequest + { + ThingName = thingName, + AttributePayload = new AttributePayload + { + Attributes = attributes, + Merge = true + } + }; + + await _amazonIoT.UpdateThingAsync(request); + _logger.LogInformation($"Updated Thing {thingName} with attributes"); + return true; + } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot update Thing - resource not found: {ex.Message}"); + return false; + } + catch (Exception ex) + { + _logger.LogError($"Couldn't update Thing attributes. Here's why: {ex.Message}"); + return false; + } + } + // snippet-end:[iot.dotnetv4.UpdateThing] + + // snippet-start:[iot.dotnetv4.DescribeEndpoint] + /// + /// Gets the AWS IoT endpoint URL. + /// + /// The endpoint URL, or null if retrieval failed. + public async Task DescribeEndpointAsync() + { + try + { + var request = new DescribeEndpointRequest + { + EndpointType = "iot:Data-ATS" + }; + + var response = await _amazonIoT.DescribeEndpointAsync(request); + _logger.LogInformation($"Retrieved endpoint: {response.EndpointAddress}"); + return response.EndpointAddress; + } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return null; + } + catch (Exception ex) + { + _logger.LogError($"Couldn't describe endpoint. Here's why: {ex.Message}"); + return null; + } + } + // snippet-end:[iot.dotnetv4.DescribeEndpoint] + + // snippet-start:[iot.dotnetv4.ListCertificates] + /// + /// Lists all certificates associated with the account. + /// + /// List of certificate information, or empty list if listing failed. + public async Task> ListCertificatesAsync() + { + try + { + var request = new ListCertificatesRequest(); + var response = await _amazonIoT.ListCertificatesAsync(request); + + _logger.LogInformation($"Retrieved {response.Certificates.Count} certificates"); + return response.Certificates; + } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return new List(); + } + catch (Exception ex) + { + _logger.LogError($"Couldn't list certificates. Here's why: {ex.Message}"); + return new List(); + } + } + // snippet-end:[iot.dotnetv4.ListCertificates] + + // snippet-start:[iot.dotnetv4.UpdateThingShadow] + /// + /// Updates the Thing's shadow with new state information. + /// + /// The name of the Thing. + /// The shadow payload in JSON format. + /// True if successful, false otherwise. + public async Task UpdateThingShadowAsync(string thingName, string shadowPayload) + { + try + { + var request = new UpdateThingShadowRequest + { + ThingName = thingName, + Payload = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(shadowPayload)) + }; + + await _amazonIotData.UpdateThingShadowAsync(request); + _logger.LogInformation($"Updated shadow for Thing {thingName}"); + return true; + } + catch (Amazon.IotData.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot update Thing shadow - resource not found: {ex.Message}"); + return false; + } + catch (Exception ex) + { + _logger.LogError($"Couldn't update Thing shadow. Here's why: {ex.Message}"); + return false; + } + } + // snippet-end:[iot.dotnetv4.UpdateThingShadow] + + // snippet-start:[iot.dotnetv4.GetThingShadow] + /// + /// Gets the Thing's shadow information. + /// + /// The name of the Thing. + /// The shadow data as a string, or null if retrieval failed. + public async Task GetThingShadowAsync(string thingName) + { + try + { + var request = new GetThingShadowRequest + { + ThingName = thingName + }; + + var response = await _amazonIotData.GetThingShadowAsync(request); + using var reader = new StreamReader(response.Payload); + var shadowData = await reader.ReadToEndAsync(); + + _logger.LogInformation($"Retrieved shadow for Thing {thingName}"); + return shadowData; + } + catch (Amazon.IotData.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot get Thing shadow - resource not found: {ex.Message}"); + return null; + } + catch (Exception ex) + { + _logger.LogError($"Couldn't get Thing shadow. Here's why: {ex.Message}"); + return null; + } + } + // snippet-end:[iot.dotnetv4.GetThingShadow] + + // snippet-start:[iot.dotnetv4.CreateTopicRule] + /// + /// Creates an IoT topic rule. + /// + /// The name of the rule. + /// The ARN of the SNS topic for the action. + /// The ARN of the IAM role. + /// True if successful, false otherwise. + public async Task CreateTopicRuleAsync(string ruleName, string snsTopicArn, string roleArn) + { + try + { + var request = new CreateTopicRuleRequest + { + RuleName = ruleName, + TopicRulePayload = new TopicRulePayload + { + Sql = "SELECT * FROM 'topic/subtopic'", + Description = $"Rule created by .NET example: {ruleName}", + Actions = new List + { + new Amazon.IoT.Model.Action + { + Sns = new SnsAction + { + TargetArn = snsTopicArn, + RoleArn = roleArn + } + } + }, + RuleDisabled = false + } + }; + + await _amazonIoT.CreateTopicRuleAsync(request); + _logger.LogInformation($"Created IoT rule {ruleName}"); + return true; + } + catch (Amazon.IoT.Model.ResourceAlreadyExistsException ex) + { + _logger.LogWarning($"Rule {ruleName} already exists: {ex.Message}"); + return false; + } + catch (Exception ex) + { + _logger.LogError($"Couldn't create topic rule. Here's why: {ex.Message}"); + return false; + } + } + // snippet-end:[iot.dotnetv4.CreateTopicRule] + + // snippet-start:[iot.dotnetv4.DeleteTopicRule] + /// + /// Deletes an IoT topic rule. + /// + /// The name of the rule. + /// True if successful, false otherwise. + public async Task DeleteTopicRuleAsync(string ruleName) + { + try + { + var request = new DeleteTopicRuleRequest + { + RuleName = ruleName, + }; + + await _amazonIoT.DeleteTopicRuleAsync(request); + _logger.LogInformation($"Deleted IoT rule {ruleName}"); + return true; + } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) + { + _logger.LogWarning($"Rule {ruleName} not found: {ex.Message}"); + return false; + } + catch (Exception ex) + { + _logger.LogError($"Couldn't delete topic rule. Here's why: {ex.Message}"); + return false; + } + } + // snippet-end:[iot.dotnetv4.DeleteTopicRule] + + // snippet-start:[iot.dotnetv4.ListTopicRules] + /// + /// Lists all IoT topic rules. + /// + /// List of topic rules, or empty list if listing failed. + public async Task> ListTopicRulesAsync() + { + try + { + var request = new ListTopicRulesRequest(); + var response = await _amazonIoT.ListTopicRulesAsync(request); + + _logger.LogInformation($"Retrieved {response.Rules.Count} IoT rules"); + return response.Rules; + } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return new List(); + } + catch (Exception ex) + { + _logger.LogError($"Couldn't list topic rules. Here's why: {ex.Message}"); + return new List(); + } + } + // snippet-end:[iot.dotnetv4.ListTopicRules] + + // snippet-start:[iot.dotnetv4.SearchIndex] + /// + /// Searches for IoT Things using the search index. + /// + /// The search query string. + /// List of Things that match the search criteria, or empty list if search failed. + public async Task> SearchIndexAsync(string queryString) + { + try + { + // First, try to perform the search + var request = new SearchIndexRequest + { + QueryString = queryString + }; + + var response = await _amazonIoT.SearchIndexAsync(request); + _logger.LogInformation($"Search found {response.Things.Count} Things"); + return response.Things; + } + catch (Amazon.IoT.Model.IndexNotReadyException ex) + { + _logger.LogWarning($"Search index not ready, setting up indexing configuration: {ex.Message}"); + return await SetupIndexAndRetrySearchAsync(queryString); + } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) when (ex.Message.Contains("index") || ex.Message.Contains("Index")) + { + _logger.LogWarning($"Search index not configured, setting up indexing configuration: {ex.Message}"); + return await SetupIndexAndRetrySearchAsync(queryString); + } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return new List(); + } + catch (Exception ex) + { + _logger.LogError($"Couldn't search index. Here's why: {ex.Message}"); + return new List(); + } + } + + /// + /// Sets up the indexing configuration and retries the search after waiting for the index to be ready. + /// + /// The search query string. + /// List of Things that match the search criteria, or empty list if setup/search failed. + private async Task> SetupIndexAndRetrySearchAsync(string queryString) + { + try + { + // Update indexing configuration to REGISTRY mode + _logger.LogInformation("Setting up IoT search indexing configuration..."); + await _amazonIoT.UpdateIndexingConfigurationAsync( + new UpdateIndexingConfigurationRequest() + { + ThingIndexingConfiguration = new ThingIndexingConfiguration() + { + ThingIndexingMode = ThingIndexingMode.REGISTRY + } + }); + + _logger.LogInformation("Indexing configuration updated. Waiting for index to be ready..."); + + // Wait for the index to be set up - this can take some time + const int maxRetries = 10; + const int retryDelaySeconds = 10; + + for (int attempt = 1; attempt <= maxRetries; attempt++) + { + try + { + _logger.LogInformation($"Waiting for index to be ready (attempt {attempt}/{maxRetries})..."); + await Task.Delay(TimeSpan.FromSeconds(retryDelaySeconds)); + + // Try to get the current indexing configuration to see if it's ready + var configResponse = await _amazonIoT.GetIndexingConfigurationAsync(new GetIndexingConfigurationRequest()); + if (configResponse.ThingIndexingConfiguration?.ThingIndexingMode == ThingIndexingMode.REGISTRY) + { + // Try the search again + var request = new SearchIndexRequest + { + QueryString = queryString + }; + + var response = await _amazonIoT.SearchIndexAsync(request); + _logger.LogInformation($"Search found {response.Things.Count} Things after index setup"); + return response.Things; + } + } + catch (Amazon.IoT.Model.IndexNotReadyException) + { + // Index still not ready, continue waiting + _logger.LogInformation("Index still not ready, continuing to wait..."); + continue; + } + catch (Amazon.IoT.Model.InvalidRequestException ex) when (ex.Message.Contains("index") || ex.Message.Contains("Index")) + { + // Index still not ready, continue waiting + _logger.LogInformation("Index still not ready, continuing to wait..."); + continue; + } + } + + _logger.LogWarning("Timeout waiting for search index to be ready after configuration update"); + return new List(); + } + catch (Exception ex) + { + _logger.LogError($"Couldn't set up search index configuration. Here's why: {ex.Message}"); + return new List(); + } + } + // snippet-end:[iot.dotnetv4.SearchIndex] + + // snippet-start:[iot.dotnetv4.DetachThingPrincipal] + /// + /// Detaches a certificate from an IoT Thing. + /// + /// The name of the Thing. + /// The ARN of the certificate to detach. + /// True if successful, false otherwise. + public async Task DetachThingPrincipalAsync(string thingName, string certificateArn) + { + try + { + var request = new DetachThingPrincipalRequest + { + ThingName = thingName, + Principal = certificateArn + }; + + await _amazonIoT.DetachThingPrincipalAsync(request); + _logger.LogInformation($"Detached certificate {certificateArn} from Thing {thingName}"); + return true; + } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot detach certificate - resource not found: {ex.Message}"); + return false; + } + catch (Exception ex) + { + _logger.LogError($"Couldn't detach certificate from Thing. Here's why: {ex.Message}"); + return false; + } + } + // snippet-end:[iot.dotnetv4.DetachThingPrincipal] + + // snippet-start:[iot.dotnetv4.DeleteCertificate] + /// + /// Deletes an IoT certificate. + /// + /// The ID of the certificate to delete. + /// True if successful, false otherwise. + public async Task DeleteCertificateAsync(string certificateId) + { + try + { + // First, update the certificate to inactive state + var updateRequest = new UpdateCertificateRequest + { + CertificateId = certificateId, + NewStatus = CertificateStatus.INACTIVE + }; + await _amazonIoT.UpdateCertificateAsync(updateRequest); + + // Then delete the certificate + var deleteRequest = new DeleteCertificateRequest + { + CertificateId = certificateId + }; + + await _amazonIoT.DeleteCertificateAsync(deleteRequest); + _logger.LogInformation($"Deleted certificate {certificateId}"); + return true; + } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot delete certificate - resource not found: {ex.Message}"); + return false; + } + catch (Exception ex) + { + _logger.LogError($"Couldn't delete certificate. Here's why: {ex.Message}"); + return false; + } + } + // snippet-end:[iot.dotnetv4.DeleteCertificate] + + // snippet-start:[iot.dotnetv4.DeleteThing] + /// + /// Deletes an IoT Thing. + /// + /// The name of the Thing to delete. + /// True if successful, false otherwise. + public async Task DeleteThingAsync(string thingName) + { + try + { + var request = new DeleteThingRequest + { + ThingName = thingName + }; + + await _amazonIoT.DeleteThingAsync(request); + _logger.LogInformation($"Deleted Thing {thingName}"); + return true; + } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot delete Thing - resource not found: {ex.Message}"); + return false; + } + catch (Exception ex) + { + _logger.LogError($"Couldn't delete Thing. Here's why: {ex.Message}"); + return false; + } + } + // snippet-end:[iot.dotnetv4.DeleteThing] + + // snippet-start:[iot.dotnetv4.ListThings] + /// + /// Lists IoT Things with pagination support. + /// + /// List of Things, or empty list if listing failed. + public async Task> ListThingsAsync() + { + try + { + // Use pages of 10. + var request = new ListThingsRequest() + { + MaxResults = 10 + }; + var response = await _amazonIoT.ListThingsAsync(request); + + // Since there is not a built-in paginator, use the NextMarker to paginate. + bool hasMoreResults = true; + + var things = new List(); + while (hasMoreResults) + { + things.AddRange(response.Things); + + // If NextMarker is not null, there are more results. Get the next page of results. + if (!String.IsNullOrEmpty(response.NextMarker)) + { + request.Marker = response.NextMarker; + response = await _amazonIoT.ListThingsAsync(request); + } + else + hasMoreResults = false; + } + + _logger.LogInformation($"Retrieved {things.Count} Things"); + return things; + } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return new List(); + } + catch (Exception ex) + { + _logger.LogError($"Couldn't list Things. Here's why: {ex.Message}"); + return new List(); + } + } + // snippet-end:[iot.dotnetv4.ListThings] + +} +// snippet-end:[iot.dotnetv4.IoTWrapper] \ No newline at end of file diff --git a/dotnetv4/IoT/IoTExamples.sln b/dotnetv4/IoT/IoTExamples.sln new file mode 100644 index 00000000000..812279b9465 --- /dev/null +++ b/dotnetv4/IoT/IoTExamples.sln @@ -0,0 +1,36 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IoTActions", "Actions\IoTActions.csproj", "{A8F2F404-F1A3-4C0C-9478-2D99B95F0001}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IoTBasics", "Scenarios\IoTBasics.csproj", "{A8F2F404-F1A3-4C0C-9478-2D99B95F0002}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IoTTests", "Tests\IoTTests.csproj", "{A8F2F404-F1A3-4C0C-9478-2D99B95F0003}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A8F2F404-F1A3-4C0C-9478-2D99B95F0001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0001}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0001}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0001}.Release|Any CPU.Build.0 = Release|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0002}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0002}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0002}.Release|Any CPU.Build.0 = Release|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0003}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0003}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0003}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C73BCD70-F2E4-4A89-9E94-47E8C2B48A41} + EndGlobalSection +EndGlobal diff --git a/dotnetv4/IoT/README.md b/dotnetv4/IoT/README.md new file mode 100644 index 00000000000..5e24ef44442 --- /dev/null +++ b/dotnetv4/IoT/README.md @@ -0,0 +1,128 @@ +# AWS IoT code examples for the SDK for .NET (v4) + +## Overview + +Shows how to use the AWS SDK for .NET (v4) to work with AWS IoT. + + + + +_AWS IoT provides secure, bi-directional communication for Internet-connected devices (such as sensors, actuators, embedded devices, wireless devices, and smart appliances) to connect to the AWS Cloud over MQTT, HTTPS, and LoRaWAN._ + +## ⚠ Important + +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). +* Running the tests might result in charges to your AWS account. +* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../README.md#Prerequisites) in the `dotnetv4` folder. + + + + + +### Get started + +- [Hello AWS IoT](Actions/HelloIoT.cs#L9) (`listThings`) + + +### Basics + +Code examples that show you how to perform the essential operations within a service. + +- [Learn the basics](Scenarios/IoTBasics.cs) + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [AttachThingPrincipal](Actions/IoTWrapper.cs#L98) +- [CreateKeysAndCertificate](Actions/IoTWrapper.cs#L67) +- [CreateThing](Actions/IoTWrapper.cs#L35) +- [CreateTopicRule](Actions/IoTWrapper.cs#L298) +- [DeleteCertificate](Actions/IoTWrapper.cs#L558) +- [DeleteThing](Actions/IoTWrapper.cs#L599) +- [DescribeEndpoint](Actions/IoTWrapper.cs#L170) +- [DetachThingPrincipal](Actions/IoTWrapper.cs#L524) +- [ListCertificates](Actions/IoTWrapper.cs#L201) +- [ListThings](Actions/IoTWrapper.cs#L631) +- [SearchIndex](Actions/IoTWrapper.cs#L409) +- [UpdateThing](Actions/IoTWrapper.cs#L132) + + + + + +## Run the examples + +### Instructions + + + + + +#### Hello AWS IoT + +This example shows you how to get started using AWS IoT. + + +#### Learn the basics + +This example shows you how to do the following: + +- Create an AWS IoT Thing. +- Generate a device certificate. +- Update an AWS IoT Thing with Attributes. +- Return a unique endpoint. +- List your AWS IoT certificates. +- Update an AWS IoT shadow. +- Write out state information. +- Creates a rule. +- List your rules. +- Search things using the Thing name. +- Delete an AWS IoT Thing. + + + + + + + + + +### Tests + +⚠ Running tests might result in charges to your AWS account. + + +To find instructions for running these tests, see the [README](../README.md#Tests) +in the `dotnetv4` folder. + + + + + + +## Additional resources + +- [AWS IoT Developer Guide](https://docs.aws.amazon.com/iot/latest/developerguide/what-is-aws-iot.html) +- [AWS IoT API Reference](https://docs.aws.amazon.com/iot/latest/apireference/Welcome.html) +- [SDK for .NET (v4) AWS IoT reference](https://docs.aws.amazon.com/sdkfornet/v4/apidocs/items/IoT/NIoT.html) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.cs b/dotnetv4/IoT/Scenarios/IoTBasics.cs new file mode 100644 index 00000000000..1ca03e6dce6 --- /dev/null +++ b/dotnetv4/IoT/Scenarios/IoTBasics.cs @@ -0,0 +1,852 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Text.Json; +using Amazon; +using Amazon.CloudFormation; +using Amazon.CloudFormation.Model; +using Amazon.Extensions.NETCore.Setup; +using Amazon.IoT; +using Amazon.IoT.Model; +using Amazon.IotData; +using IoTActions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace IoTBasics; + +// snippet-start:[iot.dotnetv4.IoTScenario] +/// +/// Scenario class for AWS IoT basics. +/// +public class IoTBasics +{ + public static bool IsInteractive = true; + public static IoTWrapper? Wrapper = null; + public static IAmazonCloudFormation? CloudFormationClient = null; + public static ILogger logger = null!; + private static IoTWrapper _iotWrapper = null!; + private static IAmazonCloudFormation _amazonCloudFormation = null!; + private static ILogger _logger = null!; + + private static string _stackName = "IoTBasicsStack"; + private static string _stackResourcePath = "../../../../../../scenarios/basics/iot/iot_usecase/resources/cfn_template.yaml"; + + /// + /// Main method for the IoT Basics scenario. + /// + /// Command line arguments. + /// A Task object. + public static async Task Main(string[] args) + { + // Set up dependency injection for the Amazon service. + using var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((_, services) => + services.AddAWSService(new AWSOptions() { Region = RegionEndpoint.USEast1 }) + .AddAWSService() + .AddTransient() + .AddLogging(builder => builder.AddConsole()) + .AddSingleton(sp => + { + var iotService = sp.GetRequiredService(); + var request = new DescribeEndpointRequest + { + EndpointType = "iot:Data-ATS" + }; + var response = iotService.DescribeEndpointAsync(request).Result; + return new AmazonIotDataClient($"https://{response.EndpointAddress}/"); + }) + ) + .Build(); + + logger = LoggerFactory.Create(builder => builder.AddConsole()) + .CreateLogger(); + + Wrapper = host.Services.GetRequiredService(); + CloudFormationClient = host.Services.GetRequiredService(); + + // Set the private fields for backwards compatibility + _logger = logger; + _iotWrapper = Wrapper; + _amazonCloudFormation = CloudFormationClient; + + Console.WriteLine(new string('-', 80)); + Console.WriteLine("Welcome to the AWS IoT example scenario."); + Console.WriteLine("This example program demonstrates various interactions with the AWS Internet of Things (IoT) Core service."); + Console.WriteLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } + Console.WriteLine(new string('-', 80)); + + try + { + await RunScenarioAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was a problem running the scenario."); + Console.WriteLine($"\nAn error occurred: {ex.Message}"); + } + + Console.WriteLine(new string('-', 80)); + Console.WriteLine("The AWS IoT scenario has successfully completed."); + Console.WriteLine(new string('-', 80)); + } + + /// + /// Run the IoT Basics scenario. + /// + /// A Task object. + public static async Task RunScenarioAsync() + { + // Use static properties if available, otherwise use private fields + var iotWrapper = Wrapper ?? _iotWrapper; + var cloudFormationClient = CloudFormationClient ?? _amazonCloudFormation; + var scenarioLogger = logger ?? _logger; + + await RunScenarioInternalAsync(iotWrapper, cloudFormationClient, scenarioLogger); + } + + /// + /// Internal method to run the IoT Basics scenario with injected dependencies. + /// + /// The IoT wrapper instance. + /// The CloudFormation client instance. + /// The logger instance. + /// A Task object. + private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) + { + string thingName = $"iot-thing-{Guid.NewGuid():N}"; + string certificateArn = ""; + string certificateId = ""; + string ruleName = $"iotruledefault"; + string snsTopicArn = ""; + + try + { + // Step 1: Create an AWS IoT Thing + Console.WriteLine(new string('-', 80)); + Console.WriteLine("1. Create an AWS IoT Thing."); + Console.WriteLine("An AWS IoT Thing represents a virtual entity in the AWS IoT service that can be associated with a physical device."); + Console.WriteLine(); + + if (IsInteractive) + { + Console.Write("Enter Thing name: "); + var userInput = Console.ReadLine(); + if (!string.IsNullOrEmpty(userInput)) + thingName = userInput; + } + else + { + Console.WriteLine($"Using default Thing name: {thingName}"); + } + + var thingArn = await iotWrapper.CreateThingAsync(thingName); + Console.WriteLine($"{thingName} was successfully created. The ARN value is {thingArn}"); + Console.WriteLine(new string('-', 80)); + + // Step 1.1: List AWS IoT Things + Console.WriteLine(new string('-', 80)); + Console.WriteLine("2. List AWS IoT Things."); + Console.WriteLine("Now let's list the IoT Things to see the Thing we just created."); + Console.WriteLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } + + var things = await iotWrapper.ListThingsAsync(); + Console.WriteLine($"Found {things.Count} IoT Things:"); + foreach (var thing in things.Take(10)) // Show first 10 things + { + Console.WriteLine($"Thing Name: {thing.ThingName}"); + Console.WriteLine($"Thing ARN: {thing.ThingArn}"); + if (thing.Attributes != null && thing.Attributes.Any()) + { + Console.WriteLine("Attributes:"); + foreach (var attr in thing.Attributes) + { + Console.WriteLine($" {attr.Key}: {attr.Value}"); + } + } + Console.WriteLine("--------------"); + } + Console.WriteLine(); + Console.WriteLine(new string('-', 80)); + + // Step 2: Generate a Device Certificate + Console.WriteLine(new string('-', 80)); + Console.WriteLine("3. Generate a device certificate."); + Console.WriteLine("A device certificate performs a role in securing the communication between devices (Things) and the AWS IoT platform."); + Console.WriteLine(); + + var createCert = "y"; + if (IsInteractive) + { + Console.Write($"Do you want to create a certificate for {thingName}? (y/n)"); + createCert = Console.ReadLine(); + } + else + { + Console.WriteLine($"Creating certificate for {thingName}..."); + } + + if (createCert?.ToLower() == "y") + { + var certificateResult = await iotWrapper.CreateKeysAndCertificateAsync(); + if (certificateResult.HasValue) + { + var (certArn, certPem, certId) = certificateResult.Value; + certificateArn = certArn; + certificateId = certId; + + Console.WriteLine($"\nCertificate:"); + // Show only first few lines of certificate for brevity + var lines = certPem.Split('\n'); + for (int i = 0; i < Math.Min(lines.Length, 5); i++) + { + Console.WriteLine(lines[i]); + } + if (lines.Length > 5) + { + Console.WriteLine("..."); + } + + Console.WriteLine($"\nCertificate ARN:"); + Console.WriteLine(certificateArn); + + // Step 3: Attach the Certificate to the AWS IoT Thing + Console.WriteLine("Attach the certificate to the AWS IoT Thing."); + var attachResult = await iotWrapper.AttachThingPrincipalAsync(thingName, certificateArn); + if (attachResult) + { + Console.WriteLine("Certificate attached to Thing successfully."); + } + else + { + Console.WriteLine("Failed to attach certificate to Thing."); + } + + Console.WriteLine("Thing Details:"); + Console.WriteLine($"Thing Name: {thingName}"); + Console.WriteLine($"Thing ARN: {thingArn}"); + } + else + { + Console.WriteLine("Failed to create certificate."); + } + } + Console.WriteLine(new string('-', 80)); + + // Step 4: Update an AWS IoT Thing with Attributes + Console.WriteLine(new string('-', 80)); + Console.WriteLine("4. Update an AWS IoT Thing with Attributes."); + Console.WriteLine("IoT Thing attributes, represented as key-value pairs, offer a pivotal advantage in facilitating efficient data"); + Console.WriteLine("management and retrieval within the AWS IoT ecosystem."); + Console.WriteLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } + + var attributes = new Dictionary + { + { "Location", "Seattle" }, + { "DeviceType", "Sensor" }, + { "Firmware", "1.2.3" } + }; + + await iotWrapper.UpdateThingAsync(thingName, attributes); + Console.WriteLine("Thing attributes updated successfully."); + Console.WriteLine(new string('-', 80)); + + // Step 5: Return a unique endpoint specific to the Amazon Web Services account + Console.WriteLine(new string('-', 80)); + Console.WriteLine("5. Return a unique endpoint specific to the Amazon Web Services account."); + Console.WriteLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } + + var endpoint = await iotWrapper.DescribeEndpointAsync(); + if (endpoint != null) + { + var subdomain = endpoint.Split('.')[0]; + Console.WriteLine($"Extracted subdomain: {subdomain}"); + Console.WriteLine($"Full Endpoint URL: https://{endpoint}"); + } + else + { + Console.WriteLine("Failed to retrieve endpoint."); + } + Console.WriteLine(new string('-', 80)); + + // Step 6: List your AWS IoT certificates + Console.WriteLine(new string('-', 80)); + Console.WriteLine("6. List your AWS IoT certificates"); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } + + var certificates = await iotWrapper.ListCertificatesAsync(); + foreach (var cert in certificates.Take(5)) // Show first 5 certificates + { + Console.WriteLine($"Cert id: {cert.CertificateId}"); + Console.WriteLine($"Cert Arn: {cert.CertificateArn}"); + } + Console.WriteLine(); + Console.WriteLine(new string('-', 80)); + + // Step 7: Create an IoT shadow + Console.WriteLine(new string('-', 80)); + Console.WriteLine("7. Update an IoT shadow that refers to a digital representation or virtual twin of a physical IoT device"); + Console.WriteLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } + + var shadowPayload = JsonSerializer.Serialize(new + { + state = new + { + desired = new + { + temperature = 25, + humidity = 50 + } + } + }); + + await iotWrapper.UpdateThingShadowAsync(thingName, shadowPayload); + Console.WriteLine("Thing Shadow updated successfully."); + Console.WriteLine(new string('-', 80)); + + // Step 8: Write out the state information, in JSON format + Console.WriteLine(new string('-', 80)); + Console.WriteLine("8. Write out the state information, in JSON format."); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } + + var shadowData = await iotWrapper.GetThingShadowAsync(thingName); + Console.WriteLine($"Received Shadow Data: {shadowData}"); + Console.WriteLine(new string('-', 80)); + + // Step 9: Set up resources (SNS topic and IAM role) and create a rule + Console.WriteLine(new string('-', 80)); + Console.WriteLine("9. Set up resources and create a rule"); + Console.WriteLine(); + + // Deploy CloudFormation stack to create SNS topic and IAM role + Console.WriteLine("Deploying CloudFormation stack to create SNS topic and IAM role..."); + + var deployStack = !IsInteractive || GetYesNoResponse("Would you like to deploy the CloudFormation stack? (y/n) "); + if (deployStack) + { + if (IsInteractive) + { + Console.Write( + $"Enter stack resource file path (or press Enter for default '{_stackResourcePath}'): "); + var userResourcePath = Console.ReadLine(); + if (!string.IsNullOrEmpty(userResourcePath)) + _stackResourcePath = userResourcePath; + } + + _stackName = PromptUserForStackName(); + + var deploySuccess = await DeployCloudFormationStack(_stackName, cloudFormationClient, scenarioLogger); + + if (deploySuccess) + { + // Get stack outputs + var stackOutputs = await GetStackOutputs(_stackName, cloudFormationClient, scenarioLogger); + if (stackOutputs != null) + { + snsTopicArn = stackOutputs["SNSTopicArn"]; + string roleArn = stackOutputs["RoleArn"]; + + Console.WriteLine($"Successfully deployed stack. SNS topic: {snsTopicArn}"); + Console.WriteLine($"Successfully deployed stack. IAM role: {roleArn}"); + + if (IsInteractive) + { + Console.Write($"Enter Rule name (press Enter for default '{ruleName}'): "); + var userRuleName = Console.ReadLine(); + if (!string.IsNullOrEmpty(userRuleName)) + ruleName = userRuleName; + } + else + { + Console.WriteLine($"Using default rule name: {ruleName}"); + } + + // Now create the IoT rule with the CloudFormation outputs + var ruleResult = await iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); + if (ruleResult) + { + Console.WriteLine("IoT Rule created successfully."); + } + else + { + Console.WriteLine("Failed to create IoT rule."); + } + } + else + { + Console.WriteLine("Failed to get stack outputs. Skipping rule creation."); + } + } + else + { + Console.WriteLine("Failed to deploy CloudFormation stack. Skipping rule creation."); + } + } + else + { + Console.WriteLine("Skipping CloudFormation stack deployment and rule creation."); + } + Console.WriteLine(new string('-', 80)); + + // Step 10: List your rules + Console.WriteLine(new string('-', 80)); + Console.WriteLine("10. List your rules."); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } + + var rules = await iotWrapper.ListTopicRulesAsync(); + Console.WriteLine("List of IoT Rules:"); + foreach (var rule in rules.Take(5)) // Show first 5 rules + { + Console.WriteLine($"Rule Name: {rule.RuleName}"); + Console.WriteLine($"Rule ARN: {rule.RuleArn}"); + Console.WriteLine("--------------"); + } + Console.WriteLine(); + Console.WriteLine(new string('-', 80)); + + // Step 11: Search things using the Thing name + Console.WriteLine(new string('-', 80)); + Console.WriteLine("11. Search things using the Thing name."); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } + + var searchResults = await iotWrapper.SearchIndexAsync($"thingName:{thingName}"); + if (searchResults.Any()) + { + Console.WriteLine($"Thing id found using search is {searchResults.First().ThingId}"); + } + else + { + Console.WriteLine($"No search results found for Thing: {thingName}"); + } + Console.WriteLine(new string('-', 80)); + + // Step 12: Cleanup - Detach and delete certificate + if (!string.IsNullOrEmpty(certificateArn)) + { + Console.WriteLine(new string('-', 80)); + var deleteCert = "y"; + if (IsInteractive) + { + Console.Write($"Do you want to detach and delete the certificate for {thingName}? (y/n)"); + deleteCert = Console.ReadLine(); + } + else + { + Console.WriteLine($"Detaching and deleting certificate for {thingName}..."); + } + + if (deleteCert?.ToLower() == "y") + { + Console.WriteLine("12. You selected to detach and delete the certificate."); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } + + await iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); + Console.WriteLine($"{certificateArn} was successfully removed from {thingName}"); + + await iotWrapper.DeleteCertificateAsync(certificateId); + Console.WriteLine($"{certificateArn} was successfully deleted."); + } + Console.WriteLine(new string('-', 80)); + } + + // Step 13: Delete the AWS IoT Thing + Console.WriteLine(new string('-', 80)); + Console.WriteLine("13. Delete the AWS IoT Thing."); + var deleteThing = "y"; + if (IsInteractive) + { + Console.Write($"Do you want to delete the IoT Thing? (y/n)"); + deleteThing = Console.ReadLine(); + } + else + { + Console.WriteLine($"Deleting IoT Thing {thingName}..."); + } + + if (deleteThing?.ToLower() == "y") + { + await iotWrapper.DeleteThingAsync(thingName); + Console.WriteLine($"Deleted Thing {thingName}"); + } + Console.WriteLine(new string('-', 80)); + + // Step 14: Clean up CloudFormation stack + if (!string.IsNullOrEmpty(snsTopicArn)) + { + Console.WriteLine(new string('-', 80)); + Console.WriteLine("14. Clean up CloudFormation stack."); + Console.WriteLine("Deleting the CloudFormation stack and all resources..."); + + var cleanup = !IsInteractive || GetYesNoResponse("Do you want to delete the CloudFormation stack and all resources? (y/n) "); + if (cleanup) + { + var ruleCleanupSuccess = await iotWrapper.DeleteTopicRuleAsync(ruleName); + + var stackCleanupSuccess = await DeleteCloudFormationStack(_stackName, cloudFormationClient, scenarioLogger); + if (ruleCleanupSuccess && stackCleanupSuccess) + { + Console.WriteLine("Successfully cleaned up CloudFormation stack and all resources."); + } + else + { + Console.WriteLine("Some cleanup operations failed. Check the logs for details."); + } + } + else + { + Console.WriteLine($"Resources will remain. Stack name: {_stackName}"); + } + Console.WriteLine(new string('-', 80)); + } + } + catch (Exception ex) + { + scenarioLogger.LogError(ex, "Error occurred during scenario execution."); + + // Cleanup on error + if (!string.IsNullOrEmpty(certificateArn) && !string.IsNullOrEmpty(thingName)) + { + try + { + await iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); + await iotWrapper.DeleteCertificateAsync(certificateId); + } + catch (Exception cleanupEx) + { + scenarioLogger.LogError(cleanupEx, "Error during cleanup."); + } + } + + if (!string.IsNullOrEmpty(thingName)) + { + try + { + await iotWrapper.DeleteThingAsync(thingName); + } + catch (Exception cleanupEx) + { + scenarioLogger.LogError(cleanupEx, "Error during Thing cleanup."); + } + } + + // Clean up CloudFormation stack on error + if (!string.IsNullOrEmpty(snsTopicArn)) + { + try + { + await _iotWrapper.DeleteTopicRuleAsync(ruleName); + await DeleteCloudFormationStack(_stackName, cloudFormationClient, scenarioLogger); + } + catch (Exception cleanupEx) + { + scenarioLogger.LogError(cleanupEx, "Error during CloudFormation stack cleanup."); + } + } + + throw; + } + } + + /// + /// Deploys the CloudFormation stack with the necessary resources. + /// + /// The name of the CloudFormation stack. + /// The CloudFormation client. + /// The logger. + /// True if the stack was deployed successfully. + private static async Task DeployCloudFormationStack(string stackName, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) + { + Console.WriteLine($"\nDeploying CloudFormation stack: {stackName}"); + + try + { + var request = new CreateStackRequest + { + StackName = stackName, + TemplateBody = await File.ReadAllTextAsync(_stackResourcePath), + Capabilities = new List { Capability.CAPABILITY_NAMED_IAM } + }; + + var response = await cloudFormationClient.CreateStackAsync(request); + + if (response.HttpStatusCode == System.Net.HttpStatusCode.OK) + { + Console.WriteLine($"CloudFormation stack creation started: {stackName}"); + + bool stackCreated = await WaitForStackCompletion(response.StackId, cloudFormationClient, scenarioLogger); + + if (stackCreated) + { + Console.WriteLine("CloudFormation stack created successfully."); + return true; + } + else + { + scenarioLogger.LogError($"CloudFormation stack creation failed: {stackName}"); + return false; + } + } + else + { + scenarioLogger.LogError($"Failed to create CloudFormation stack: {stackName}"); + return false; + } + } + catch (AlreadyExistsException) + { + scenarioLogger.LogWarning($"CloudFormation stack '{stackName}' already exists. Please provide a unique name."); + var newStackName = PromptUserForStackName(); + return await DeployCloudFormationStack(newStackName, cloudFormationClient, scenarioLogger); + } + catch (Exception ex) + { + scenarioLogger.LogError(ex, $"An error occurred while deploying the CloudFormation stack: {stackName}"); + return false; + } + } + + /// + /// Waits for the CloudFormation stack to be in the CREATE_COMPLETE state. + /// + /// The ID of the CloudFormation stack. + /// The CloudFormation client. + /// The logger. + /// True if the stack was created successfully. + private static async Task WaitForStackCompletion(string stackId, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) + { + int retryCount = 0; + const int maxRetries = 30; + const int retryDelay = 10000; + + while (retryCount < maxRetries) + { + var describeStacksRequest = new DescribeStacksRequest + { + StackName = stackId + }; + + var describeStacksResponse = await cloudFormationClient.DescribeStacksAsync(describeStacksRequest); + + if (describeStacksResponse.Stacks.Count > 0) + { + if (describeStacksResponse.Stacks[0].StackStatus == StackStatus.CREATE_COMPLETE) + { + return true; + } + if (describeStacksResponse.Stacks[0].StackStatus == StackStatus.CREATE_FAILED || + describeStacksResponse.Stacks[0].StackStatus == StackStatus.ROLLBACK_COMPLETE) + { + return false; + } + } + + Console.WriteLine("Waiting for CloudFormation stack creation to complete..."); + await Task.Delay(retryDelay); + retryCount++; + } + + scenarioLogger.LogError("Timed out waiting for CloudFormation stack creation to complete."); + return false; + } + + /// + /// Gets the outputs from the CloudFormation stack. + /// + /// The name of the CloudFormation stack. + /// The CloudFormation client. + /// The logger. + /// A dictionary of stack outputs. + private static async Task?> GetStackOutputs(string stackName, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) + { + try + { + var describeStacksRequest = new DescribeStacksRequest + { + StackName = stackName + }; + + var response = await cloudFormationClient.DescribeStacksAsync(describeStacksRequest); + + if (response.Stacks.Count > 0) + { + var outputs = new Dictionary(); + foreach (var output in response.Stacks[0].Outputs) + { + outputs[output.OutputKey] = output.OutputValue; + } + return outputs; + } + + return null; + } + catch (Exception ex) + { + scenarioLogger.LogError(ex, $"Failed to get stack outputs for {stackName}"); + return null; + } + } + + /// + /// Deletes the CloudFormation stack and waits for confirmation. + /// + /// The name of the CloudFormation stack. + /// The CloudFormation client. + /// The logger. + /// True if the stack was deleted successfully. + private static async Task DeleteCloudFormationStack(string stackName, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) + { + try + { + var request = new DeleteStackRequest + { + StackName = stackName + }; + + await cloudFormationClient.DeleteStackAsync(request); + Console.WriteLine($"CloudFormation stack '{stackName}' is being deleted. This may take a few minutes."); + + bool stackDeleted = await WaitForStackDeletion(stackName, cloudFormationClient, scenarioLogger); + + if (stackDeleted) + { + Console.WriteLine($"CloudFormation stack '{stackName}' has been deleted."); + return true; + } + else + { + scenarioLogger.LogError($"Failed to delete CloudFormation stack '{stackName}'."); + return false; + } + } + catch (Exception ex) + { + scenarioLogger.LogError(ex, $"An error occurred while deleting the CloudFormation stack: {stackName}"); + return false; + } + } + + /// + /// Waits for the stack to be deleted. + /// + /// The name of the CloudFormation stack. + /// The CloudFormation client. + /// The logger. + /// True if the stack was deleted successfully. + private static async Task WaitForStackDeletion(string stackName, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) + { + int retryCount = 0; + const int maxRetries = 30; + const int retryDelay = 10000; + + while (retryCount < maxRetries) + { + var describeStacksRequest = new DescribeStacksRequest + { + StackName = stackName + }; + + try + { + var describeStacksResponse = await cloudFormationClient.DescribeStacksAsync(describeStacksRequest); + + if (describeStacksResponse.Stacks.Count == 0 || + describeStacksResponse.Stacks[0].StackStatus == StackStatus.DELETE_COMPLETE) + { + return true; + } + } + catch (AmazonCloudFormationException ex) when (ex.ErrorCode == "ValidationError") + { + return true; + } + + Console.WriteLine($"Waiting for CloudFormation stack '{stackName}' to be deleted..."); + await Task.Delay(retryDelay); + retryCount++; + } + + scenarioLogger.LogError($"Timed out waiting for CloudFormation stack '{stackName}' to be deleted."); + return false; + } + + /// + /// Helper method to get a yes or no response from the user. + /// + private static bool GetYesNoResponse(string question) + { + Console.WriteLine(question); + var ynResponse = Console.ReadLine(); + var response = ynResponse != null && ynResponse.Equals("y", StringComparison.InvariantCultureIgnoreCase); + return response; + } + + /// + /// Prompts the user for a stack name. + /// + private static string PromptUserForStackName() + { + if (IsInteractive) + { + Console.Write($"Enter a name for the CloudFormation stack (press Enter for default '{_stackName}'): "); + string? input = Console.ReadLine(); + if (!string.IsNullOrWhiteSpace(input)) + { + var regex = new System.Text.RegularExpressions.Regex("[a-zA-Z][-a-zA-Z0-9]*"); + if (!regex.IsMatch(input)) + { + Console.WriteLine($"Invalid stack name. Using default: {_stackName}"); + return _stackName; + } + return input; + } + } + return _stackName; + } +} +// snippet-end:[iot.dotnetv4.IoTScenario] \ No newline at end of file diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.csproj b/dotnetv4/IoT/Scenarios/IoTBasics.csproj new file mode 100644 index 00000000000..b5409fe8683 --- /dev/null +++ b/dotnetv4/IoT/Scenarios/IoTBasics.csproj @@ -0,0 +1,31 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs new file mode 100644 index 00000000000..2616e612bb7 --- /dev/null +++ b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs @@ -0,0 +1,64 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.CloudFormation; +using Amazon.IoT; +using Amazon.IotData; +using IoTActions; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace IoTTests; + +/// +/// Integration tests for the AWS IoT Basics scenario. +/// +public class IoTBasicsTests +{ + /// + /// Verifies the scenario with an integration test. No errors should be logged. + /// + /// Async task. + [Fact] + [Trait("Category", "Integration")] + public async Task TestScenarioIntegration() + { + // Arrange + IoTBasics.IoTBasics.IsInteractive = false; + + var loggerScenarioMock = new Mock>(); + var loggerWrapperMock = new Mock>(); + + loggerScenarioMock.Setup(logger => logger.Log( + It.Is(logLevel => logLevel == Microsoft.Extensions.Logging.LogLevel.Error), + It.IsAny(), + It.Is((@object, @type) => true), + It.IsAny(), + It.IsAny>() + )); + + // Act + IoTBasics.IoTBasics.logger = loggerScenarioMock.Object; + + // Set up the wrapper and CloudFormation client + IoTBasics.IoTBasics.Wrapper = new IoTWrapper( + new AmazonIoTClient(), + new AmazonIotDataClient("https://dummy-iot-endpoint.amazonaws.com/"), + loggerWrapperMock.Object); + + IoTBasics.IoTBasics.CloudFormationClient = new AmazonCloudFormationClient(); + + await IoTBasics.IoTBasics.RunScenarioAsync(); + + // Assert no errors logged + loggerScenarioMock.Verify( + logger => logger.Log( + It.Is(logLevel => logLevel == Microsoft.Extensions.Logging.LogLevel.Error), + It.IsAny(), + It.Is((@object, @type) => true), + It.IsAny(), + It.IsAny>()), + Times.Never); + } +} \ No newline at end of file diff --git a/dotnetv4/IoT/Tests/IoTTests.csproj b/dotnetv4/IoT/Tests/IoTTests.csproj new file mode 100644 index 00000000000..84cf3d4fb76 --- /dev/null +++ b/dotnetv4/IoT/Tests/IoTTests.csproj @@ -0,0 +1,36 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/javav2/example_code/iot/README.md b/javav2/example_code/iot/README.md index 2c5c80544ba..fe4953a53a7 100644 --- a/javav2/example_code/iot/README.md +++ b/javav2/example_code/iot/README.md @@ -56,7 +56,6 @@ Code excerpts that show you how to call individual service functions. - [DetachThingPrincipal](src/main/java/com/example/iot/scenario/IotActions.java#L572) - [ListCertificates](src/main/java/com/example/iot/scenario/IotActions.java#L368) - [SearchIndex](src/main/java/com/example/iot/scenario/IotActions.java#L531) -- [UpdateThing](src/main/java/com/example/iot/scenario/IotActions.java#L267) @@ -84,7 +83,7 @@ This example shows you how to do the following: - Update an AWS IoT Thing with Attributes. - Return a unique endpoint. - List your AWS IoT certificates. -- Create an AWS IoT shadow. +- Update an AWS IoT shadow. - Write out state information. - Creates a rule. - List your rules. diff --git a/kotlin/services/iot/README.md b/kotlin/services/iot/README.md index 58a4fb91b24..1c69a063d74 100644 --- a/kotlin/services/iot/README.md +++ b/kotlin/services/iot/README.md @@ -84,7 +84,7 @@ This example shows you how to do the following: - Update an AWS IoT Thing with Attributes. - Return a unique endpoint. - List your AWS IoT certificates. -- Create an AWS IoT shadow. +- Update an AWS IoT shadow. - Write out state information. - Creates a rule. - List your rules. diff --git a/scenarios/basics/controltower/SPECIFICATION.md b/scenarios/basics/controltower/SPECIFICATION.md index b4e9c8590dc..58ee1fbfdc4 100644 --- a/scenarios/basics/controltower/SPECIFICATION.md +++ b/scenarios/basics/controltower/SPECIFICATION.md @@ -210,7 +210,7 @@ Thanks for watching! ## Errors The following errors are handled in the Control Tower wrapper class: -| action | Error | Handling | +| Action | Error | Handling | |------------------------|-----------------------|------------------------------------------------------------------------| | `ListBaselines` | AccessDeniedException | Notify the user of insufficient permissions and exit. | | `ListEnabledBaselines` | AccessDeniedException | Notify the user of insufficient permissions and exit. | diff --git a/scenarios/basics/iot/SPECIFICATION.md b/scenarios/basics/iot/SPECIFICATION.md index 4b841154538..2a8c0cc8a58 100644 --- a/scenarios/basics/iot/SPECIFICATION.md +++ b/scenarios/basics/iot/SPECIFICATION.md @@ -6,14 +6,22 @@ This example shows how to use AWS SDKs to perform device management use cases us The AWS Iot API provides secure, bi-directional communication between Internet-connected devices (such as sensors, actuators, embedded devices, or smart appliances) and the Amazon Web Services cloud. This example shows some typical use cases such as creating things, creating certifications, applying the certifications to the IoT Thing and so on. ## Resources -This program requires these AWS resources. +This program should create and manage these AWS resources automatically using CloudFormation and the provided stack .yaml file: -1. **roleARN** - The ARN of an IAM role that has permission to work with AWS IOT. -2. **snsAction** - An ARN of an SNS topic. +1. **roleARN** - The ARN of an IAM role that has permission to work with AWS IoT. This role is created through CloudFormation stack deployment with proper permissions to publish to SNS topics. +2. **snsAction** - An ARN of an SNS topic. This topic is created through CloudFormation stack deployment for use with IoT rules. + +### CloudFormation Integration +- **Setup**: The scenario deploys a CloudFormation stack using the template file `iot_usecase/resources/cfn_template.yaml` +- **Resource Creation**: All required resources (SNS topic and IAM role) are defined in the CloudFormation template +- **Output Retrieval**: The scenario retrieves the SNS topic ARN and IAM role ARN from the CloudFormation stack outputs +- **Cleanup**: At the end of the scenario execution, the entire CloudFormation stack is deleted, ensuring all resources are properly cleaned up + +The CloudFormation template provides Infrastructure as Code (IaC) benefits, ensuring consistent and repeatable resource deployment across different environments. ## Hello AWS IoT -This program is intended for users not familiar with the Amazon IoT SDK to easily get up and running. The logic is to show use of `listThingsPaginator`. +This program is intended for users not familiar with the Amazon IoT SDK to easily get up and running. The logic is to show use of `listThings` for up to 10 things. ## Scenario Program Flow @@ -23,50 +31,89 @@ This scenario demonstrates the following key AWS IoT Service operations: - Use the `CreateThing` API to create a new AWS IoT Thing. - Specify the Thing name and any desired Thing attributes. -2. **Generate a Device Certificate**: +2. **List AWS IoT Things**: + - Use the `ListThings` API to retrieve a list of all AWS IoT Things in the account. + - Display the Thing names, ARNs, and any associated attributes. + - This step demonstrates how to verify that the Thing was created successfully and shows other existing Things. + +3. **Generate a Device Certificate**: - Use the `CreateKeysAndCertificate` API to generate a new device certificate. - The certificate is used to authenticate the device when connecting to AWS IoT. -3. **Attach the Certificate to the AWS IoT Thing**: +4. **Attach the Certificate to the AWS IoT Thing**: - Use the `AttachThingPrincipal` API to associate the device certificate with the AWS IoT Thing. - This allows the device to authenticate and communicate with the AWS IoT Core service. -4. **Update an AWS IoT Thing with Attributes**: +5. **Update an AWS IoT Thing with Attributes**: - Use the `UpdateThingShadow` API to update the Thing's shadow with new attribute values. - The Thing's shadow represents the device's state and properties. -5. **Get an AWS IoT Endpoint**: +6. **Get an AWS IoT Endpoint**: - Use the `DescribeEndpoint` API to retrieve the AWS IoT Core service endpoint. - The device uses this endpoint to connect and communicate with AWS IoT. -6. **List Certificates**: +7. **List Certificates**: - Use the `ListCertificates` API to retrieve a list of all certificates associated with the AWS IoT account. -7. **Detach and Delete the Certificate**: +8. **Detach and Delete the Certificate**: - Use the `DetachThingPrincipal` API to detach the certificate from the AWS IoT Thing. - Use the `DeleteCertificate` API to delete the certificate. -8. **Update the Thing Shadow**: +9. **Update the Thing Shadow**: - Use the `UpdateThingShadow` API to update the Thing's shadow with new state information. - The Thing's shadow represents the device's state and properties. -9. **Write State Information in JSON Format**: - - The state information is written in JSON format, which is the standard data format used by AWS IoT. +10. **Write State Information in JSON Format**: + - Use the 'GetThingShadow' to get the state information. The state information is written in JSON format, which is the standard data format used by AWS IoT. -10. **Create an AWS IoT Rule**: +11. **Create an AWS IoT Rule**: - Use the `CreateTopicRule` API to create a new AWS IoT Rule. - Rules allow you to define actions to be performed based on device data or events. -11. **List AWS IoT Rules**: +12. **List AWS IoT Rules**: - Use the `ListTopicRules` API to retrieve a list of all AWS IoT Rules. -12. **Search AWS IoT Things**: - - Use the `SearchThings` API to search for AWS IoT Things based on various criteria, such as Thing name, attributes, or shadow state. - -13. **Delete an AWS IoT Thing**: +13. **Search AWS IoT Things**: + - Use the `SearchIndex` API to search for AWS IoT Things based on various criteria, such as Thing name, attributes, or shadow state. + - **Automatic Index Configuration**: The search functionality includes intelligent handling of index setup: + - If the search index is not configured, the system automatically detects this condition through exception handling + - Catches `IndexNotReadyException` and `InvalidRequestException` that indicate the search index needs to be set up + - Automatically configures the Thing indexing mode to `REGISTRY` to enable search functionality + - Implements a retry mechanism with up to 10 attempts, waiting 10 seconds between each attempt for the index to become ready + - Validates the indexing configuration status before retrying search operations + - Provides detailed logging throughout the index setup process to keep users informed of progress + +14. **Delete an AWS IoT Thing**: - Use the `DeleteThing` API to delete an AWS IoT Thing. - Note: We have buy off on these operations from IoT SME. + +15. **Clean up resources**: + - Clean up the stack and rule. + + +## Exception Handling + +Each AWS IoT operation can throw specific exceptions that should be handled appropriately. The following table lists the potential exceptions for each action: + +| Action | Error | Handling | +|------------------------|---------------------------------|------------------------------------------------------------------------| +| **CreateThing** | ResourceAlreadyExistsException | Skip the creation and notify the user +| **ListThings** | ThrottlingException | Notify the user to try again later +| **CreateKeysAndCertificate** | ThrottlingException | Notify the user to try again later +| **AttachThingPrincipal** | ResourceNotFoundException | Notify cannot perform action and return +| **UpdateThing** | ResourceNotFoundException | Notify cannot perform action and return +| **DescribeEndpoint** | ThrottlingException | Notify the user to try again later +| **ListCertificates** | ThrottlingException | Notify the user to try again later +| **UpdateThingShadow** | ResourceNotFoundException | Notify cannot perform action and return +| **GetThingShadow** | ResourceNotFoundException | Notify cannot perform action and return +| **CreateTopicRule** | ResourceAlreadyExistsException | Skip the creation and notify the user +| **ListTopicRules** | ThrottlingException | Notify the user to try again later +| **SearchIndex** | ThrottlingException | Notify the user to try again later +| **DetachThingPrincipal** | ResourceNotFoundException | Notify cannot perform action and return +| **DeleteCertificate** | ResourceNotFoundException | Notify cannot perform action and return +| **DeleteThing** | ResourceNotFoundException | Notify cannot perform action and return +| **ListThings** | ThrottlingException | Notify the user to try again later + ### Program execution @@ -92,7 +139,27 @@ Enter Thing name: foo5543 foo5543 was successfully created. The ARN value is arn:aws:iot:us-east-1:814548047983:thing/foo5543 -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -2. Generate a device certificate. +2. List AWS IoT Things. +Now let's list the IoT Things to see the Thing we just created. + +Press Enter to continue... +Found 3 IoT Things: +Thing Name: foo5543 +Thing ARN: arn:aws:iot:us-east-1:814548047983:thing/foo5543 +-------------- +Thing Name: existing-thing-1 +Thing ARN: arn:aws:iot:us-east-1:814548047983:thing/existing-thing-1 +Attributes: + Location: Seattle + DeviceType: Sensor +-------------- +Thing Name: existing-thing-2 +Thing ARN: arn:aws:iot:us-east-1:814548047983:thing/existing-thing-2 +-------------- + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +3. Generate a device certificate. A device certificate performs a role in securing the communication between devices (Things) and the AWS IoT platform. Do you want to create a certificate for foo5543? (y/n)y @@ -113,7 +180,7 @@ Thing Name: foo5543 Thing ARN: arn:aws:iot:us-east-1:814548047983:thing/foo5543 -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -3. Update an AWS IoT Thing with Attributes. +4. Update an AWS IoT Thing with Attributes. IoT Thing attributes, represented as key-value pairs, offer a pivotal advantage in facilitating efficient data management and retrieval within the AWS IoT ecosystem. @@ -121,7 +188,7 @@ Press Enter to continue... Thing attributes updated successfully. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -4. Return a unique endpoint specific to the Amazon Web Services account. +5. Return a unique endpoint specific to the Amazon Web Services account. An IoT Endpoint refers to a specific URL or Uniform Resource Locator that serves as the entry point for communication between IoT devices and the AWS IoT service. Press Enter to continue... @@ -129,7 +196,7 @@ Extracted subdomain: a39q2exsoth3da Full Endpoint URL: https://a39q2exsoth3da-ats.iot.us-east-1.amazonaws.com -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -5. List your AWS IoT certificates +6. List your AWS IoT certificates Press Enter to continue... Cert id: 1c9cd9a0f315b58e549e84c38ada37ced24e89047a15ff7ac4abafae9ff6dfc6 Cert Arn: arn:aws:iot:us-east-1:814548047983:cert/1c9cd9a0f315b58e549e84c38ada37ced24e89047a15ff7ac4abafae9ff6dfc6 @@ -140,7 +207,7 @@ Cert Arn: arn:aws:iot:us-east-1:814548047983:cert/c0d340f1fa8484075d84b523144369 -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -6. Create an IoT shadow that refers to a digital representation or virtual twin of a physical IoT device +7. Create an IoT shadow that refers to a digital representation or virtual twin of a physical IoT device A Thing Shadow refers to a feature that enables you to create a virtual representation, or "shadow," of a physical device or thing. The Thing Shadow allows you to synchronize and control the state of a device between the cloud and the device itself. and the AWS IoT service. For example, you can write and retrieve JSON data from a Thing Shadow. @@ -149,12 +216,12 @@ Press Enter to continue... Thing Shadow updated successfully. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -7. Write out the state information, in JSON format. +8. Write out the state information, in JSON format. Press Enter to continue... Received Shadow Data: {"state":{"reported":{"temperature":25,"humidity":50}},"metadata":{"reported":{"temperature":{"timestamp":1707413791},"humidity":{"timestamp":1707413791}}},"version":1,"timestamp":1707413794} -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -8. Creates a rule +9. Creates a rule Creates a rule that is an administrator-level action. Any user who has permission to create rules will be able to access data processed by the rule. @@ -162,7 +229,7 @@ Enter Rule name: rule8823 IoT Rule created successfully. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -9. List your rules. +10. List your rules. Press Enter to continue... List of IoT Rules: Rule Name: rule0099 @@ -179,23 +246,34 @@ Rule ARN: arn:aws:iot:us-east-1:814548047983:rule/YourRuleName11 -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -10. Search things using the Thing name. +11. Search things using the Thing name. Press Enter to continue... Thing id found using search is abad8003-3abd-4614-bc04-8d0b6211eb9e -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Do you want to detach and delete the certificate for foo5543? (y/n)y -11. You selected to detach amd delete the certificate. +12. You selected to detach amd delete the certificate. Press Enter to continue... arn:aws:iot:us-east-1:814548047983:cert/1c9cd9a0f315b58e549e84c38ada37ced24e89047a15ff7ac4abafae9ff6dfc6 was successfully removed from foo5543 arn:aws:iot:us-east-1:814548047983:cert/1c9cd9a0f315b58e549e84c38ada37ced24e89047a15ff7ac4abafae9ff6dfc6 was successfully deleted. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -12. Delete the AWS IoT Thing. +13. Delete the AWS IoT Thing. Do you want to delete the IoT Thing? (y/n)y Deleted Thing foo5543 -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- +14. Clean up CloudFormation stack. +Deleting the CloudFormation stack and all resources... +CloudFormation stack 'IoTBasicsStack' is being deleted. This may take a few minutes. +Waiting for CloudFormation stack 'IoTBasicsStack' to be deleted... +Waiting for CloudFormation stack 'IoTBasicsStack' to be deleted... +Waiting for CloudFormation stack 'IoTBasicsStack' to be deleted... +Waiting for CloudFormation stack 'IoTBasicsStack' to be deleted... +Waiting for CloudFormation stack 'IoTBasicsStack' to be deleted... +CloudFormation stack 'IoTBasicsStack' has been deleted. +Successfully cleaned up CloudFormation stack and all resources. +-------------------------------------------------------------------------------- The AWS IoT workflow has successfully completed. -------------------------------------------------------------------------------- ``` @@ -210,7 +288,7 @@ The following table describes the metadata used in this scenario. | `describeEndpoint` | iot_metadata.yaml | iot_DescribeEndpoint | | `listThings` | iot_metadata.yaml | iot_ListThings | | `listCertificates` | iot_metadata.yaml | iot_ListCertificates | -| `CreateKeysAndCertificate` | iot_metadata.yaml | iot_CreateKeysAndCertificate | +| `createKeysAndCertificate` | iot_metadata.yaml | iot_CreateKeysAndCertificate | | `deleteCertificate` | iot_metadata.yaml | iot_DeleteCertificate | | `searchIndex` | iot_metadata.yaml | iot_SearchIndex | | `deleteThing` | iot_metadata.yaml | iot_DeleteThing | @@ -218,7 +296,10 @@ The following table describes the metadata used in this scenario. | `attachThingPrincipal` | iot_metadata.yaml | iot_AttachThingPrincipal | | `detachThingPrincipal` | iot_metadata.yaml | iot_DetachThingPrincipal | | `updateThing` | iot_metadata.yaml | iot_UpdateThing | +| `updateThingShadow` | iot-data_metadata.yaml | io-data-plane_UpdateThingShadow | +| `getThingShadow` | iot-data_metadata.yaml | io-data-plane_GetThingShadow | | `createTopicRule` | iot_metadata.yaml | iot_CreateTopicRule | | `createThing` | iot_metadata.yaml | iot_CreateThing | -| `hello ` | iot_metadata.yaml | iot_Hello | -| `scenario | iot_metadata.yaml | iot_Scenario | +| `listThings` | iot_metadata.yaml | iot_ListThings | +| `hello` | iot_metadata.yaml | iot_Hello | +| `scenario` | iot_metadata.yaml | iot_Scenario |