diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9f36d9f6e4..a63191bc62 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -80,6 +80,15 @@ jobs: /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:ExcludeByFile="**/${{ env.TESTS_FOLDER }}/" \ ${{ env.TESTS_FOLDER }}/Integration/LoRaWan.Tests.Integration.csproj + # Run cli tests + - name: Run cli unit tests + run: | + dotnet test --configuration ${{ env.buildConfiguration }} \ + --logger trx -r ${{ env.TESTS_RESULTS_FOLDER }}/Cli.Unit \ + /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:ExcludeByFile="**/${{ env.TESTS_FOLDER }}/" \ + ./Tools/Cli-LoRa-Device-Provisioning/Tests/LoRaWan.Tools.CLI.Tests.Unit/LoRaWan.Tools.CLI.Tests.Unit.csproj + + # Upload test results as artifact - uses: actions/upload-artifact@v3 if: success() || failure() @@ -88,6 +97,7 @@ jobs: path: | ${{ env.TESTS_RESULTS_FOLDER }}/Unit ${{ env.TESTS_RESULTS_FOLDER }}/Integration + ${{ env.TESTS_RESULTS_FOLDER }}/Cli.Unit - name: Upload to Codecov test reports uses: codecov/codecov-action@v3 diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs index 124162d932..21bf26e92a 100644 --- a/AssemblyInfo.cs +++ b/AssemblyInfo.cs @@ -12,4 +12,5 @@ [assembly: InternalsVisibleTo("LoRaWan.Tests.E2E")] [assembly: InternalsVisibleTo("LoRaWan.Tests.Common")] [assembly: InternalsVisibleTo("LoRaWan.Tests.Simulation")] +[assembly: InternalsVisibleTo("LoRaWan.Tools.CLI.Tests.Unit")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/LoRaEngine/LoraKeysManagerFacade/CreateEdgeDevice.cs b/LoRaEngine/LoraKeysManagerFacade/CreateEdgeDevice.cs deleted file mode 100644 index 61fd683819..0000000000 --- a/LoRaEngine/LoraKeysManagerFacade/CreateEdgeDevice.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace LoraKeysManagerFacade -{ - using System; - using System.Net; - using System.Net.Http; - using System.Text; - using System.Threading.Tasks; - using LoRaTools; - using LoRaWan; - using Microsoft.AspNetCore.Http; - using Microsoft.Azure.WebJobs; - using Microsoft.Azure.WebJobs.Extensions.Http; - using Microsoft.Extensions.Logging; - - public class CreateEdgeDevice - { - private readonly IDeviceRegistryManager registryManager; - - public CreateEdgeDevice(IDeviceRegistryManager registryManager) - { - this.registryManager = registryManager; - } - - [FunctionName(nameof(CreateEdgeDevice))] - public async Task CreateEdgeDeviceImp( - [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, - ILogger log) - { - // parse query parameter - var queryStrings = req.GetQueryParameterDictionary(); - - // required arguments - if (!queryStrings.TryGetValue("deviceName", out var deviceName) || - !queryStrings.TryGetValue("publishingUserName", out var publishingUserName) || - !queryStrings.TryGetValue("publishingPassword", out var publishingPassword) || - !queryStrings.TryGetValue("region", out var region) || - !queryStrings.TryGetValue("stationEui", out var stationEuiString) || - !queryStrings.TryGetValue("resetPin", out var resetPin)) - { - return new HttpResponseMessage(HttpStatusCode.BadRequest) { ReasonPhrase = "Missing required parameters." }; - } - - if (!StationEui.TryParse(stationEuiString, out _)) - { - return new HttpResponseMessage(HttpStatusCode.BadRequest) { ReasonPhrase = "Station EUI could not be properly parsed." }; - } - - // optional arguments - _ = queryStrings.TryGetValue("spiSpeed", out var spiSpeed); - _ = queryStrings.TryGetValue("spiDev", out var spiDev); - - _ = bool.TryParse(Environment.GetEnvironmentVariable("DEPLOY_DEVICE"), out var deployEndDevice); - - try - { - await this.registryManager.DeployEdgeDeviceAsync(deviceName, resetPin, spiSpeed, spiDev, publishingUserName, publishingPassword); - - await this.registryManager.DeployConcentratorAsync(stationEuiString, region); - - // This section will get deployed ONLY if the user selected the "deploy end device" options. - // Information in this if clause, is for demo purpose only and should not be used for productive workloads. - if (deployEndDevice) - { - _ = await this.registryManager.DeployEndDevicesAsync(); - } - } -#pragma warning disable CA1031 // Do not catch general exception types. This will go away when we implement #242 - catch (Exception ex) -#pragma warning restore CA1031 // Do not catch general exception types - { - log.LogWarning(ex.Message); - - // In case of an exception in device provisioning we want to make sure that we return a proper template if our devices are successfullycreated - var edgeGateway = await this.registryManager.GetTwinAsync(deviceName); - - if (edgeGateway == null) - { - return PrepareResponse(HttpStatusCode.Conflict); - } - - if (deployEndDevice && !await this.registryManager.DeployEndDevicesAsync()) - { - return PrepareResponse(HttpStatusCode.Conflict); - } - - return PrepareResponse(HttpStatusCode.OK); - } - - return PrepareResponse(HttpStatusCode.OK); - } - - private static HttpResponseMessage PrepareResponse(HttpStatusCode httpStatusCode) - { - var template = @"{'$schema': 'https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#', 'contentVersion': '1.0.0.0', 'parameters': {}, 'variables': {}, 'resources': []}"; - var response = new HttpResponseMessage(httpStatusCode); - if (httpStatusCode == HttpStatusCode.OK) - { - response.Content = new StringContent(template, Encoding.UTF8, "application/json"); - } - - return response; - } - } -} diff --git a/LoRaEngine/LoraKeysManagerFacade/FacadeStartup.cs b/LoRaEngine/LoraKeysManagerFacade/FacadeStartup.cs index 7c975b14f9..c8df9ffd65 100644 --- a/LoRaEngine/LoraKeysManagerFacade/FacadeStartup.cs +++ b/LoRaEngine/LoraKeysManagerFacade/FacadeStartup.cs @@ -62,7 +62,6 @@ public override void Configure(IFunctionsHostBuilder builder) deviceCacheStore, sp.GetRequiredService(), sp.GetRequiredService>())) - .AddSingleton() .AddSingleton(sp => new RedisChannelPublisher(redis, sp.GetRequiredService>())) .AddSingleton() .AddSingleton() diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IDeviceRegistryManager.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IDeviceRegistryManager.cs index ce5cffedba..5f6538606a 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IDeviceRegistryManager.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IDeviceRegistryManager.cs @@ -23,16 +23,5 @@ public interface IDeviceRegistryManager IRegistryPageResult FindDeviceByDevEUI(DevEui devEUI); Task UpdateTwinAsync(string deviceName, IDeviceTwin twin, string eTag); Task RemoveDeviceAsync(string deviceId); - Task DeployEdgeDeviceAsync( - string deviceId, - string resetPin, - string spiSpeed, - string spiDev, - string publishingUserName, - string publishingPassword, - string networkId = Constants.NetworkId, - string lnsHostAddress = "ws://mylns:5000"); - Task DeployConcentratorAsync(string stationEuiString, string region, string networkId = Constants.NetworkId); - Task DeployEndDevicesAsync(); } } diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubRegistryManager.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubRegistryManager.cs index e3928d20e7..c5cbe442c2 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubRegistryManager.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoraTools/IoTHubImpl/IoTHubRegistryManager.cs @@ -113,133 +113,5 @@ public async Task GetLoRaDeviceTwinAsync(string deviceId, Cance public async Task GetTwinAsync(string deviceId, CancellationToken? cancellationToken = null) => await this.instance.GetTwinAsync(deviceId, cancellationToken ?? CancellationToken.None) is { } twin ? new IoTHubDeviceTwin(twin) : null; - - public async Task DeployEdgeDeviceAsync( - string deviceId, - string resetPin, - string spiSpeed, - string spiDev, - string publishingUserName, - string publishingPassword, - string networkId = Constants.NetworkId, - string lnsHostAddress = "ws://mylns:5000") - { - // Get function facade key - var base64Auth = Convert.ToBase64String(Encoding.Default.GetBytes($"{publishingUserName}:{publishingPassword}")); - var apiUrl = new Uri($"https://{Environment.GetEnvironmentVariable("WEBSITE_CONTENTSHARE")}.scm.azurewebsites.net"); - var siteUrl = new Uri($"https://{Environment.GetEnvironmentVariable("WEBSITE_CONTENTSHARE")}.azurewebsites.net"); - string jwt; - using (var client = this.httpClientFactory.CreateClient()) - { - client.DefaultRequestHeaders.Add("Authorization", $"Basic {base64Auth}"); - var result = await client.GetAsync(new Uri(apiUrl, "/api/functions/admin/token")); - jwt = (await result.Content.ReadAsStringAsync()).Trim('"'); // get JWT for call funtion key - } - - var facadeKey = string.Empty; - using (var client = this.httpClientFactory.CreateClient()) - { - client.DefaultRequestHeaders.Add("Authorization", "Bearer " + jwt); - var response = await client.GetAsync(new Uri(siteUrl, "/admin/host/keys")); - var jsonResult = await response.Content.ReadAsStringAsync(); - dynamic resObject = JsonConvert.DeserializeObject(jsonResult); - facadeKey = resObject.keys[0].value; - } - - var edgeGatewayDevice = new Device(deviceId) - { - Capabilities = new DeviceCapabilities() - { - IotEdge = true - } - }; - - _ = await this.instance.AddDeviceAsync(edgeGatewayDevice); - _ = await this.instance.AddModuleAsync(new Module(deviceId, "LoRaWanNetworkSrvModule")); - - async Task GetConfigurationContentAsync(Uri configLocation, IDictionary tokenReplacements) - { - using var httpClient = this.httpClientFactory.CreateClient(); - var json = await httpClient.GetStringAsync(configLocation); - foreach (var r in tokenReplacements) - json = json.Replace(r.Key, r.Value, StringComparison.Ordinal); - return JsonConvert.DeserializeObject(json); - } - - var deviceConfigurationContent = await GetConfigurationContentAsync(new Uri(Environment.GetEnvironmentVariable("DEVICE_CONFIG_LOCATION")), new Dictionary - { - ["[$reset_pin]"] = resetPin, - ["[$spi_speed]"] = string.IsNullOrEmpty(spiSpeed) || string.Equals(spiSpeed, "8", StringComparison.OrdinalIgnoreCase) ? string.Empty : ",'SPI_SPEED':{'value':'2'}", - ["[$spi_dev]"] = string.IsNullOrEmpty(spiDev) || string.Equals(spiDev, "0", StringComparison.OrdinalIgnoreCase) ? string.Empty : $",'SPI_DEV':{{'value':'{spiDev}'}}" - }); - - await this.instance.ApplyConfigurationContentOnDeviceAsync(deviceId, deviceConfigurationContent); - - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("LOG_ANALYTICS_WORKSPACE_ID"))) - { - this.logger.LogDebug("Opted-in to use Azure Monitor on the edge. Deploying the observability layer."); - // If Appinsights Key is set this means that user opted in to use Azure Monitor. - _ = await this.instance.AddModuleAsync(new Module(deviceId, "IotHubMetricsCollectorModule")); - var observabilityConfigurationContent = await GetConfigurationContentAsync(new Uri(Environment.GetEnvironmentVariable("OBSERVABILITY_CONFIG_LOCATION")), new Dictionary - { - ["[$iot_hub_resource_id]"] = Environment.GetEnvironmentVariable("IOT_HUB_RESOURCE_ID"), - ["[$log_analytics_workspace_id]"] = Environment.GetEnvironmentVariable("LOG_ANALYTICS_WORKSPACE_ID"), - ["[$log_analytics_shared_key]"] = Environment.GetEnvironmentVariable("LOG_ANALYTICS_WORKSPACE_KEY") - }); - - _ = await this.instance.AddConfigurationAsync(new Configuration($"obs-{Guid.NewGuid()}") - { - Content = observabilityConfigurationContent, - TargetCondition = $"deviceId='{deviceId}'" - }); - } - - var twin = new Twin(); - twin.Properties.Desired = new TwinCollection($"{{FacadeServerUrl:'https://{Environment.GetEnvironmentVariable("FACADE_HOST_NAME", EnvironmentVariableTarget.Process)}.azurewebsites.net/api/',FacadeAuthCode: '{facadeKey}'}}"); - twin.Properties.Desired["hostAddress"] = new Uri(lnsHostAddress); - twin.Tags[Constants.NetworkTagName] = networkId; - var remoteTwin = await this.instance.GetTwinAsync(deviceId); - - _ = await this.instance.UpdateTwinAsync(deviceId, "LoRaWanNetworkSrvModule", twin, remoteTwin.ETag); - } - - public async Task DeployConcentratorAsync(string stationEuiString, string region, string networkId = Constants.NetworkId) - { - // Deploy concentrator - using var httpClient = this.httpClientFactory.CreateClient(); - var regionalConfiguration = region switch - { - var s when string.Equals("EU", s, StringComparison.OrdinalIgnoreCase) => await httpClient.GetStringAsync(new Uri(Environment.GetEnvironmentVariable("EU863_CONFIG_LOCATION", EnvironmentVariableTarget.Process))), - var s when string.Equals("US", s, StringComparison.OrdinalIgnoreCase) => await httpClient.GetStringAsync(new Uri(Environment.GetEnvironmentVariable("US902_CONFIG_LOCATION", EnvironmentVariableTarget.Process))), - _ => throw new SwitchExpressionException("Region should be either 'EU' or 'US'") - }; - - var concentratorDevice = new Device(stationEuiString); - _ = await this.instance.AddDeviceAsync(concentratorDevice); - var concentratorTwin = await this.instance.GetTwinAsync(stationEuiString); - concentratorTwin.Properties.Desired["routerConfig"] = JsonConvert.DeserializeObject(regionalConfiguration); - concentratorTwin.Tags[Constants.NetworkTagName] = networkId; - _ = await this.instance.UpdateTwinAsync(stationEuiString, concentratorTwin, concentratorTwin.ETag); - } - - public async Task DeployEndDevicesAsync() - { - var otaaDevice = await this.instance.GetDeviceAsync(Constants.OtaaDeviceId) - ?? await this.instance.AddDeviceAsync(new Device(Constants.OtaaDeviceId)); - - var otaaEndTwin = new Twin(); - otaaEndTwin.Properties.Desired = new TwinCollection(/*lang=json*/ @"{AppEUI:'BE7A0000000014E2',AppKey:'8AFE71A145B253E49C3031AD068277A1',GatewayID:'',SensorDecoder:'DecoderValueSensor'}"); - var otaaRemoteTwin = _ = await this.instance.GetTwinAsync(Constants.OtaaDeviceId); - _ = await this.instance.UpdateTwinAsync(Constants.OtaaDeviceId, otaaEndTwin, otaaRemoteTwin.ETag); - - var abpDevice = await this.instance.GetDeviceAsync(Constants.AbpDeviceId) - ?? await this.instance.AddDeviceAsync(new Device(Constants.AbpDeviceId)); - var abpTwin = new Twin(); - abpTwin.Properties.Desired = new TwinCollection(/*lang=json*/ @"{AppSKey:'2B7E151628AED2A6ABF7158809CF4F3C',NwkSKey:'3B7E151628AED2A6ABF7158809CF4F3C',GatewayID:'',DevAddr:'0228B1B1',SensorDecoder:'DecoderValueSensor'}"); - var abpRemoteTwin = await this.instance.GetTwinAsync(Constants.AbpDeviceId); - _ = await this.instance.UpdateTwinAsync(Constants.AbpDeviceId, abpTwin, abpRemoteTwin.ETag); - - return abpDevice != null && otaaDevice != null; - } } } diff --git a/TemplateBicep/README.md b/TemplateBicep/README.md index ab3ff7d84c..58b5e112ab 100644 --- a/TemplateBicep/README.md +++ b/TemplateBicep/README.md @@ -39,7 +39,7 @@ For device creation debugging, there are alternatives other than deploying the w Option 1: Run bash script locally ```plain -FACADE_SERVER_URL="https://myapp.com/api" IOTHUB_CONNECTION_STRING="" LORA_CLI_URL="https://github.com/Azure/iotedge-lorawan-starterkit/releases/download/v2.2.0/lora-cli.linux-x64.tar.gz" EDGE_GATEWAY_NAME="" STATION_DEVICE_NAME="" DEPLOY_DEVICE=1 RESET_PIN= ./create_device.sh +FACADE_SERVER_URL="https://myapp.com/api" IOTHUB_CONNECTION_STRING="" LORA_CLI_URL="https://github.com/Azure/iotedge-lorawan-starterkit/releases/download/v2.2.0/lora-cli.linux-x64.tar.gz" EDGE_GATEWAY_NAME="" STATION_DEVICE_NAME="" DEPLOY_DEVICE=1 RESET_PIN= LORA_VERSION="" ./create_device.sh ``` Option 2: Run the device provisioning Bicep @@ -47,5 +47,5 @@ Option 2: Run the device provisioning Bicep ```plain az deployment group create --resource-group --template-file ./devices.bicep --parameters iothubName="" resetPin= edgeGatewayName="" spiSpeed= spiDev= functionAppName="" region="" stationEui="" logAnalyticsName="" -loraCliUrl="" deployDevice=true +loraCliUrl="" version="" deployDevice=true ``` diff --git a/TemplateBicep/create_device.sh b/TemplateBicep/create_device.sh index acc9d376d7..3882b5c62c 100644 --- a/TemplateBicep/create_device.sh +++ b/TemplateBicep/create_device.sh @@ -23,7 +23,7 @@ create_devices_with_lora_cli() { fi echo "Creating gateway $EDGE_GATEWAY_NAME..." - ./loradeviceprovisioning add-gateway --reset-pin "$RESET_PIN" --device-id "$EDGE_GATEWAY_NAME" --spi-dev "$SPI_DEV" --spi-speed "$SPI_SPEED" --api-url "$FACADE_SERVER_URL" --api-key "$FACADE_AUTH_CODE" --lns-host-address "$LNS_HOST_ADDRESS" --network "$NETWORK" --monitoring "$monitoringEnabled" --iothub-resource-id "$IOTHUB_RESOURCE_ID" --log-analytics-workspace-id "$LOG_ANALYTICS_WORKSPACE_ID" --log-analytics-shared-key "$LOG_ANALYTICS_SHARED_KEY" + ./loradeviceprovisioning add-gateway --reset-pin "$RESET_PIN" --device-id "$EDGE_GATEWAY_NAME" --spi-dev "$SPI_DEV" --spi-speed "$SPI_SPEED" --api-url "$FACADE_SERVER_URL" --api-key "$FACADE_AUTH_CODE" --lns-host-address "$LNS_HOST_ADDRESS" --network "$NETWORK" --monitoring "$monitoringEnabled" --iothub-resource-id "$IOTHUB_RESOURCE_ID" --log-analytics-workspace-id "$LOG_ANALYTICS_WORKSPACE_ID" --log-analytics-shared-key "$LOG_ANALYTICS_SHARED_KEY" --lora-version "$LORA_VERSION" echo "Creating concentrator $STATION_DEVICE_NAME for region $regionName..." ./loradeviceprovisioning add --type concentrator --region "$regionName" --stationeui "$STATION_DEVICE_NAME" --no-cups --network "$NETWORK" diff --git a/TemplateBicep/devices.bicep b/TemplateBicep/devices.bicep index 8e2c7c1976..a87eca9141 100644 --- a/TemplateBicep/devices.bicep +++ b/TemplateBicep/devices.bicep @@ -1,11 +1,11 @@ param location string = resourceGroup().location -param iothubName string = '' -param edgeGatewayName string = '' +param iothubName string +param edgeGatewayName string param resetPin int param spiSpeed int param spiDev int param utcValue string = utcNow() -param functionAppName string = '' +param functionAppName string param region string param stationEui string param lnsHostAddress string = 'ws://mylns:5000' @@ -13,6 +13,7 @@ param useAzureMonitorOnEdge bool = true param logAnalyticsName string param deployDevice bool param loraCliUrl string +param version string resource iotHub 'Microsoft.Devices/IotHubs@2021-07-02' existing = { name: iothubName @@ -106,6 +107,10 @@ resource createIothubDevices 'Microsoft.Resources/deploymentScripts@2020-10-01' name: 'LORA_CLI_URL' value: loraCliUrl } + { + name: 'LORA_VERSION' + value: version + } ] scriptContent: loadTextContent('./create_device.sh') } diff --git a/TemplateBicep/function.bicep b/TemplateBicep/function.bicep index 6c0fd7beaf..b58e600a4b 100644 --- a/TemplateBicep/function.bicep +++ b/TemplateBicep/function.bicep @@ -1,4 +1,3 @@ -param deployDevice bool param gitUsername string param version string @@ -106,14 +105,6 @@ resource azureFunction 'Microsoft.Web/sites@2022-03-01' = { name: 'FUNCTIONS_EXTENSION_VERSION' value: '~4' } - { - name: 'DEPLOY_DEVICE' - value: string(deployDevice) - } - { - name: 'DEVICE_CONFIG_LOCATION' - value: 'https://raw.githubusercontent.com/${gitUsername}/iotedge-lorawan-starterkit/v${version}/Template/deviceConfiguration.json' - } { name: 'APPINSIGHTS_INSTRUMENTATIONKEY' value: reference(appInsights.id, '2015-05-01').InstrumentationKey @@ -122,10 +113,6 @@ resource azureFunction 'Microsoft.Web/sites@2022-03-01' = { name: 'WEBSITE_RUN_FROM_PACKAGE' value: functionZipBinary } - { - name: 'OBSERVABILITY_CONFIG_LOCATION' - value: 'https://raw.githubusercontent.com/${gitUsername}/iotedge-lorawan-starterkit/v${version}/Template/observabilityConfiguration.json' - } { name: 'IOT_HUB_RESOURCE_ID' value: iotHub.id @@ -138,14 +125,6 @@ resource azureFunction 'Microsoft.Web/sites@2022-03-01' = { name: 'LOG_ANALYTICS_WORKSPACE_KEY' value: useAzureMonitorOnEdge ? listKeys(logAnalytics.id, '2022-10-01').primarySharedKey : '' } - { - name: 'EU863_CONFIG_LOCATION' - value: 'https://raw.githubusercontent.com/${gitUsername}/iotedge-lorawan-starterkit/v${version}/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/EU863.json' - } - { - name: 'US902_CONFIG_LOCATION' - value: 'https://raw.githubusercontent.com/${gitUsername}/iotedge-lorawan-starterkit/v${version}/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/US902.json' - } ] } } diff --git a/TemplateBicep/main.bicep b/TemplateBicep/main.bicep index 3e34d5fe3d..6e313ff86c 100644 --- a/TemplateBicep/main.bicep +++ b/TemplateBicep/main.bicep @@ -36,7 +36,7 @@ param useDiscoveryService bool = false @description('The Git Username. Default is Azure.') param gitUsername string = 'Azure' -@description('The Git version to use. Default is 2.2.0.') +@description('The LoRaWAN Starter Kit version to use.') param version string = '2.2.0' @description('The location of the cli tool to be used for device provisioning.') @@ -72,7 +72,6 @@ module function './function.bicep' = { params: { appInsightName: observability.outputs.appInsightName logAnalyticsName: observability.outputs.logAnalyticsName - deployDevice: deployDevice uniqueSolutionPrefix: uniqueSolutionPrefix useAzureMonitorOnEdge: useAzureMonitorOnEdge hostingPlanLocation: location @@ -121,5 +120,6 @@ module createDevices 'devices.bicep' = { spiSpeed: spiSpeed spiDev: spiDev loraCliUrl: loraCliUrl + version: version } } diff --git a/Tests/Simulation/LoRaWan.Tests.Simulation.csproj b/Tests/Simulation/LoRaWan.Tests.Simulation.csproj index 2decddd887..215a511ae1 100644 --- a/Tests/Simulation/LoRaWan.Tests.Simulation.csproj +++ b/Tests/Simulation/LoRaWan.Tests.Simulation.csproj @@ -5,7 +5,7 @@ false - + PreserveNewest diff --git a/Tests/Unit/IoTHubImpl/IoTHubRegistryManagerTests.cs b/Tests/Unit/IoTHubImpl/IoTHubRegistryManagerTests.cs index fb3e492598..3f498b9f02 100644 --- a/Tests/Unit/IoTHubImpl/IoTHubRegistryManagerTests.cs +++ b/Tests/Unit/IoTHubImpl/IoTHubRegistryManagerTests.cs @@ -371,411 +371,6 @@ public async Task GetDevicePrimaryKeyAsync() Assert.Equal(mockPrimaryKey, result); } - [Fact] - public async Task AddDevice() - { - // Arrange - using var manager = CreateManager(); - var devEUI = new DevEui(123456789); - var mockTwin = new Twin(devEUI.ToString()); - - var mockDeviceTwin = new IoTHubDeviceTwin(mockTwin); - - mockRegistryManager.Setup(c => c.AddDeviceWithTwinAsync(It.Is(d => d.Id == devEUI.ToString()), It.Is(t => t == mockTwin))) - .ReturnsAsync(new BulkRegistryOperationResult - { - IsSuccessful = true - }); - - // Act - var result = await manager.AddDeviceAsync(mockDeviceTwin); - - // Assert - Assert.True(result); - } - - [Fact] - public async Task WhenBulkOperationFailed_AddDevice_Should_Return_False() - { - // Arrange - using var manager = CreateManager(); - var devEUI = new DevEui(123456789); - var mockTwin = new Twin(devEUI.ToString()); - - var mockDeviceTwin = new IoTHubDeviceTwin(mockTwin); - - mockRegistryManager.Setup(c => c.AddDeviceWithTwinAsync(It.Is(d => d.Id == devEUI.ToString()), It.Is(t => t == mockTwin))) - .ReturnsAsync(new BulkRegistryOperationResult - { - IsSuccessful = false - }); - - // Act - var result = await manager.AddDeviceAsync(mockDeviceTwin); - - // Assert - Assert.False(result); - } - - [Theory] - [InlineData("2", "1", "3", "publishUserName", "publishPassword")] - [InlineData("2", "1", "3", "fakeUser", "fakePassword", "fakeNetworkId")] - [InlineData("2", "1", "3", "fakeUser", "fakePassword", "fakeNetworkId", "ws://fakelns:5000")] - public async Task DeployEdgeDevice(string resetPin, - string spiSpeed, - string spiDev, - string publishingUserName, - string publishingPassword, - string networkId = Constants.NetworkId, - string lnsHostAddress = "ws://mylns:5000") - { - // Arrange - using var manager = CreateManager(); - ConfigurationContent configurationContent = null; - Twin networkServerModuleTwin = null; - - var deviceId = this.SetupForEdgeDeployment( - publishingUserName, - publishingPassword, - (string _, ConfigurationContent content) => configurationContent = content, - (string _, string _, Twin t, string _) => networkServerModuleTwin = t); - - // Act - await manager.DeployEdgeDeviceAsync(deviceId, resetPin, spiSpeed, spiDev, publishingUserName, publishingPassword, networkId, lnsHostAddress); - - // Assert - Assert.Equal($"{{\"modulesContent\":{{\"$edgeAgent\":{{\"properties.desired\":{{\"modules\":{{\"LoRaBasicsStationModule\":{{\"env\":{{\"RESET_PIN\":{{\"value\":\"{resetPin}\"}},\"TC_URI\":{{\"value\":\"ws://172.17.0.1:5000\"}},\"SPI_DEV\":{{\"value\":\"{spiDev}\"}},\"SPI_SPEED\":{{\"value\":\"2\"}}}}}}}}}}}}}},\"moduleContent\":{{}},\"deviceContent\":{{}}}}", JsonConvert.SerializeObject(configurationContent)); - Assert.Equal($"{{\"deviceId\":null,\"etag\":null,\"version\":null,\"tags\":{{\"network\":\"{networkId}\"}},\"properties\":{{\"desired\":{{\"FacadeServerUrl\":\"https://fake-facade.azurewebsites.net/api/\",\"FacadeAuthCode\":\"uzW4cD3VH88di5UB8kr7U8Ri\",\"hostAddress\":\"{lnsHostAddress}\"}},\"reported\":{{}}}}}}", JsonConvert.SerializeObject(networkServerModuleTwin)); - - this.mockRegistryManager.Verify(c => c.AddDeviceAsync(It.IsAny()), Times.Once); - this.mockRegistryManager.Verify(c => c.AddModuleAsync(It.IsAny()), Times.Once); - this.mockRegistryManager.Verify(c => c.ApplyConfigurationContentOnDeviceAsync(It.Is(deviceId, StringComparer.OrdinalIgnoreCase), It.IsAny()), Times.Once); - this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.Is(deviceId, StringComparer.OrdinalIgnoreCase)), Times.Once); - this.mockRegistryManager.Verify(c => c.UpdateTwinAsync( - It.Is(deviceId, StringComparer.OrdinalIgnoreCase), - It.Is("LoRaWanNetworkSrvModule", StringComparer.OrdinalIgnoreCase), - It.IsAny(), - It.IsAny()), Times.Once); - - this.mockHttpClientHandler.VerifyNoOutstandingRequest(); - this.mockHttpClientHandler.VerifyNoOutstandingExpectation(); - } - - [Fact] - public async Task DeployEdgeDeviceWhenOmmitingSpiDevAndAndSpiSpeedSettingsAreNotSendToConfiguration() - { - var publishingUserName = Guid.NewGuid().ToString(); - var publishingPassword = Guid.NewGuid().ToString(); - - // Arrange - using var manager = CreateManager(); - ConfigurationContent configurationContent = null; - Twin networkServerModuleTwin = null; - - var deviceId = this.SetupForEdgeDeployment( - publishingUserName, - publishingPassword, - (string _, ConfigurationContent content) => configurationContent = content, - (string _, string _, Twin t, string _) => networkServerModuleTwin = t); - - // Act - await manager.DeployEdgeDeviceAsync(deviceId, "2", null, null, publishingUserName, publishingPassword, Constants.NetworkId, "ws://mylns:5000"); - - // Assert - Assert.Equal($"{{\"modulesContent\":{{\"$edgeAgent\":{{\"properties.desired\":{{\"modules\":{{\"LoRaBasicsStationModule\":{{\"env\":{{\"RESET_PIN\":{{\"value\":\"2\"}},\"TC_URI\":{{\"value\":\"ws://172.17.0.1:5000\"}}}}}}}}}}}}}},\"moduleContent\":{{}},\"deviceContent\":{{}}}}", JsonConvert.SerializeObject(configurationContent)); - - this.mockRegistryManager.Verify(c => c.AddDeviceAsync(It.IsAny()), Times.Once); - this.mockRegistryManager.Verify(c => c.AddModuleAsync(It.IsAny()), Times.Once); - this.mockRegistryManager.Verify(c => c.ApplyConfigurationContentOnDeviceAsync(It.Is(deviceId, StringComparer.OrdinalIgnoreCase), It.IsAny()), Times.Once); - this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.Is(deviceId, StringComparer.OrdinalIgnoreCase)), Times.Once); - this.mockRegistryManager.Verify(c => c.UpdateTwinAsync( - It.Is(deviceId, StringComparer.OrdinalIgnoreCase), - It.Is("LoRaWanNetworkSrvModule", StringComparer.OrdinalIgnoreCase), - It.IsAny(), - It.IsAny()), Times.Once); - - this.mockHttpClientHandler.VerifyNoOutstandingRequest(); - this.mockHttpClientHandler.VerifyNoOutstandingExpectation(); - } - - [Fact] - public async Task DeployEdgeDeviceSettingLogAnalyticsWorkspaceShouldDeployIotHubMetricsCollectorModule() - { - var publishingUserName = Guid.NewGuid().ToString(); - var publishingPassword = Guid.NewGuid().ToString(); - - // Arrange - using var manager = CreateManager(); - ConfigurationContent configurationContent = null; - Configuration iotHubMetricsCollectorModuleConfiguration = null; - - Twin networkServerModuleTwin = null; - - Environment.SetEnvironmentVariable("LOG_ANALYTICS_WORKSPACE_ID", "fake-workspace-id"); - Environment.SetEnvironmentVariable("IOT_HUB_RESOURCE_ID", "fake-hub-id"); - Environment.SetEnvironmentVariable("LOG_ANALYTICS_WORKSPACE_KEY", "fake-workspace-key"); - Environment.SetEnvironmentVariable("OBSERVABILITY_CONFIG_LOCATION", "https://fake.local/observabilityConfig.json"); - - var deviceId = this.SetupForEdgeDeployment( - publishingUserName, - publishingPassword, - (string _, ConfigurationContent content) => configurationContent = content, - (string _, string _, Twin t, string _) => networkServerModuleTwin = t); - - this.mockRegistryManager.Setup(c => c.AddModuleAsync(It.Is(m => m.DeviceId == deviceId && m.Id == "IotHubMetricsCollectorModule"))) - .ReturnsAsync((Module m) => m); - - this.mockRegistryManager.Setup(c => c.AddConfigurationAsync(It.Is(conf => conf.TargetCondition == $"deviceId='{deviceId}'"))) - .ReturnsAsync((Configuration c) => c) - .Callback((Configuration c) => iotHubMetricsCollectorModuleConfiguration = c); - -#pragma warning disable JSON001 // Invalid JSON pattern - _ = this.mockHttpClientHandler.When(HttpMethod.Get, "/observabilityConfig.json") - .Respond(HttpStatusCode.OK, MediaTypeNames.Application.Json, /*lang=json,strict*/ "{\"modulesContent\":{\"$edgeAgent\":{\"properties.desired.modules.IotHubMetricsCollectorModule\":{\"settings\":{\"image\":\"mcr.microsoft.com/azureiotedge-metrics-collector:1.0\"},\"type\":\"docker\",\"env\":{\"ResourceId\":{\"value\":\"[$iot_hub_resource_id]\"},\"UploadTarget\":{\"value\":\"AzureMonitor\"},\"LogAnalyticsWorkspaceId\":{\"value\":\"[$log_analytics_workspace_id]\"},\"LogAnalyticsSharedKey\":{\"value\":\"[$log_analytics_shared_key]\"},\"MetricsEndpointsCSV\":{\"value\":\"http://edgeHub:9600/metrics,http://edgeAgent:9600/metrics\"}},\"status\":\"running\",\"restartPolicy\":\"always\",\"version\":\"1.0\"}}}}"); -#pragma warning restore JSON001 // Invalid JSON pattern - - // Act - await manager.DeployEdgeDeviceAsync(deviceId, "2", null, null, publishingUserName, publishingPassword, Constants.NetworkId, "ws://mylns:5000"); - - // Assert - Assert.Equal(/*lang=json,strict*/ "{\"modulesContent\":{\"$edgeAgent\":{\"properties.desired\":{\"modules\":{\"LoRaBasicsStationModule\":{\"env\":{\"RESET_PIN\":{\"value\":\"2\"},\"TC_URI\":{\"value\":\"ws://172.17.0.1:5000\"}}}}}}},\"moduleContent\":{},\"deviceContent\":{}}", JsonConvert.SerializeObject(configurationContent)); - Assert.Equal(/*lang=json,strict*/ "{\"modulesContent\":{\"$edgeAgent\":{\"properties.desired.modules.IotHubMetricsCollectorModule\":{\"settings\":{\"image\":\"mcr.microsoft.com/azureiotedge-metrics-collector:1.0\"},\"type\":\"docker\",\"env\":{\"ResourceId\":{\"value\":\"fake-hub-id\"},\"UploadTarget\":{\"value\":\"AzureMonitor\"},\"LogAnalyticsWorkspaceId\":{\"value\":\"fake-workspace-id\"},\"LogAnalyticsSharedKey\":{\"value\":\"fake-workspace-key\"},\"MetricsEndpointsCSV\":{\"value\":\"http://edgeHub:9600/metrics,http://edgeAgent:9600/metrics\"}},\"status\":\"running\",\"restartPolicy\":\"always\",\"version\":\"1.0\"}}},\"moduleContent\":{},\"deviceContent\":{}}", JsonConvert.SerializeObject(iotHubMetricsCollectorModuleConfiguration.Content)); - - this.mockRegistryManager.Verify(c => c.AddDeviceAsync(It.IsAny()), Times.Once); - this.mockRegistryManager.Verify(c => c.AddModuleAsync(It.IsAny()), Times.Exactly(2)); - this.mockRegistryManager.Verify(c => c.AddModuleAsync(It.Is(m => m.DeviceId == deviceId && m.Id == "IotHubMetricsCollectorModule")), Times.Once); - this.mockRegistryManager.Verify(c => c.AddConfigurationAsync(It.Is(conf => conf.TargetCondition == $"deviceId='{deviceId}'")), Times.Once); - - this.mockRegistryManager.Verify(c => c.ApplyConfigurationContentOnDeviceAsync(It.Is(deviceId, StringComparer.OrdinalIgnoreCase), It.IsAny()), Times.Once); - this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.Is(deviceId, StringComparer.OrdinalIgnoreCase)), Times.Once); - this.mockRegistryManager.Verify(c => c.UpdateTwinAsync( - It.Is(deviceId, StringComparer.OrdinalIgnoreCase), - It.Is("LoRaWanNetworkSrvModule", StringComparer.OrdinalIgnoreCase), - It.IsAny(), - It.IsAny()), Times.Once); - - this.mockHttpClientHandler.VerifyNoOutstandingRequest(); - this.mockHttpClientHandler.VerifyNoOutstandingExpectation(); - } - - [Theory] - [InlineData("EU")] - [InlineData("US")] - [InlineData("EU", "fakeNetwork")] - [InlineData("US", "fakeNetwork")] - public async Task DeployConcentrator(string region, string networkId = Constants.NetworkId) - { - // Arrange - using var manager = CreateManager(); - Environment.SetEnvironmentVariable("EU863_CONFIG_LOCATION", "https://fake.local/eu863.config.json"); - Environment.SetEnvironmentVariable("US902_CONFIG_LOCATION", "https://fake.local/us902.config.json"); - const string stationEui = "123456789"; - var eTag = $"{DateTime.Now.Ticks}"; - - this.mockHttpClientFactory.Setup(c => c.CreateClient(It.IsAny())) - .Returns(() => mockHttpClientHandler.ToHttpClient()); - - _ = region switch - { - "EU" => this.mockHttpClientHandler.When(HttpMethod.Get, "/eu863.config.json") - .Respond(HttpStatusCode.OK, MediaTypeNames.Application.Json, /*lang=json,strict*/ "{\"config\":\"EU\"}"), - "US" => this.mockHttpClientHandler.When(HttpMethod.Get, "/us902.config.json") - .Respond(HttpStatusCode.OK, MediaTypeNames.Application.Json, /*lang=json,strict*/ "{\"config\":\"US\"}"), - _ => throw new ArgumentException($"{region} is not supported."), - }; - - this.mockRegistryManager.Setup(c => c.AddDeviceAsync(It.Is(d => d.Id == stationEui))) - .ReturnsAsync((Device d) => d); - - _ = this.mockRegistryManager.Setup(c => c.GetTwinAsync(It.Is(stationEui, StringComparer.OrdinalIgnoreCase))) - .ReturnsAsync(new Twin(stationEui) - { - ETag = eTag - }); - - _ = this.mockRegistryManager.Setup(c => c.UpdateTwinAsync( - It.Is(stationEui, StringComparer.OrdinalIgnoreCase), - It.IsAny(), - It.Is(eTag, StringComparer.OrdinalIgnoreCase))) - .ReturnsAsync((string _, Twin t, string _) => t) - .Callback((string _, Twin t, string _) => - { - Assert.Equal($"{{\"config\":\"{region}\"}}", JsonConvert.SerializeObject(t.Properties.Desired["routerConfig"])); - Assert.Equal($"\"{networkId}\"", JsonConvert.SerializeObject(t.Tags[Constants.NetworkTagName])); - }); - - // Act - await manager.DeployConcentratorAsync(stationEui, region, networkId); - - // Assert - this.mockRegistryManager.Verify(c => c.AddDeviceAsync(It.IsAny()), Times.Once); - this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.Is(stationEui, StringComparer.OrdinalIgnoreCase)), Times.Once); - this.mockRegistryManager.Verify(c => c.UpdateTwinAsync( - It.Is(stationEui, StringComparer.OrdinalIgnoreCase), - It.IsAny(), - It.Is(eTag, StringComparer.OrdinalIgnoreCase)), Times.Once); - - this.mockHttpClientHandler.VerifyNoOutstandingRequest(); - this.mockHttpClientHandler.VerifyNoOutstandingExpectation(); - } - - [Fact] - public async Task DeployConcentratorWithNotImplementedRegionShouldThrowSwitchExpressionException() - { - // Arrange - using var manager = CreateManager(); - - this.mockHttpClientFactory.Setup(c => c.CreateClient(It.IsAny())) - .Returns(() => mockHttpClientHandler.ToHttpClient()); - - // Act - _ = await Assert.ThrowsAsync(() => manager.DeployConcentratorAsync("123456789", "FAKE")); - } - - [Fact] - public async Task DeployEndDevicesShouldCreateEndDevices() - { - // Arrange - using var manager = CreateManager(); - - Dictionary deviceTwins = new(); - var eTag = $"{DateTime.Now.Ticks}"; - - this.mockRegistryManager.Setup(c => c.GetDeviceAsync(It.IsAny())) - .ReturnsAsync((string _) => null); - - this.mockRegistryManager.Setup(c => c.AddDeviceAsync(It.IsAny())) - .ReturnsAsync((Device d) => d); - - this.mockRegistryManager.Setup(c => c.GetTwinAsync(It.IsAny())) - .ReturnsAsync((string id) => new Twin(id) - { - ETag = eTag - }); - - this.mockRegistryManager.Setup(c => c.UpdateTwinAsync(It.IsAny(), It.IsAny(), It.Is(eTag, StringComparer.OrdinalIgnoreCase))) - .ReturnsAsync((string _, Twin t, string _) => t) - .Callback((string id, Twin t, string _) => deviceTwins.Add(id, t)); - - // Act - var result = await manager.DeployEndDevicesAsync(); - - // Assert - Assert.True(result); - var abpDevice = deviceTwins[Constants.AbpDeviceId]; - var otaaDevice = deviceTwins[Constants.OtaaDeviceId]; - - Assert.Equal(/*lang=json*/ "{\"AppEUI\":\"BE7A0000000014E2\",\"AppKey\":\"8AFE71A145B253E49C3031AD068277A1\",\"GatewayID\":\"\",\"SensorDecoder\":\"DecoderValueSensor\"}", JsonConvert.SerializeObject(otaaDevice.Properties.Desired)); - Assert.Equal(/*lang=json*/ "{\"AppSKey\":\"2B7E151628AED2A6ABF7158809CF4F3C\",\"NwkSKey\":\"3B7E151628AED2A6ABF7158809CF4F3C\",\"GatewayID\":\"\",\"DevAddr\":\"0228B1B1\",\"SensorDecoder\":\"DecoderValueSensor\"}", JsonConvert.SerializeObject(abpDevice.Properties.Desired)); - - this.mockRegistryManager.Verify(c => c.GetDeviceAsync(It.IsAny()), Times.Exactly(2)); - this.mockRegistryManager.Verify(c => c.AddDeviceAsync(It.IsAny()), Times.Exactly(2)); - this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.IsAny()), Times.Exactly(2)); - this.mockRegistryManager.Verify(c => c.UpdateTwinAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); - } - - [Fact] - public async Task DeployEndDevicesShouldBeIdempotent() - { - // Arrange - using var manager = CreateManager(); - - Dictionary deviceTwins = new(); - var eTag = $"{DateTime.Now.Ticks}"; - - this.mockRegistryManager.Setup(c => c.GetDeviceAsync(It.IsAny())) - .ReturnsAsync((string id) => new Device(id)); - - this.mockRegistryManager.Setup(c => c.GetTwinAsync(It.IsAny())) - .ReturnsAsync((string id) => new Twin(id) - { - ETag = eTag - }); - - this.mockRegistryManager.Setup(c => c.UpdateTwinAsync(It.IsAny(), It.IsAny(), It.Is(eTag, StringComparer.OrdinalIgnoreCase))) - .ReturnsAsync((string _, Twin t, string _) => t) - .Callback((string id, Twin t, string _) => deviceTwins.Add(id, t)); - - // Act - var result = await manager.DeployEndDevicesAsync(); - - // Assert - Assert.True(result); - var abpDevice = deviceTwins[Constants.AbpDeviceId]; - var otaaDevice = deviceTwins[Constants.OtaaDeviceId]; - - Assert.Equal(/*lang=json*/ "{\"AppEUI\":\"BE7A0000000014E2\",\"AppKey\":\"8AFE71A145B253E49C3031AD068277A1\",\"GatewayID\":\"\",\"SensorDecoder\":\"DecoderValueSensor\"}", JsonConvert.SerializeObject(otaaDevice.Properties.Desired)); - Assert.Equal(/*lang=json*/ "{\"AppSKey\":\"2B7E151628AED2A6ABF7158809CF4F3C\",\"NwkSKey\":\"3B7E151628AED2A6ABF7158809CF4F3C\",\"GatewayID\":\"\",\"DevAddr\":\"0228B1B1\",\"SensorDecoder\":\"DecoderValueSensor\"}", JsonConvert.SerializeObject(abpDevice.Properties.Desired)); - - this.mockRegistryManager.Verify(c => c.GetDeviceAsync(It.IsAny()), Times.Exactly(2)); - this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.IsAny()), Times.Exactly(2)); - this.mockRegistryManager.Verify(c => c.UpdateTwinAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); - } - - private string SetupForEdgeDeployment(string publishingUserName, string publishingPassword, - Action onApplyConfigurationContentOnDevice, - Func onUpdateLoRaWanNetworkServerModuleTwin) - { - this.mockHttpClientFactory.Setup(c => c.CreateClient(It.IsAny())) - .Returns(() => mockHttpClientHandler.ToHttpClient()); - - const string deviceId = "edgeTest"; - var eTag = $"{DateTime.Now.Ticks}"; - - Environment.SetEnvironmentVariable("FACADE_HOST_NAME", "fake-facade"); - Environment.SetEnvironmentVariable("WEBSITE_CONTENTSHARE", "fake.local"); - Environment.SetEnvironmentVariable("DEVICE_CONFIG_LOCATION", "https://fake.local/deviceConfiguration.json"); - - this.mockRegistryManager.Setup(c => c.AddDeviceAsync(It.Is(d => d.Id == deviceId && d.Capabilities.IotEdge))) - .ReturnsAsync((Device d) => d); - - this.mockRegistryManager.Setup(c => c.AddModuleAsync(It.Is(m => m.DeviceId == deviceId && m.Id == "LoRaWanNetworkSrvModule"))) - .ReturnsAsync((Module m) => m); - - _ = this.mockHttpClientHandler.When(HttpMethod.Get, "/api/functions/admin/token") - .With(c => - { - Assert.Equal("Basic", c.Headers.Authorization.Scheme); - Assert.Equal(Convert.ToBase64String(Encoding.Default.GetBytes($"{publishingUserName}:{publishingPassword}")), c.Headers.Authorization.Parameter); - - return true; - }) - .Respond(HttpStatusCode.OK, MediaTypeNames.Text.Plain, "JWT-BEARER-TOKEN"); - - _ = this.mockHttpClientHandler.When(HttpMethod.Get, "/admin/host/keys") - .With(c => - { - Assert.Equal("Bearer", c.Headers.Authorization.Scheme); - Assert.Equal("JWT-BEARER-TOKEN", c.Headers.Authorization.Parameter); - return true; - }) - .Respond(HttpStatusCode.OK, MediaTypeNames.Application.Json, /*lang=json,strict*/ "{\"keys\":[{\"name\":\"default\",\"value\":\"uzW4cD3VH88di5UB8kr7U8Ri\"},{\"name\":\"master\",\"value\":\"4bF86stCFr7ga8A7j59XEYnX\"}]}"); - -#pragma warning disable JSON001 // Invalid JSON pattern - _ = this.mockHttpClientHandler.When(HttpMethod.Get, "/deviceConfiguration.json") - .Respond(HttpStatusCode.OK, MediaTypeNames.Application.Json, /*lang=json,strict*/ "{\"modulesContent\":{\"$edgeAgent\":{\"properties.desired\":{\"modules\":{\"LoRaBasicsStationModule\":{\"env\":{\"RESET_PIN\":{\"value\":\"[$reset_pin]\"},\"TC_URI\":{\"value\":\"ws://172.17.0.1:5000\"}[$spi_dev][$spi_speed]}}}}}}}"); -#pragma warning restore JSON001 // Invalid JSON pattern - - _ = this.mockRegistryManager.Setup(c => c.ApplyConfigurationContentOnDeviceAsync(It.Is(deviceId, StringComparer.OrdinalIgnoreCase), It.IsAny())) - .Callback(onApplyConfigurationContentOnDevice) - .Returns(Task.CompletedTask); - - _ = this.mockRegistryManager.Setup(c => c.GetTwinAsync(It.Is(deviceId, StringComparer.OrdinalIgnoreCase))) - .ReturnsAsync(new Twin(deviceId) - { - ETag = eTag - }); - - _ = this.mockRegistryManager.Setup(c => c.UpdateTwinAsync( - It.Is(deviceId, StringComparer.OrdinalIgnoreCase), - It.Is("LoRaWanNetworkSrvModule", StringComparer.OrdinalIgnoreCase), - It.IsAny(), - It.Is(eTag, StringComparer.OrdinalIgnoreCase))) - .ReturnsAsync(onUpdateLoRaWanNetworkServerModuleTwin); - - return deviceId; - } - protected virtual ValueTask DisposeAsync(bool disposing) { if (!this.disposedValue) diff --git a/Tools/Cli-LoRa-Device-Provisioning/BuildForRelease.ps1 b/Tools/Cli-LoRa-Device-Provisioning/BuildForRelease.ps1 index bba1ead600..9c7bdb63c2 100644 --- a/Tools/Cli-LoRa-Device-Provisioning/BuildForRelease.ps1 +++ b/Tools/Cli-LoRa-Device-Provisioning/BuildForRelease.ps1 @@ -1,25 +1,26 @@ # Builds the cli and prepares it to be updated to a release $DotNetVersion="net6.0" +$ProjectFolder="./LoRaWan.Tools.CLI" Write-Host "📦 Build and package Linux x64 version..." -ForegroundColor DarkYellow -$LinuxDestinationRelativePath="./bin/Release/$DotNetVersion/linux-x64/lora-cli.linux-x64.tar.gz" -dotnet publish -r linux-x64 /p:PublishSingleFile=true --self-contained -c Release --verbosity quiet -tar -czf $LinuxDestinationRelativePath -C "./bin/Release/$DotNetVersion/linux-x64/publish" . +$LinuxDestinationRelativePath="$ProjectFolder/bin/Release/$DotNetVersion/linux-x64/lora-cli.linux-x64.tar.gz" +dotnet publish $ProjectFolder -r linux-x64 /p:PublishSingleFile=true --self-contained -c Release --verbosity quiet +tar -czf $LinuxDestinationRelativePath -C "$ProjectFolder/bin/Release/$DotNetVersion/linux-x64/publish" . Write-Host "📦 Build and package Linux musl x64 version..." -ForegroundColor DarkYellow -$LinuxMuslDestinationRelativePath="./bin/Release/$DotNetVersion/linux-musl-x64/lora-cli.linux-musl-x64.tar.gz" -dotnet publish -r linux-musl-x64 /p:PublishSingleFile=true --self-contained -c Release --verbosity quiet -tar -czf $LinuxMuslDestinationRelativePath -C "./bin/Release/$DotNetVersion/linux-musl-x64/publish" . +$LinuxMuslDestinationRelativePath="$ProjectFolder/bin/Release/$DotNetVersion/linux-musl-x64/lora-cli.linux-musl-x64.tar.gz" +dotnet publish $ProjectFolder -r linux-musl-x64 /p:PublishSingleFile=true --self-contained -c Release --verbosity quiet +tar -czf $LinuxMuslDestinationRelativePath -C "$ProjectFolder/bin/Release/$DotNetVersion/linux-musl-x64/publish" . Write-Host "📦 Build and package Win x64 version..." -ForegroundColor DarkYellow -$WindowsDestinationRelativePath="./bin/Release/$DotNetVersion/win-x64/lora-cli.win-x64.zip" +$WindowsDestinationRelativePath="$ProjectFolder/bin/Release/$DotNetVersion/win-x64/lora-cli.win-x64.zip" dotnet publish -r win-x64 /p:PublishSingleFile=true --self-contained -c Release --verbosity quiet -Compress-Archive -Force -Path ".\bin\Release\$DotNetVersion\win-x64\publish\" -DestinationPath $WindowsDestinationRelativePath +Compress-Archive -Force -Path "$ProjectFolder/bin/Release/$DotNetVersion/win-x64/publish" -DestinationPath $WindowsDestinationRelativePath Write-Host "📦 Build and package OSX x64 version..." -ForegroundColor DarkYellow -$OsxDestinationRelativePath=".\bin\Release\$DotNetVersion\osx-x64\lora-cli.osx-x64.zip" +$OsxDestinationRelativePath="$ProjectFolder/bin/Release/$DotNetVersion/osx-x64/lora-cli.osx-x64.zip" dotnet publish -r osx-x64 /p:PublishSingleFile=true --self-contained -c Release --verbosity quiet -Compress-Archive -Force -Path ".\bin\Release\$DotNetVersion\osx-x64\publish\" -DestinationPath $OsxDestinationRelativePath +Compress-Archive -Force -Path "$ProjectFolder/bin/Release/$DotNetVersion/osx-x64/publish" -DestinationPath $OsxDestinationRelativePath Write-Host "🥳 Build complete!" -ForegroundColor Green Write-Host "Linux x64 -> " -ForegroundColor DarkYellow -NoNewline diff --git a/Tools/Cli-LoRa-Device-Provisioning/Cli-LoRa-Device-Provisioning.sln b/Tools/Cli-LoRa-Device-Provisioning/Cli-LoRa-Device-Provisioning.sln index 30a1f01686..548f336e7a 100644 --- a/Tools/Cli-LoRa-Device-Provisioning/Cli-LoRa-Device-Provisioning.sln +++ b/Tools/Cli-LoRa-Device-Provisioning/Cli-LoRa-Device-Provisioning.sln @@ -1,9 +1,12 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28701.123 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32929.385 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cli-LoRa-Device-Provisioning", "Cli-LoRa-Device-Provisioning.csproj", "{87A3F193-8161-466F-9157-CCBFE8936DDE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoRaWan.Tools.CLI", "LoRaWan.Tools.CLI\LoRaWan.Tools.CLI.csproj", "{87A3F193-8161-466F-9157-CCBFE8936DDE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoRaWan.Tools.CLI.Tests.Unit", "Tests\LoRaWan.Tools.CLI.Tests.Unit\LoRaWan.Tools.CLI.Tests.Unit.csproj", "{D07D922B-478A-44FB-B126-7BCFBD38542A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{C7B0937F-63F4-4356-997B-42F0535F5292}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,10 +18,17 @@ Global {87A3F193-8161-466F-9157-CCBFE8936DDE}.Debug|Any CPU.Build.0 = Debug|Any CPU {87A3F193-8161-466F-9157-CCBFE8936DDE}.Release|Any CPU.ActiveCfg = Release|Any CPU {87A3F193-8161-466F-9157-CCBFE8936DDE}.Release|Any CPU.Build.0 = Release|Any CPU + {D07D922B-478A-44FB-B126-7BCFBD38542A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D07D922B-478A-44FB-B126-7BCFBD38542A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D07D922B-478A-44FB-B126-7BCFBD38542A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D07D922B-478A-44FB-B126-7BCFBD38542A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D07D922B-478A-44FB-B126-7BCFBD38542A} = {C7B0937F-63F4-4356-997B-42F0535F5292} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {00860FFA-EA04-4309-9923-180B16973CE7} EndGlobalSection diff --git a/Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/AddGatewayOption.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/AddGatewayOption.cs similarity index 95% rename from Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/AddGatewayOption.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/AddGatewayOption.cs index 4982f3e558..ebf1811f26 100644 --- a/Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/AddGatewayOption.cs +++ b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/AddGatewayOption.cs @@ -113,5 +113,12 @@ public class AddGatewayOption HelpText = "Indicates the Log Analytics shared key used to authenticate." )] public string LogAnalyticsSharedKey { get; set; } + + [Option( + "lora-version", + Required = true, + HelpText = "LoRaWAN Starter Kit version" + )] + public string LoRaVersion { get; set; } } } diff --git a/Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/AddOptions.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/AddOptions.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/AddOptions.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/AddOptions.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/BulkVerifyOptions.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/BulkVerifyOptions.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/BulkVerifyOptions.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/BulkVerifyOptions.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/DeviceType.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/DeviceType.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/DeviceType.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/DeviceType.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/ListOptions.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/ListOptions.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/ListOptions.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/ListOptions.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/QueryOptions.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/QueryOptions.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/QueryOptions.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/QueryOptions.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/RemoveOptions.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/RemoveOptions.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/RemoveOptions.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/RemoveOptions.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/RevokeOptions.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/RevokeOptions.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/RevokeOptions.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/RevokeOptions.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/RotateCertificateOptions.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/RotateCertificateOptions.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/RotateCertificateOptions.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/RotateCertificateOptions.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/UpdateOptions.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/UpdateOptions.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/UpdateOptions.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/UpdateOptions.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/UpgradeFirmwareOptions.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/UpgradeFirmwareOptions.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/UpgradeFirmwareOptions.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/UpgradeFirmwareOptions.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/VerifyOptions.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/VerifyOptions.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/CommandLineOptions/VerifyOptions.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/CommandLineOptions/VerifyOptions.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/Constants.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Constants.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/Constants.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Constants.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/AS923-1.json b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/DefaultRouterConfig/AS923-1.json similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/AS923-1.json rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/DefaultRouterConfig/AS923-1.json diff --git a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/AS923-2.json b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/DefaultRouterConfig/AS923-2.json similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/AS923-2.json rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/DefaultRouterConfig/AS923-2.json diff --git a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/AS923-3.json b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/DefaultRouterConfig/AS923-3.json similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/AS923-3.json rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/DefaultRouterConfig/AS923-3.json diff --git a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/CN470RP1.json b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/DefaultRouterConfig/CN470RP1.json similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/CN470RP1.json rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/DefaultRouterConfig/CN470RP1.json diff --git a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/CN470RP2.json b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/DefaultRouterConfig/CN470RP2.json similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/CN470RP2.json rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/DefaultRouterConfig/CN470RP2.json diff --git a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/EU863.json b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/DefaultRouterConfig/EU863.json similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/EU863.json rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/DefaultRouterConfig/EU863.json diff --git a/Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/US902.json b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/DefaultRouterConfig/US902.json similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/DefaultRouterConfig/US902.json rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/DefaultRouterConfig/US902.json diff --git a/Tools/Cli-LoRa-Device-Provisioning/DeviceTags.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/DeviceTags.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/DeviceTags.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/DeviceTags.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/Enums.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Enums.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/Enums.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Enums.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/Helpers/ConfigurationHelper.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Helpers/ConfigurationHelper.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/Helpers/ConfigurationHelper.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Helpers/ConfigurationHelper.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/Helpers/ConversionHelper.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Helpers/ConversionHelper.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/Helpers/ConversionHelper.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Helpers/ConversionHelper.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/Helpers/IoTDeviceHelper.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Helpers/IoTDeviceHelper.cs similarity index 99% rename from Tools/Cli-LoRa-Device-Provisioning/Helpers/IoTDeviceHelper.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Helpers/IoTDeviceHelper.cs index 595de7bf5a..da11330a90 100644 --- a/Tools/Cli-LoRa-Device-Provisioning/Helpers/IoTDeviceHelper.cs +++ b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Helpers/IoTDeviceHelper.cs @@ -18,7 +18,7 @@ namespace LoRaWan.Tools.CLI.Helpers internal class IoTDeviceHelper { - private const string DefaultRouterConfigFolder = "DefaultRouterConfig"; + internal const string DefaultRouterConfigFolder = "DefaultRouterConfig"; private static readonly string[] ClassTypes = { "A", "C" }; private static readonly string[] DeduplicationModes = { "None", "Drop", "Mark" }; @@ -974,7 +974,7 @@ public static Twin CreateConcentratorTwin(AddOptions opts, uint crcChecksum, Uri twin.Tags[DeviceTags.DeviceTypeTagName] = new string[] { DeviceTags.DeviceTypes.Concentrator }; twin.Tags[DeviceTags.RegionTagName] = opts.Region.ToLowerInvariant(); - if (string.IsNullOrEmpty(opts.Network)) + if (!string.IsNullOrEmpty(opts.Network)) { twin.Tags[DeviceTags.NetworkTagName] = opts.Network; } diff --git a/Tools/Cli-LoRa-Device-Provisioning/Helpers/NetIdHelper.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Helpers/NetIdHelper.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/Helpers/NetIdHelper.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Helpers/NetIdHelper.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/Helpers/ValidationHelper.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Helpers/ValidationHelper.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/Helpers/ValidationHelper.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Helpers/ValidationHelper.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/Keygen.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Keygen.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/Keygen.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Keygen.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/Cli-LoRa-Device-Provisioning.csproj b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/LoRaWan.Tools.CLI.csproj similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/Cli-LoRa-Device-Provisioning.csproj rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/LoRaWan.Tools.CLI.csproj diff --git a/Tools/Cli-LoRa-Device-Provisioning/Program.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Program.cs similarity index 75% rename from Tools/Cli-LoRa-Device-Provisioning/Program.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Program.cs index b557ddce9f..036ef9436f 100644 --- a/Tools/Cli-LoRa-Device-Provisioning/Program.cs +++ b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/Program.cs @@ -22,7 +22,6 @@ namespace LoRaWan.Tools.CLI public static class Program { - private static readonly ConfigurationHelper ConfigurationHelper = new ConfigurationHelper(); private const string EDGE_GATEWAY_MANIFEST_FILE = "./gateway-deployment-template.json"; private const string EDGE_GATEWAY_OBSERVABILITY_MANIFEST_FILE = "./gateway-observability-layer-template.json"; @@ -30,53 +29,24 @@ public static async Task Main(string[] args) { if (args is null) throw new ArgumentNullException(nameof(args)); + WriteAzureLogo(); + Console.WriteLine("Azure IoT Edge LoRaWAN Starter Kit LoRa Device Provisioning Tool."); + Console.Write("This tool complements "); + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine("http://aka.ms/lora"); + Console.ResetColor(); + Console.WriteLine(); + try { - WriteAzureLogo(); - Console.WriteLine("Azure IoT Edge LoRaWAN Starter Kit LoRa Device Provisioning Tool."); - Console.Write("This tool complements "); - Console.ForegroundColor = ConsoleColor.Blue; - Console.WriteLine("http://aka.ms/lora"); - Console.ResetColor(); - Console.WriteLine(); - - if (!ConfigurationHelper.ReadConfig(args)) + var configurationHelper = new ConfigurationHelper(); + if (!configurationHelper.ReadConfig(args)) { WriteToConsole("Failed to parse configuration.", ConsoleColor.Red); return (int)ExitCode.Error; } - using var parser = new Parser(config => - { - config.CaseInsensitiveEnumValues = true; - config.HelpWriter = Console.Error; - }); - - var success = await parser.ParseArguments(args) - .MapResult( - (ListOptions opts) => RunListAndReturnExitCode(opts), - (QueryOptions opts) => RunQueryAndReturnExitCode(opts), - (VerifyOptions opts) => RunVerifyAndReturnExitCode(opts), - (BulkVerifyOptions opts) => RunBulkVerifyAndReturnExitCode(opts), - (AddOptions opts) => RunAddAndReturnExitCode(opts), - (AddGatewayOption opts) => RunAddGatewayAndReturnExitCode(opts), - (UpdateOptions opts) => RunUpdateAndReturnExitCode(opts), - (RemoveOptions opts) => RunRemoveAndReturnExitCode(opts), - (RotateCertificateOptions opts) => RunRotateCertificateAndReturnExitCodeAsync(opts), - (RevokeOptions opts) => RunRevokeAndReturnExitCodeAsync(opts), - (UpgradeFirmwareOptions opts) => RunUpgradeFirmwareAndReturnExitCodeAsync(opts), - errs => Task.FromResult(false)); - - if (success) - { - WriteToConsole("Successfully terminated.", ConsoleColor.Green); - return (int)ExitCode.Success; - } - else - { - WriteToConsole("Terminated with errors.", ConsoleColor.Red); - return (int)ExitCode.Error; - } + return await Run(args, configurationHelper); } #pragma warning disable CA1031 // Do not catch general exception types // Fallback error handling for whole CLI. @@ -86,17 +56,52 @@ public static async Task Main(string[] args) WriteToConsole($"Terminated with error: {ex}.", ConsoleColor.Red); return (int)ExitCode.Error; } + } - static void WriteToConsole(string message, ConsoleColor color) + internal static async Task Run(string[] args, ConfigurationHelper configurationHelper) + { + using var parser = new Parser(config => { - Console.ForegroundColor = color; - Console.WriteLine(); - Console.WriteLine(message); - Console.ResetColor(); + config.CaseInsensitiveEnumValues = true; + config.HelpWriter = Console.Error; + }); + + var success = await parser.ParseArguments(args) + .MapResult( + (ListOptions opts) => RunListAndReturnExitCode(configurationHelper, opts), + (QueryOptions opts) => RunQueryAndReturnExitCode(configurationHelper, opts), + (VerifyOptions opts) => RunVerifyAndReturnExitCode(configurationHelper, opts), + (BulkVerifyOptions opts) => RunBulkVerifyAndReturnExitCode(configurationHelper, opts), + (AddOptions opts) => RunAddAndReturnExitCode(configurationHelper, opts), + (AddGatewayOption opts) => RunAddGatewayAndReturnExitCode(configurationHelper, opts), + (UpdateOptions opts) => RunUpdateAndReturnExitCode(configurationHelper, opts), + (RemoveOptions opts) => RunRemoveAndReturnExitCode(configurationHelper, opts), + (RotateCertificateOptions opts) => RunRotateCertificateAndReturnExitCodeAsync(configurationHelper, opts), + (RevokeOptions opts) => RunRevokeAndReturnExitCodeAsync(configurationHelper, opts), + (UpgradeFirmwareOptions opts) => RunUpgradeFirmwareAndReturnExitCodeAsync(configurationHelper, opts), + errs => Task.FromResult(false)); + + if (success) + { + WriteToConsole("Successfully terminated.", ConsoleColor.Green); + return (int)ExitCode.Success; } + else + { + WriteToConsole("Terminated with errors.", ConsoleColor.Red); + return (int)ExitCode.Error; + } + } + + private static void WriteToConsole(string message, ConsoleColor color) + { + Console.ForegroundColor = color; + Console.WriteLine(); + Console.WriteLine(message); + Console.ResetColor(); } - private static async Task RunListAndReturnExitCode(ListOptions opts) + private static async Task RunListAndReturnExitCode(ConfigurationHelper configurationHelper, ListOptions opts) { if (!int.TryParse(opts.Page, out var page)) page = 10; @@ -104,14 +109,14 @@ private static async Task RunListAndReturnExitCode(ListOptions opts) if (!int.TryParse(opts.Total, out var total)) total = -1; - var isSuccess = await IoTDeviceHelper.QueryDevices(ConfigurationHelper, page, total); + var isSuccess = await IoTDeviceHelper.QueryDevices(configurationHelper, page, total); return isSuccess; } - private static async Task RunQueryAndReturnExitCode(QueryOptions opts) + private static async Task RunQueryAndReturnExitCode(ConfigurationHelper configurationHelper, QueryOptions opts) { - var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.DevEui, ConfigurationHelper); + var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.DevEui, configurationHelper); if (twin != null) { @@ -125,14 +130,14 @@ private static async Task RunQueryAndReturnExitCode(QueryOptions opts) } } - private static async Task RunVerifyAndReturnExitCode(VerifyOptions opts) + private static async Task RunVerifyAndReturnExitCode(ConfigurationHelper configurationHelper, VerifyOptions opts) { - var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.DevEui, ConfigurationHelper); + var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.DevEui, configurationHelper); if (twin != null) { StatusConsole.WriteTwin(opts.DevEui, twin); - return IoTDeviceHelper.VerifyDeviceTwin(opts.DevEui, opts.NetId, twin, ConfigurationHelper, true); + return IoTDeviceHelper.VerifyDeviceTwin(opts.DevEui, opts.NetId, twin, configurationHelper, true); } else { @@ -141,12 +146,12 @@ private static async Task RunVerifyAndReturnExitCode(VerifyOptions opts) } } - private static async Task RunBulkVerifyAndReturnExitCode(BulkVerifyOptions opts) + private static async Task RunBulkVerifyAndReturnExitCode(ConfigurationHelper configurationHelper, BulkVerifyOptions opts) { if (!int.TryParse(opts.Page, out var page)) page = 0; - var isSuccess = await IoTDeviceHelper.QueryDevicesAndVerify(ConfigurationHelper, page); + var isSuccess = await IoTDeviceHelper.QueryDevicesAndVerify(configurationHelper, page); Console.WriteLine(); if (isSuccess) @@ -161,23 +166,23 @@ private static async Task RunBulkVerifyAndReturnExitCode(BulkVerifyOptions return isSuccess; } - private static async Task RunAddAndReturnExitCode(AddOptions opts) + private static async Task RunAddAndReturnExitCode(ConfigurationHelper configurationHelper, AddOptions opts) { opts = IoTDeviceHelper.CleanOptions(opts, true) as AddOptions; if (opts.Type == DeviceType.Concentrator) { - return await CreateConcentratorDevice(opts); + return await CreateConcentratorDevice(configurationHelper, opts); } var isSuccess = false; - opts = IoTDeviceHelper.CompleteMissingAddOptions(opts, ConfigurationHelper); + opts = IoTDeviceHelper.CompleteMissingAddOptions(opts, configurationHelper); - if (IoTDeviceHelper.VerifyDevice(opts, null, null, null, ConfigurationHelper, true)) + if (IoTDeviceHelper.VerifyDevice(opts, null, null, null, configurationHelper, true)) { var twin = IoTDeviceHelper.CreateDeviceTwin(opts); - isSuccess = await IoTDeviceHelper.WriteDeviceTwin(twin, opts.DevEui, ConfigurationHelper, true); + isSuccess = await IoTDeviceHelper.WriteDeviceTwin(twin, opts.DevEui, configurationHelper, true); } else { @@ -186,14 +191,14 @@ private static async Task RunAddAndReturnExitCode(AddOptions opts) if (isSuccess) { - var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.DevEui, ConfigurationHelper); + var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.DevEui, configurationHelper); StatusConsole.WriteTwin(opts.DevEui, twin); } return isSuccess; } - private static async Task RunAddGatewayAndReturnExitCode(AddGatewayOption opts) + private static async Task RunAddGatewayAndReturnExitCode(ConfigurationHelper configurationHelper, AddGatewayOption opts) { if (true == opts.MonitoringEnabled) { @@ -216,7 +221,7 @@ private static async Task RunAddGatewayAndReturnExitCode(AddGatewayOption } var deploymentLayerContent = await GetEdgeObservabilityDeployment(opts); - if (!await IoTDeviceHelper.CreateObservabilityDeploymentLayer(opts, deploymentLayerContent, ConfigurationHelper)) + if (!await IoTDeviceHelper.CreateObservabilityDeploymentLayer(opts, deploymentLayerContent, configurationHelper)) { StatusConsole.WriteLogLine(MessageType.Error, "Failed to deploy observability deployment layer."); return false; @@ -224,7 +229,7 @@ private static async Task RunAddGatewayAndReturnExitCode(AddGatewayOption } var deviceConfigurationContent = await GetEdgeGatewayDeployment(opts); - return await IoTDeviceHelper.CreateGatewayTwin(opts, deviceConfigurationContent, ConfigurationHelper); + return await IoTDeviceHelper.CreateGatewayTwin(opts, deviceConfigurationContent, configurationHelper); } private static async Task GetEdgeObservabilityDeployment(AddGatewayOption opts) @@ -251,13 +256,14 @@ private static async Task GetEdgeGatewayDeployment(AddGate var tokenReplacements = new Dictionary { { "[$reset_pin]", opts.ResetPin.ToString() }, - { "[\"$spi_speed\"]", opts.SpiSpeed != AddGatewayOption.DefaultSpiSpeed ? string.Empty : ",\"SPI_SPEED\":{\"value\":\"2\"}" }, - { "[\"$spi_dev\"]", opts.SpiDev != AddGatewayOption.DefaultSpiDev ? string.Empty : $",\"SPI_DEV\":{{\"value\":\"{opts.SpiDev}\"}}" }, + { "[\"$spi_speed\"]", opts.SpiSpeed == AddGatewayOption.DefaultSpiSpeed ? string.Empty : ",\"SPI_SPEED\":{\"value\":\"2\"}" }, + { "[\"$spi_dev\"]", opts.SpiDev == AddGatewayOption.DefaultSpiDev ? string.Empty : $",\"SPI_DEV\":{{\"value\":\"{opts.SpiDev}\"}}" }, { "[$TWIN_FACADE_SERVER_URL]", opts.ApiURL.ToString() }, { "[$TWIN_FACADE_AUTH_CODE]", opts.ApiAuthCode }, { "[$TWIN_HOST_ADDRESS]", opts.TwinHostAddress }, { "[$TWIN_NETWORK]", opts.Network }, - { "[$az_edge_version]", opts.AzureIotEdgeVersion } + { "[$az_edge_version]", opts.AzureIotEdgeVersion }, + { "[$lora_version]", opts.LoRaVersion }, }; foreach (var token in tokenReplacements) @@ -268,17 +274,17 @@ private static async Task GetEdgeGatewayDeployment(AddGate return JsonConvert.DeserializeObject(manifest); } - private static async Task CreateConcentratorDevice(AddOptions opts) + private static async Task CreateConcentratorDevice(ConfigurationHelper configurationHelper, AddOptions opts) { var isVerified = IoTDeviceHelper.VerifyConcentrator(opts); if (!isVerified) return false; - if (!opts.NoCups && ConfigurationHelper.CertificateStorageContainerClient is null) + if (!opts.NoCups && configurationHelper.CertificateStorageContainerClient is null) { StatusConsole.WriteLogLine(MessageType.Error, "Storage account is not correctly configured."); return false; } - if (await IoTDeviceHelper.QueryDeviceTwin(opts.StationEui, ConfigurationHelper) is not null) + if (await IoTDeviceHelper.QueryDeviceTwin(opts.StationEui, configurationHelper) is not null) { StatusConsole.WriteLogLine(MessageType.Error, "Station was already created, please use the 'update' verb to update an existing station."); return false; @@ -287,19 +293,19 @@ private static async Task CreateConcentratorDevice(AddOptions opts) if (opts.NoCups) { var twin = IoTDeviceHelper.CreateConcentratorTwin(opts, 0, null); - return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, ConfigurationHelper, true); + return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, configurationHelper, true); } else { - return await UploadCertificateBundleAsync(opts.CertificateBundleLocation, opts.StationEui, async (crcHash, bundleStorageUri) => + return await UploadCertificateBundleAsync(configurationHelper, opts.CertificateBundleLocation, opts.StationEui, async (crcHash, bundleStorageUri) => { var twin = IoTDeviceHelper.CreateConcentratorTwin(opts, crcHash, bundleStorageUri); - return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, ConfigurationHelper, true); + return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, configurationHelper, true); }); } } - private static async Task RunRotateCertificateAndReturnExitCodeAsync(RotateCertificateOptions opts) + private static async Task RunRotateCertificateAndReturnExitCodeAsync(ConfigurationHelper configurationHelper, RotateCertificateOptions opts) { if (!File.Exists(opts.CertificateBundleLocation)) { @@ -307,7 +313,7 @@ private static async Task RunRotateCertificateAndReturnExitCodeAsync(Rotat return false; } - var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.StationEui, ConfigurationHelper); + var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.StationEui, configurationHelper); if (twin is null) { @@ -321,7 +327,7 @@ private static async Task RunRotateCertificateAndReturnExitCodeAsync(Rotat var oldTcCredentialBundleLocation = new Uri(cupsProperties[TwinProperty.TcCredentialUrl].ToString()); // Upload new certificate bundle - var success = await UploadCertificateBundleAsync(opts.CertificateBundleLocation, opts.StationEui, async (crcHash, bundleStorageUri) => + var success = await UploadCertificateBundleAsync(configurationHelper, opts.CertificateBundleLocation, opts.StationEui, async (crcHash, bundleStorageUri) => { var thumbprints = (JArray)twinJObject[TwinProperty.ClientThumbprint]; if (!thumbprints.Any(t => string.Equals(t.ToString(), opts.ClientCertificateThumbprint, StringComparison.OrdinalIgnoreCase))) @@ -333,25 +339,25 @@ private static async Task RunRotateCertificateAndReturnExitCodeAsync(Rotat twin.Properties.Desired[TwinProperty.Cups][TwinProperty.CupsCredentialUrl] = bundleStorageUri; twin.Properties.Desired[TwinProperty.Cups][TwinProperty.TcCredentialUrl] = bundleStorageUri; - return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, ConfigurationHelper, isNewDevice: false); + return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, configurationHelper, isNewDevice: false); }); // Clean up old certificate bundles try { - _ = await ConfigurationHelper.CertificateStorageContainerClient.DeleteBlobIfExistsAsync(oldCupsCredentialBundleLocation.Segments.Last()); + _ = await configurationHelper.CertificateStorageContainerClient.DeleteBlobIfExistsAsync(oldCupsCredentialBundleLocation.Segments.Last()); } finally { - _ = await ConfigurationHelper.CertificateStorageContainerClient.DeleteBlobIfExistsAsync(oldTcCredentialBundleLocation.Segments.Last()); + _ = await configurationHelper.CertificateStorageContainerClient.DeleteBlobIfExistsAsync(oldTcCredentialBundleLocation.Segments.Last()); } return success; } - private static async Task RunRevokeAndReturnExitCodeAsync(RevokeOptions opts) + private static async Task RunRevokeAndReturnExitCodeAsync(ConfigurationHelper configurationHelper, RevokeOptions opts) { - var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.StationEui, ConfigurationHelper); + var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.StationEui, configurationHelper); if (twin is null) { @@ -368,25 +374,25 @@ private static async Task RunRevokeAndReturnExitCodeAsync(RevokeOptions op t?.Remove(); twin.Properties.Desired[TwinProperty.ClientThumbprint] = clientThumprints; - return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, ConfigurationHelper, isNewDevice: false); + return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, configurationHelper, isNewDevice: false); } - private static async Task RunUpdateAndReturnExitCode(UpdateOptions opts) + private static async Task RunUpdateAndReturnExitCode(ConfigurationHelper configurationHelper, UpdateOptions opts) { var isSuccess = false; opts = IoTDeviceHelper.CleanOptions(opts, false) as UpdateOptions; - opts = IoTDeviceHelper.CompleteMissingUpdateOptions(opts, ConfigurationHelper); + opts = IoTDeviceHelper.CompleteMissingUpdateOptions(opts, configurationHelper); - var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.DevEui, ConfigurationHelper); + var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.DevEui, configurationHelper); if (twin != null) { twin = IoTDeviceHelper.UpdateDeviceTwin(twin, opts); - if (IoTDeviceHelper.VerifyDeviceTwin(opts.DevEui, opts.NetId, twin, ConfigurationHelper, true)) + if (IoTDeviceHelper.VerifyDeviceTwin(opts.DevEui, opts.NetId, twin, configurationHelper, true)) { - isSuccess = await IoTDeviceHelper.WriteDeviceTwin(twin, opts.DevEui, ConfigurationHelper, false); + isSuccess = await IoTDeviceHelper.WriteDeviceTwin(twin, opts.DevEui, configurationHelper, false); if (isSuccess) { @@ -414,10 +420,10 @@ private static async Task RunUpdateAndReturnExitCode(UpdateOptions opts) return isSuccess; } - private static async Task UploadCertificateBundleAsync(string certificateBundleLocation, string stationEui, Func> uploadSuccessActionAsync) + private static async Task UploadCertificateBundleAsync(ConfigurationHelper configurationHelper, string certificateBundleLocation, string stationEui, Func> uploadSuccessActionAsync) { var certificateBundleBlobName = $"{stationEui}-{Guid.NewGuid():N}"; - var blobClient = ConfigurationHelper.CertificateStorageContainerClient.GetBlobClient(certificateBundleBlobName); + var blobClient = configurationHelper.CertificateStorageContainerClient.GetBlobClient(certificateBundleBlobName); var fileContent = File.ReadAllBytes(certificateBundleLocation); try @@ -447,7 +453,7 @@ private static async Task UploadCertificateBundleAsync(string certificateB Task CleanupAsync() => blobClient.DeleteIfExistsAsync(); } - private static async Task RunUpgradeFirmwareAndReturnExitCodeAsync(UpgradeFirmwareOptions opts) + private static async Task RunUpgradeFirmwareAndReturnExitCodeAsync(ConfigurationHelper configurationHelper, UpgradeFirmwareOptions opts) { if (!File.Exists(opts.FirmwareLocation)) { @@ -468,9 +474,9 @@ private static async Task RunUpgradeFirmwareAndReturnExitCodeAsync(Upgrade } // Upload firmware file to storage account - var success = await UploadFirmwareAsync(opts.FirmwareLocation, opts.StationEui, opts.Package, async (firmwareBlobUri) => + var success = await UploadFirmwareAsync(configurationHelper, opts.FirmwareLocation, opts.StationEui, opts.Package, async (firmwareBlobUri) => { - var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.StationEui, ConfigurationHelper); + var twin = await IoTDeviceHelper.QueryDeviceTwin(opts.StationEui, configurationHelper); if (twin is null) { @@ -490,16 +496,16 @@ private static async Task RunUpgradeFirmwareAndReturnExitCodeAsync(Upgrade twin.Properties.Desired[TwinProperty.Cups][TwinProperty.FirmwareKeyChecksum] = checksum; twin.Properties.Desired[TwinProperty.Cups][TwinProperty.FirmwareSignature] = File.ReadAllText(opts.DigestLocation, Encoding.UTF8); - return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, ConfigurationHelper, isNewDevice: false); + return await IoTDeviceHelper.WriteDeviceTwin(twin, opts.StationEui, configurationHelper, isNewDevice: false); }); return success; } - private static async Task UploadFirmwareAsync(string firmwareLocation, string stationEui, string package, Func> uploadSuccessActionAsync) + private static async Task UploadFirmwareAsync(ConfigurationHelper configurationHelper, string firmwareLocation, string stationEui, string package, Func> uploadSuccessActionAsync) { var firmwareBlobName = $"{stationEui}-{package}"; - var blobClient = ConfigurationHelper.FirmwareStorageContainerClient.GetBlobClient(firmwareBlobName); + var blobClient = configurationHelper.FirmwareStorageContainerClient.GetBlobClient(firmwareBlobName); var fileContent = File.ReadAllBytes(firmwareLocation); StatusConsole.WriteLogLine(MessageType.Info, $"Uploading firmware {firmwareBlobName} to storage account..."); @@ -533,9 +539,9 @@ private static async Task UploadFirmwareAsync(string firmwareLocation, str Task CleanupAsync() => blobClient.DeleteIfExistsAsync(); } - private static async Task RunRemoveAndReturnExitCode(RemoveOptions opts) + private static async Task RunRemoveAndReturnExitCode(ConfigurationHelper configurationHelper, RemoveOptions opts) { - return await IoTDeviceHelper.RemoveDevice(opts.DevEui, ConfigurationHelper); + return await IoTDeviceHelper.RemoveDevice(opts.DevEui, configurationHelper); } private static void WriteAzureLogo() diff --git a/Tools/Cli-LoRa-Device-Provisioning/StatusConsole.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/StatusConsole.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/StatusConsole.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/StatusConsole.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/TwinProperty.cs b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/TwinProperty.cs similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/TwinProperty.cs rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/TwinProperty.cs diff --git a/Tools/Cli-LoRa-Device-Provisioning/appsettings.json b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/appsettings.json similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/appsettings.json rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/appsettings.json diff --git a/Tools/Cli-LoRa-Device-Provisioning/gateway-deployment-template.json b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/gateway-deployment-template.json similarity index 95% rename from Tools/Cli-LoRa-Device-Provisioning/gateway-deployment-template.json rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/gateway-deployment-template.json index 855a1669a1..cc96a51b62 100644 --- a/Tools/Cli-LoRa-Device-Provisioning/gateway-deployment-template.json +++ b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/gateway-deployment-template.json @@ -46,7 +46,7 @@ "LoRaWanNetworkSrvModule": { "type": "docker", "settings": { - "image": "loraedge/lorawannetworksrvmodule:2.2.0", + "image": "loraedge/lorawannetworksrvmodule:[$lora_version]", "createOptions": "{\"ExposedPorts\": { \"5000/tcp\": {}}, \"HostConfig\": { \"PortBindings\": {\"5000/tcp\": [ { \"HostPort\": \"5000\", \"HostIp\":\"172.17.0.1\" } ]}}}" }, "version": "1.0", @@ -64,7 +64,7 @@ "LoRaBasicsStationModule": { "type": "docker", "settings": { - "image": "loraedge/lorabasicsstationmodule:2.2.0", + "image": "loraedge/lorabasicsstationmodule:[$lora_version]", "createOptions": " {\"HostConfig\": {\"NetworkMode\": \"host\", \"Privileged\": true }, \"NetworkingConfig\": {\"EndpointsConfig\": {\"host\": {} }}}" }, "env": { diff --git a/Tools/Cli-LoRa-Device-Provisioning/gateway-observability-layer-template.json b/Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/gateway-observability-layer-template.json similarity index 100% rename from Tools/Cli-LoRa-Device-Provisioning/gateway-observability-layer-template.json rename to Tools/Cli-LoRa-Device-Provisioning/LoRaWan.Tools.CLI/gateway-observability-layer-template.json diff --git a/Tools/Cli-LoRa-Device-Provisioning/README.md b/Tools/Cli-LoRa-Device-Provisioning/README.md index 14ae997da3..c53fa576d3 100644 --- a/Tools/Cli-LoRa-Device-Provisioning/README.md +++ b/Tools/Cli-LoRa-Device-Provisioning/README.md @@ -1,3 +1,8 @@ # Device Provisioning -This file has been moved to: \ No newline at end of file +Main documentation is found here: . + +## Build release artifacts + +To build release artifacts use the PowerShell script provided (BuildForRelease.ps1). +It creats a self-container package of the cli for different platforms (Windows, Linux and MacOS). diff --git a/Tools/Cli-LoRa-Device-Provisioning/Tests/LoRaWan.Tools.CLI.Tests.Unit/.editorconfig b/Tools/Cli-LoRa-Device-Provisioning/Tests/LoRaWan.Tools.CLI.Tests.Unit/.editorconfig new file mode 100644 index 0000000000..3df6c0c252 --- /dev/null +++ b/Tools/Cli-LoRa-Device-Provisioning/Tests/LoRaWan.Tools.CLI.Tests.Unit/.editorconfig @@ -0,0 +1,31 @@ +[*.cs] + +# CA1707: Identifiers should not contain underscores +dotnet_diagnostic.CA1707.severity = none + +# CA1303: Console.WriteLine should get strings from resource table +dotnet_diagnostic.CA1303.severity = none + +# Expression value is never used +dotnet_diagnostic.IDE0058.severity=suggestion + +# CA5394: Do not use insecure randomness +dotnet_diagnostic.CA5394.severity = none + +# CA5399: Definitely disable HttpClient certificate revocation list check +dotnet_diagnostic.CA5399.severity = none + +# CA1062: Validate arguments of public methods +dotnet_diagnostic.CA1062.severity = suggestion + +# xUnit1004: Do not skip tests +dotnet_diagnostic.xUnit1004.severity=suggestion + +# CA1034: Do not nest types +dotnet_diagnostic.CA1034.severity = none + +# CA1031: Do not catch general exception types +dotnet_diagnostic.CA1031.severity=suggestion + +# IDE0055: Fix formatting +dotnet_diagnostic.IDE0055.severity=suggestion diff --git a/Tools/Cli-LoRa-Device-Provisioning/Tests/LoRaWan.Tools.CLI.Tests.Unit/DeviceProvisioningTest.cs b/Tools/Cli-LoRa-Device-Provisioning/Tests/LoRaWan.Tools.CLI.Tests.Unit/DeviceProvisioningTest.cs new file mode 100644 index 0000000000..6310787aa8 --- /dev/null +++ b/Tools/Cli-LoRa-Device-Provisioning/Tests/LoRaWan.Tools.CLI.Tests.Unit/DeviceProvisioningTest.cs @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tools.CLI.Tests.Unit +{ + using System.Globalization; + using LoRaWan.Tools.CLI.Helpers; + using Microsoft.Azure.Devices; + using Microsoft.Azure.Devices.Shared; + using Moq; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + public class DeviceProvisioningTest + { + private readonly ConfigurationHelper configurationHelper; + private readonly Mock registryManager; + + private const string NetworkName = "quickstartnetwork"; + private const string DevEUI = "46AAC86800430028"; + private const string Decoder = "DecoderValueSensor"; + private const string LoRaVersion = "999.999.10"; // using an non-existing version to ensure it is not hardcoded with a valid value + private const string IotEdgeVersion = "1.4"; + + // OTAA Properties + private const string AppKey = "8AFE71A145B253E49C3031AD068277A1"; + private const string AppEui = "BE7A0000000014E2"; + + // ABP properties + private const string AppSKey = "2B7E151628AED2A6ABF7158809CF4F3C"; + private const string NwkSKey = "3B7E151628AED2A6ABF7158809CF4F3C"; + private const string DevAddr = "0228B1B1"; + + public DeviceProvisioningTest() + { + this.registryManager = new Mock(); + this.configurationHelper = new ConfigurationHelper + { + NetId = ValidationHelper.CleanNetId(Constants.DefaultNetId.ToString(CultureInfo.InvariantCulture)), + RegistryManager = this.registryManager.Object + }; + } + + private static string[] CreateArgs(string input) + { + return input.Split(' ', StringSplitOptions.RemoveEmptyEntries); + } + + private static IDictionary GetConcentratorRouterConfig(string region) + { + if (region is null) throw new ArgumentNullException(nameof(region)); + var fileName = Path.Combine(IoTDeviceHelper.DefaultRouterConfigFolder, $"{region.ToUpperInvariant()}.json"); + var raw = File.ReadAllText(fileName); + + return JsonConvert.DeserializeObject>(raw)!; + } + + [Fact] + public async Task AddABPDevice() + { + // Arrange + var savedTwin = new Twin(); + + this.registryManager.Setup(c => c.AddDeviceWithTwinAsync( + It.Is(d => d.Id == DevEUI.ToString()), + It.IsNotNull())) + .Callback((Device d, Twin t) => + { + Assert.Equal(NetworkName, t.Tags[DeviceTags.NetworkTagName].ToString()); + Assert.Equal(new string[] { DeviceTags.DeviceTypes.Leaf }, ((JArray)t.Tags[DeviceTags.DeviceTypeTagName]).Select(x => x.ToString()).ToArray()); + Assert.Equal(AppSKey, t.Properties.Desired[TwinProperty.AppSKey].ToString()); + Assert.Equal(NwkSKey, t.Properties.Desired[TwinProperty.NwkSKey].ToString()); + Assert.Equal(DevAddr, t.Properties.Desired[TwinProperty.DevAddr].ToString()); + Assert.Equal(Decoder, t.Properties.Desired[TwinProperty.SensorDecoder].ToString()); + Assert.Equal(string.Empty, t.Properties.Desired[TwinProperty.GatewayID].ToString()); + savedTwin = t; + }) + .ReturnsAsync(new BulkRegistryOperationResult + { + IsSuccessful = true + }); + + this.registryManager.Setup(x => x.GetTwinAsync(DevEUI)) + .ReturnsAsync(savedTwin); + + // Act + var args = CreateArgs($"add --type abp --deveui {DevEUI} --appskey {AppSKey} --nwkskey {NwkSKey} --devaddr {DevAddr} --decoder {Decoder} --network {NetworkName}"); + var actual = await Program.Run(args, this.configurationHelper); + + // Assert + Assert.Equal(0, actual); + this.registryManager.Verify(c => c.AddDeviceWithTwinAsync( + It.Is(d => d.Id == DevEUI.ToString()), + It.IsNotNull()), Times.Once()); + } + + [Fact] + public async Task AddOTAADevice() + { + // Arrange + var savedTwin = new Twin(); + + this.registryManager.Setup(c => c.AddDeviceWithTwinAsync( + It.Is(d => d.Id == DevEUI.ToString()), + It.IsNotNull())) + .Callback((Device d, Twin t) => + { + Assert.Equal(NetworkName, t.Tags[DeviceTags.NetworkTagName].ToString()); + Assert.Equal(new string[] { DeviceTags.DeviceTypes.Leaf }, ((JArray)t.Tags[DeviceTags.DeviceTypeTagName]).Select(x => x.ToString()).ToArray()); + Assert.Equal(AppKey, t.Properties.Desired[TwinProperty.AppKey].ToString()); + Assert.Equal(AppEui, t.Properties.Desired[TwinProperty.AppEUI].ToString()); + Assert.Equal(Decoder, t.Properties.Desired[TwinProperty.SensorDecoder].ToString()); + Assert.Equal(string.Empty, t.Properties.Desired[TwinProperty.GatewayID].ToString()); + savedTwin = t; + }) + .ReturnsAsync(new BulkRegistryOperationResult + { + IsSuccessful = true + }); + + this.registryManager.Setup(x => x.GetTwinAsync(DevEUI)) + .ReturnsAsync(savedTwin); + + // Act + var args = CreateArgs($"add --type otaa --deveui {DevEUI} --appeui {AppEui} --appkey {AppKey} --decoder {Decoder} --network {NetworkName}"); + var actual = await Program.Run(args, this.configurationHelper); + + // Assert + Assert.Equal(0, actual); + this.registryManager.Verify(c => c.AddDeviceWithTwinAsync( + It.Is(d => d.Id == DevEUI.ToString()), + It.IsNotNull()), Times.Once()); + } + + [Fact] + + public async Task WhenBulkOperationFailed_AddDevice_Should_Return_False() + { + // Arrange + this.registryManager.Setup(c => c.AddDeviceWithTwinAsync(It.Is(d => d.Id == DevEUI.ToString()), It.IsNotNull())) + .ReturnsAsync(new BulkRegistryOperationResult + { + IsSuccessful = false + }); + + // Act + var args = CreateArgs($"add --type otaa --deveui {DevEUI} --appeui 8AFE71A145B253E49C3031AD068277A1 --appkey BE7A0000000014E2 --decoder MyDecoder --network myNetwork"); + var actual = await Program.Run(args, this.configurationHelper); + + // Assert + Assert.Equal(-1, actual); + } + + [Theory] + [InlineData("2", "1", "3")] + [InlineData("2", "1", "3", "fakeNetworkId")] + [InlineData("2", "1", "3", "fakeNetworkId", "ws://fakelns:5000")] + public async Task DeployEdgeDevice( + string resetPin, + string spiSpeed, + string spiDev, + string networkId = NetworkName, + string lnsHostAddress = "ws://mylns:5000") + { + // Arrange + const string deviceId = "myGateway"; + const string facadeURL = "https://myfunc.azurewebsites.com/api"; + const string facadeAuthCode = "secret-code"; + + ConfigurationContent? actualConfiguration = null; + this.registryManager.Setup(x => x.ApplyConfigurationContentOnDeviceAsync(deviceId, It.IsNotNull())) + .Callback((string deviceId, ConfigurationContent c) => actualConfiguration = c); + + this.registryManager.Setup(c => c.AddDeviceWithTwinAsync(It.Is(d => d.Id == deviceId && d.Capabilities.IotEdge), It.IsNotNull())) + .ReturnsAsync(new BulkRegistryOperationResult + { + IsSuccessful = true + }); + + var actualSpiSpeed = 2; + + // Act + var args = CreateArgs($"add-gateway --reset-pin {resetPin} --device-id {deviceId} --spi-dev {spiDev} --spi-speed {spiSpeed} --api-url {facadeURL} --api-key {facadeAuthCode} --lns-host-address {lnsHostAddress} --network {networkId} --lora-version {LoRaVersion}"); + var actual = await Program.Run(args, this.configurationHelper); + + // Assert + Assert.Equal(0, actual); + this.registryManager.Verify(x => x.ApplyConfigurationContentOnDeviceAsync(deviceId, It.IsNotNull()), Times.Once); + this.registryManager.Verify(c => c.AddDeviceWithTwinAsync(It.Is(d => d.Id == deviceId && d.Capabilities.IotEdge), It.IsNotNull()), Times.Once); + + // Should not deploy monitoring layer + this.registryManager.Verify(x => x.AddConfigurationAsync(It.IsNotNull()), Times.Never); + + var actualConfigurationJson = JsonConvert.SerializeObject(actualConfiguration); + var expectedConfigurationJson = $"{{\"modulesContent\":{{\"$edgeAgent\":{{\"properties.desired\":{{\"schemaVersion\":\"1.0\",\"runtime\":{{\"type\":\"docker\",\"settings\":{{\"loggingOptions\":\"\",\"minDockerVersion\":\"v1.25\"}}}},\"systemModules\":{{\"edgeAgent\":{{\"type\":\"docker\",\"settings\":{{\"image\":\"mcr.microsoft.com/azureiotedge-agent:{IotEdgeVersion}\",\"createOptions\":\"{{}}\"}}}},\"edgeHub\":{{\"type\":\"docker\",\"settings\":{{\"image\":\"mcr.microsoft.com/azureiotedge-hub:{IotEdgeVersion}\",\"createOptions\":\"{{ \\\"HostConfig\\\": {{ \\\"PortBindings\\\": {{\\\"8883/tcp\\\": [ {{\\\"HostPort\\\": \\\"8883\\\" }} ], \\\"443/tcp\\\": [ {{ \\\"HostPort\\\": \\\"443\\\" }} ], \\\"5671/tcp\\\": [ {{ \\\"HostPort\\\": \\\"5671\\\" }}] }} }}}}\"}},\"env\":{{\"OptimizeForPerformance\":{{\"value\":\"false\"}},\"mqttSettings__enabled\":{{\"value\":\"false\"}},\"AuthenticationMode\":{{\"value\":\"CloudAndScope\"}},\"NestedEdgeEnabled\":{{\"value\":\"false\"}}}},\"status\":\"running\",\"restartPolicy\":\"always\"}}}},\"modules\":{{\"LoRaWanNetworkSrvModule\":{{\"type\":\"docker\",\"settings\":{{\"image\":\"loraedge/lorawannetworksrvmodule:{LoRaVersion}\",\"createOptions\":\"{{\\\"ExposedPorts\\\": {{ \\\"5000/tcp\\\": {{}}}}, \\\"HostConfig\\\": {{ \\\"PortBindings\\\": {{\\\"5000/tcp\\\": [ {{ \\\"HostPort\\\": \\\"5000\\\", \\\"HostIp\\\":\\\"172.17.0.1\\\" }} ]}}}}}}\"}},\"version\":\"1.0\",\"env\":{{\"ENABLE_GATEWAY\":{{\"value\":\"true\"}},\"LOG_LEVEL\":{{\"value\":\"2\"}}}},\"status\":\"running\",\"restartPolicy\":\"always\"}},\"LoRaBasicsStationModule\":{{\"type\":\"docker\",\"settings\":{{\"image\":\"loraedge/lorabasicsstationmodule:{LoRaVersion}\",\"createOptions\":\" {{\\\"HostConfig\\\": {{\\\"NetworkMode\\\": \\\"host\\\", \\\"Privileged\\\": true }}, \\\"NetworkingConfig\\\": {{\\\"EndpointsConfig\\\": {{\\\"host\\\": {{}} }}}}}}\"}},\"env\":{{\"RESET_PIN\":{{\"value\":\"{resetPin}\"}},\"TC_URI\":{{\"value\":\"ws://172.17.0.1:5000\"}},\"SPI_DEV\":{{\"value\":\"{spiDev}\"}},\"SPI_SPEED\":{{\"value\":\"{actualSpiSpeed}\"}}}},\"version\":\"1.0\",\"status\":\"running\",\"restartPolicy\":\"always\"}}}}}}}},\"$edgeHub\":{{\"properties.desired\":{{\"schemaVersion\":\"1.0\",\"routes\":{{\"route\":\"FROM /* INTO $upstream\"}},\"storeAndForwardConfiguration\":{{\"timeToLiveSecs\":7200}}}}}},\"LoRaWanNetworkSrvModule\":{{\"properties.desired\":{{\"FacadeServerUrl\":\"{facadeURL}\",\"FacadeAuthCode\":\"{facadeAuthCode}\",\"hostAddress\":\"{lnsHostAddress}\",\"network\":\"{networkId}\"}}}}}},\"moduleContent\":{{}},\"deviceContent\":{{}}}}"; + Assert.Equal(expectedConfigurationJson, actualConfigurationJson); + } + + [Fact] + public async Task DeployEdgeDeviceWhenOmmitingSpiDevAndAndSpiSpeedSettingsAreNotSendToConfiguration() + { + // Arrange + const string deviceId = "myGateway"; + const string facadeURL = "https://myfunc.azurewebsites.com/api"; + const string facadeAuthCode = "secret-code"; + const string lnsHostAddress = "ws://mylns:5000"; + const int resetPin = 2; + + ConfigurationContent? actualConfiguration = null; + this.registryManager.Setup(x => x.ApplyConfigurationContentOnDeviceAsync(deviceId, It.IsNotNull())) + .Callback((string deviceId, ConfigurationContent c) => actualConfiguration = c); + + this.registryManager.Setup(c => c.AddDeviceWithTwinAsync(It.Is(d => d.Id == deviceId && d.Capabilities.IotEdge), It.IsNotNull())) + .ReturnsAsync(new BulkRegistryOperationResult + { + IsSuccessful = true + }); + + // Act + var args = CreateArgs($"add-gateway --reset-pin {resetPin} --device-id {deviceId} --api-url {facadeURL} --api-key {facadeAuthCode} --lns-host-address {lnsHostAddress} --network {NetworkName} --lora-version {LoRaVersion}"); + var actual = await Program.Run(args, this.configurationHelper); + + // Assert + Assert.Equal(0, actual); + this.registryManager.Verify(x => x.ApplyConfigurationContentOnDeviceAsync(deviceId, It.IsNotNull()), Times.Once); + this.registryManager.Verify(c => c.AddDeviceWithTwinAsync(It.Is(d => d.Id == deviceId && d.Capabilities.IotEdge), It.IsNotNull()), Times.Once); + + var actualConfigurationJson = JsonConvert.SerializeObject(actualConfiguration); + var expectedConfigurationJson = $"{{\"modulesContent\":{{\"$edgeAgent\":{{\"properties.desired\":{{\"schemaVersion\":\"1.0\",\"runtime\":{{\"type\":\"docker\",\"settings\":{{\"loggingOptions\":\"\",\"minDockerVersion\":\"v1.25\"}}}},\"systemModules\":{{\"edgeAgent\":{{\"type\":\"docker\",\"settings\":{{\"image\":\"mcr.microsoft.com/azureiotedge-agent:{IotEdgeVersion}\",\"createOptions\":\"{{}}\"}}}},\"edgeHub\":{{\"type\":\"docker\",\"settings\":{{\"image\":\"mcr.microsoft.com/azureiotedge-hub:{IotEdgeVersion}\",\"createOptions\":\"{{ \\\"HostConfig\\\": {{ \\\"PortBindings\\\": {{\\\"8883/tcp\\\": [ {{\\\"HostPort\\\": \\\"8883\\\" }} ], \\\"443/tcp\\\": [ {{ \\\"HostPort\\\": \\\"443\\\" }} ], \\\"5671/tcp\\\": [ {{ \\\"HostPort\\\": \\\"5671\\\" }}] }} }}}}\"}},\"env\":{{\"OptimizeForPerformance\":{{\"value\":\"false\"}},\"mqttSettings__enabled\":{{\"value\":\"false\"}},\"AuthenticationMode\":{{\"value\":\"CloudAndScope\"}},\"NestedEdgeEnabled\":{{\"value\":\"false\"}}}},\"status\":\"running\",\"restartPolicy\":\"always\"}}}},\"modules\":{{\"LoRaWanNetworkSrvModule\":{{\"type\":\"docker\",\"settings\":{{\"image\":\"loraedge/lorawannetworksrvmodule:{LoRaVersion}\",\"createOptions\":\"{{\\\"ExposedPorts\\\": {{ \\\"5000/tcp\\\": {{}}}}, \\\"HostConfig\\\": {{ \\\"PortBindings\\\": {{\\\"5000/tcp\\\": [ {{ \\\"HostPort\\\": \\\"5000\\\", \\\"HostIp\\\":\\\"172.17.0.1\\\" }} ]}}}}}}\"}},\"version\":\"1.0\",\"env\":{{\"ENABLE_GATEWAY\":{{\"value\":\"true\"}},\"LOG_LEVEL\":{{\"value\":\"2\"}}}},\"status\":\"running\",\"restartPolicy\":\"always\"}},\"LoRaBasicsStationModule\":{{\"type\":\"docker\",\"settings\":{{\"image\":\"loraedge/lorabasicsstationmodule:{LoRaVersion}\",\"createOptions\":\" {{\\\"HostConfig\\\": {{\\\"NetworkMode\\\": \\\"host\\\", \\\"Privileged\\\": true }}, \\\"NetworkingConfig\\\": {{\\\"EndpointsConfig\\\": {{\\\"host\\\": {{}} }}}}}}\"}},\"env\":{{\"RESET_PIN\":{{\"value\":\"{resetPin}\"}},\"TC_URI\":{{\"value\":\"ws://172.17.0.1:5000\"}}}},\"version\":\"1.0\",\"status\":\"running\",\"restartPolicy\":\"always\"}}}}}}}},\"$edgeHub\":{{\"properties.desired\":{{\"schemaVersion\":\"1.0\",\"routes\":{{\"route\":\"FROM /* INTO $upstream\"}},\"storeAndForwardConfiguration\":{{\"timeToLiveSecs\":7200}}}}}},\"LoRaWanNetworkSrvModule\":{{\"properties.desired\":{{\"FacadeServerUrl\":\"{facadeURL}\",\"FacadeAuthCode\":\"{facadeAuthCode}\",\"hostAddress\":\"{lnsHostAddress}\",\"network\":\"{NetworkName}\"}}}}}},\"moduleContent\":{{}},\"deviceContent\":{{}}}}"; + Assert.Equal(expectedConfigurationJson, actualConfigurationJson); + } + + [Fact] + public async Task DeployEdgeDeviceSettingLogAnalyticsWorkspaceShouldDeployIotHubMetricsCollectorModule() + { + // Arrange + const string logAnalyticsWorkspaceId = "fake-workspace-id"; + const string iothubResourceId = "fake-hub-id"; + const string logAnalyticsWorkspaceKey = "fake-workspace-key"; + const string deviceId = "myGateway"; + const string facadeURL = "https://myfunc.azurewebsites.com/api"; + const string facadeAuthCode = "secret-code"; + const string lnsHostAddress = "ws://mylns:5000"; + const int resetPin = 2; + + Configuration? actualConfiguration = null; + this.registryManager.Setup(x => x.AddConfigurationAsync(It.IsNotNull())) + .Callback((Configuration c) => actualConfiguration = c); + + this.registryManager.Setup(c => c.AddDeviceWithTwinAsync(It.Is(d => d.Id == deviceId && d.Capabilities.IotEdge), It.IsNotNull())) + .ReturnsAsync(new BulkRegistryOperationResult + { + IsSuccessful = true + }); + + // Act + var args = CreateArgs($"add-gateway --reset-pin {resetPin} --device-id {deviceId} --api-url {facadeURL} --api-key {facadeAuthCode} --lns-host-address {lnsHostAddress} --network {NetworkName} --monitoring true --iothub-resource-id {iothubResourceId} --log-analytics-workspace-id {logAnalyticsWorkspaceId} --log-analytics-shared-key {logAnalyticsWorkspaceKey} --lora-version {LoRaVersion}"); + var actual = await Program.Run(args, this.configurationHelper); + + // Assert + Assert.Equal(0, actual); + this.registryManager.Verify(x => x.AddConfigurationAsync(It.IsNotNull()), Times.Once); + this.registryManager.Verify(c => c.AddDeviceWithTwinAsync(It.Is(d => d.Id == deviceId && d.Capabilities.IotEdge), It.IsNotNull()), Times.Once); + + Assert.NotNull(actualConfiguration); + Assert.Equal($"deviceId='{deviceId}'", actualConfiguration!.TargetCondition); + var actualConfigurationJson = JsonConvert.SerializeObject(actualConfiguration.Content); + var expectedConfigurationJson = $"{{\"modulesContent\":{{\"$edgeAgent\":{{\"properties.desired.modules.IotHubMetricsCollectorModule\":{{\"settings\":{{\"image\":\"mcr.microsoft.com/azureiotedge-metrics-collector:1.0\"}},\"type\":\"docker\",\"env\":{{\"ResourceId\":{{\"value\":\"{iothubResourceId}\"}},\"UploadTarget\":{{\"value\":\"AzureMonitor\"}},\"LogAnalyticsWorkspaceId\":{{\"value\":\"{logAnalyticsWorkspaceId}\"}},\"LogAnalyticsSharedKey\":{{\"value\":\"{logAnalyticsWorkspaceKey}\"}},\"MetricsEndpointsCSV\":{{\"value\":\"http://edgeHub:9600/metrics,http://edgeAgent:9600/metrics\"}}}},\"status\":\"running\",\"restartPolicy\":\"always\",\"version\":\"1.0\"}}}}}},\"moduleContent\":{{}},\"deviceContent\":{{}}}}"; + Assert.Equal(expectedConfigurationJson, actualConfigurationJson); + } + + [Theory] + [InlineData("EU863")] + [InlineData("US902")] + [InlineData("EU863", "fakeNetwork")] + [InlineData("US902", "fakeNetwork")] + public async Task DeployConcentrator(string region, string networkId = NetworkName) + { + // Arrange + const string stationEui = "123456789"; + var eTag = Guid.NewGuid().ToString(); + Twin? emptyTwin = null; + Twin? savedTwin = null; + + this.registryManager.Setup(c => c.AddDeviceWithTwinAsync( + It.Is(d => d.Id == stationEui), + It.IsNotNull())) + .Callback((Device d, Twin t) => + { + Assert.Equal(networkId, t.Tags[DeviceTags.NetworkTagName].ToString()); + Assert.Equal(new string[] { DeviceTags.DeviceTypes.Concentrator }, ((JArray)t.Tags[DeviceTags.DeviceTypeTagName]).Select(x => x.ToString()).ToArray()); +#pragma warning disable CA1308 // Normalize strings to uppercase + Assert.Equal(region.ToLowerInvariant(), t.Tags[DeviceTags.RegionTagName].ToString()); +#pragma warning restore CA1308 // Normalize strings to uppercase + savedTwin = t; + }) + .ReturnsAsync(new BulkRegistryOperationResult + { + IsSuccessful = true + }); + + this.registryManager.SetupSequence(c => c.GetTwinAsync(It.Is(stationEui, StringComparer.OrdinalIgnoreCase))) + // First time it won't find it + .ReturnsAsync(emptyTwin) + // After it was inserted we will find it + .ReturnsAsync(new Twin(stationEui) + { + ETag = eTag + }); + + // Act + var args = CreateArgs($"add --type concentrator --region {region} --stationeui {stationEui} --no-cups --network {networkId}"); + var actual = await Program.Run(args, this.configurationHelper); + + // Assert + Assert.Equal(0, actual); + Assert.NotNull(savedTwin); + + // Create a JSON without whitespaces or newlines that is easy to compare + var expectedConf = GetConcentratorRouterConfig(region); + var expectedRouterConfig = JsonConvert.SerializeObject(expectedConf[TwinProperty.RouterConfig]); + + var actualRouterConfig = JsonUtil.Strictify(savedTwin!.Properties.Desired[TwinProperty.RouterConfig].ToString()); + Assert.Equal(expectedRouterConfig, actualRouterConfig); + } + + [Fact] + public async Task DeployConcentratorWithNotImplementedRegionShouldThrowSwitchExpressionException() + { + // Act + var args = CreateArgs($"add --type concentrator --region INVALID --stationeui 1111222 --no-cups --network {NetworkName}"); + var actual = await Program.Run(args, this.configurationHelper); + + // Assert + Assert.Equal(-1, actual); + } + } +} diff --git a/Tools/Cli-LoRa-Device-Provisioning/Tests/LoRaWan.Tools.CLI.Tests.Unit/JsonUtil.cs b/Tools/Cli-LoRa-Device-Provisioning/Tests/LoRaWan.Tools.CLI.Tests.Unit/JsonUtil.cs new file mode 100644 index 0000000000..947b159a5e --- /dev/null +++ b/Tools/Cli-LoRa-Device-Provisioning/Tests/LoRaWan.Tools.CLI.Tests.Unit/JsonUtil.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.Tools.CLI.Tests.Unit +{ + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + internal static class JsonUtil + { + /// + /// Takes somewhat non-conforming JSON + /// (as accepted by Json.NET) + /// text and re-formats it to be strictly conforming to RFC 7159. + /// + /// + /// This is a helper primarily designed to make it easier to express JSON as C# literals in + /// inline data for theory tests, where the double quotes don't have to be escaped. + /// + public static string Strictify(string json) => + JToken.Parse(json).ToString(Formatting.None); + } +} diff --git a/Tools/Cli-LoRa-Device-Provisioning/Tests/LoRaWan.Tools.CLI.Tests.Unit/LoRaWan.Tools.CLI.Tests.Unit.csproj b/Tools/Cli-LoRa-Device-Provisioning/Tests/LoRaWan.Tools.CLI.Tests.Unit/LoRaWan.Tools.CLI.Tests.Unit.csproj new file mode 100644 index 0000000000..4b6c2f7be6 --- /dev/null +++ b/Tools/Cli-LoRa-Device-Provisioning/Tests/LoRaWan.Tools.CLI.Tests.Unit/LoRaWan.Tools.CLI.Tests.Unit.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + LoRaWan.Tools.CLI.Tests.Unit + enable + enable + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Tools/Cli-LoRa-Device-Provisioning/Tests/LoRaWan.Tools.CLI.Tests.Unit/Usings.cs b/Tools/Cli-LoRa-Device-Provisioning/Tests/LoRaWan.Tools.CLI.Tests.Unit/Usings.cs new file mode 100644 index 0000000000..c41aab6c65 --- /dev/null +++ b/Tools/Cli-LoRa-Device-Provisioning/Tests/LoRaWan.Tools.CLI.Tests.Unit/Usings.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#pragma warning disable IDE0065 // Misplaced using directive: Global usings +global using Xunit; +#pragma warning restore IDE0065 // Misplaced using directive