Remove create edge function (#1937)
* Remove create edge function Co-authored-by: Mikhail Chatillon <chmikhai@microsoft.com>
This commit is contained in:
Родитель
f44e93d92a
Коммит
1d1202c3df
|
@ -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
|
||||
|
|
|
@ -12,4 +12,5 @@ using System.Runtime.CompilerServices;
|
|||
[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")]
|
||||
|
|
|
@ -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<HttpResponseMessage> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -62,7 +62,6 @@ namespace LoraKeysManagerFacade
|
|||
deviceCacheStore,
|
||||
sp.GetRequiredService<ILoggerFactory>(),
|
||||
sp.GetRequiredService<ILogger<LoRaADRServerManager>>()))
|
||||
.AddSingleton<CreateEdgeDevice>()
|
||||
.AddSingleton<IChannelPublisher>(sp => new RedisChannelPublisher(redis, sp.GetRequiredService<ILogger<RedisChannelPublisher>>()))
|
||||
.AddSingleton<DeviceGetter>()
|
||||
.AddSingleton<IEdgeDeviceGetter, EdgeDeviceGetter>()
|
||||
|
|
|
@ -23,16 +23,5 @@ namespace LoRaTools
|
|||
IRegistryPageResult<ILoRaDeviceTwin> FindDeviceByDevEUI(DevEui devEUI);
|
||||
Task<IDeviceTwin> 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<bool> DeployEndDevicesAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,133 +113,5 @@ namespace LoRaTools.IoTHubImpl
|
|||
|
||||
public async Task<IDeviceTwin> 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<ConfigurationContent> GetConfigurationContentAsync(Uri configLocation, IDictionary<string, string> 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<ConfigurationContent>(json);
|
||||
}
|
||||
|
||||
var deviceConfigurationContent = await GetConfigurationContentAsync(new Uri(Environment.GetEnvironmentVariable("DEVICE_CONFIG_LOCATION")), new Dictionary<string, string>
|
||||
{
|
||||
["[$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<string, string>
|
||||
{
|
||||
["[$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<JObject>(regionalConfiguration);
|
||||
concentratorTwin.Tags[Constants.NetworkTagName] = networkId;
|
||||
_ = await this.instance.UpdateTwinAsync(stationEuiString, concentratorTwin, concentratorTwin.ETag);
|
||||
}
|
||||
|
||||
public async Task<bool> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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="<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="<iotedge-device-name>" STATION_DEVICE_NAME="<concentrator-device-name>" DEPLOY_DEVICE=1 RESET_PIN=<concentrator-reset-pin> ./create_device.sh
|
||||
FACADE_SERVER_URL="https://myapp.com/api" IOTHUB_CONNECTION_STRING="<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="<iotedge-device-name>" STATION_DEVICE_NAME="<concentrator-device-name>" DEPLOY_DEVICE=1 RESET_PIN=<concentrator-reset-pin> LORA_VERSION="<lora-starter-kit-release-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 <resource-group-name> --template-file ./devices.bicep --parameters iothubName="<unique-name>"
|
||||
resetPin=<based-on-your-setup> edgeGatewayName="<gateway-device-name>" spiSpeed=<based-on-your-setup> spiDev=<based-on-your-setup> functionAppName="<function-name>" region="<lora-region>" stationEui="<concentrator-device-name>" logAnalyticsName="<log-analytics-name>"
|
||||
loraCliUrl="<lora-cli-linux-musl-version-download-url>" deployDevice=true
|
||||
loraCliUrl="<lora-cli-linux-musl-version-download-url>" version="<lora-starter-kit-release-version>" deployDevice=true
|
||||
```
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\Tools\Cli-LoRa-Device-Provisioning\DefaultRouterConfig\EU863.json" Link="EU863.json">
|
||||
<Content Include="..\..\Tools\Cli-LoRa-Device-Provisioning\LoRaWan.Tools.CLI\DefaultRouterConfig\EU863.json" Link="EU863.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
|
|
@ -371,411 +371,6 @@ namespace LoRaWan.Tests.Unit.IoTHubImpl
|
|||
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<Device>(d => d.Id == devEUI.ToString()), It.Is<Twin>(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<Device>(d => d.Id == devEUI.ToString()), It.Is<Twin>(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<Device>()), Times.Once);
|
||||
this.mockRegistryManager.Verify(c => c.AddModuleAsync(It.IsAny<Module>()), Times.Once);
|
||||
this.mockRegistryManager.Verify(c => c.ApplyConfigurationContentOnDeviceAsync(It.Is<string>(deviceId, StringComparer.OrdinalIgnoreCase), It.IsAny<ConfigurationContent>()), Times.Once);
|
||||
this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.Is<string>(deviceId, StringComparer.OrdinalIgnoreCase)), Times.Once);
|
||||
this.mockRegistryManager.Verify(c => c.UpdateTwinAsync(
|
||||
It.Is(deviceId, StringComparer.OrdinalIgnoreCase),
|
||||
It.Is("LoRaWanNetworkSrvModule", StringComparer.OrdinalIgnoreCase),
|
||||
It.IsAny<Twin>(),
|
||||
It.IsAny<string>()), 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<Device>()), Times.Once);
|
||||
this.mockRegistryManager.Verify(c => c.AddModuleAsync(It.IsAny<Module>()), Times.Once);
|
||||
this.mockRegistryManager.Verify(c => c.ApplyConfigurationContentOnDeviceAsync(It.Is<string>(deviceId, StringComparer.OrdinalIgnoreCase), It.IsAny<ConfigurationContent>()), Times.Once);
|
||||
this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.Is<string>(deviceId, StringComparer.OrdinalIgnoreCase)), Times.Once);
|
||||
this.mockRegistryManager.Verify(c => c.UpdateTwinAsync(
|
||||
It.Is(deviceId, StringComparer.OrdinalIgnoreCase),
|
||||
It.Is("LoRaWanNetworkSrvModule", StringComparer.OrdinalIgnoreCase),
|
||||
It.IsAny<Twin>(),
|
||||
It.IsAny<string>()), 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<Module>(m => m.DeviceId == deviceId && m.Id == "IotHubMetricsCollectorModule")))
|
||||
.ReturnsAsync((Module m) => m);
|
||||
|
||||
this.mockRegistryManager.Setup(c => c.AddConfigurationAsync(It.Is<Configuration>(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<Device>()), Times.Once);
|
||||
this.mockRegistryManager.Verify(c => c.AddModuleAsync(It.IsAny<Module>()), Times.Exactly(2));
|
||||
this.mockRegistryManager.Verify(c => c.AddModuleAsync(It.Is<Module>(m => m.DeviceId == deviceId && m.Id == "IotHubMetricsCollectorModule")), Times.Once);
|
||||
this.mockRegistryManager.Verify(c => c.AddConfigurationAsync(It.Is<Configuration>(conf => conf.TargetCondition == $"deviceId='{deviceId}'")), Times.Once);
|
||||
|
||||
this.mockRegistryManager.Verify(c => c.ApplyConfigurationContentOnDeviceAsync(It.Is<string>(deviceId, StringComparer.OrdinalIgnoreCase), It.IsAny<ConfigurationContent>()), Times.Once);
|
||||
this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.Is<string>(deviceId, StringComparer.OrdinalIgnoreCase)), Times.Once);
|
||||
this.mockRegistryManager.Verify(c => c.UpdateTwinAsync(
|
||||
It.Is(deviceId, StringComparer.OrdinalIgnoreCase),
|
||||
It.Is("LoRaWanNetworkSrvModule", StringComparer.OrdinalIgnoreCase),
|
||||
It.IsAny<Twin>(),
|
||||
It.IsAny<string>()), 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<string>()))
|
||||
.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<Device>(d => d.Id == stationEui)))
|
||||
.ReturnsAsync((Device d) => d);
|
||||
|
||||
_ = this.mockRegistryManager.Setup(c => c.GetTwinAsync(It.Is<string>(stationEui, StringComparer.OrdinalIgnoreCase)))
|
||||
.ReturnsAsync(new Twin(stationEui)
|
||||
{
|
||||
ETag = eTag
|
||||
});
|
||||
|
||||
_ = this.mockRegistryManager.Setup(c => c.UpdateTwinAsync(
|
||||
It.Is(stationEui, StringComparer.OrdinalIgnoreCase),
|
||||
It.IsAny<Twin>(),
|
||||
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<Device>()), Times.Once);
|
||||
this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.Is<string>(stationEui, StringComparer.OrdinalIgnoreCase)), Times.Once);
|
||||
this.mockRegistryManager.Verify(c => c.UpdateTwinAsync(
|
||||
It.Is(stationEui, StringComparer.OrdinalIgnoreCase),
|
||||
It.IsAny<Twin>(),
|
||||
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<string>()))
|
||||
.Returns(() => mockHttpClientHandler.ToHttpClient());
|
||||
|
||||
// Act
|
||||
_ = await Assert.ThrowsAsync<SwitchExpressionException>(() => manager.DeployConcentratorAsync("123456789", "FAKE"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeployEndDevicesShouldCreateEndDevices()
|
||||
{
|
||||
// Arrange
|
||||
using var manager = CreateManager();
|
||||
|
||||
Dictionary<string, Twin> deviceTwins = new();
|
||||
var eTag = $"{DateTime.Now.Ticks}";
|
||||
|
||||
this.mockRegistryManager.Setup(c => c.GetDeviceAsync(It.IsAny<string>()))
|
||||
.ReturnsAsync((string _) => null);
|
||||
|
||||
this.mockRegistryManager.Setup(c => c.AddDeviceAsync(It.IsAny<Device>()))
|
||||
.ReturnsAsync((Device d) => d);
|
||||
|
||||
this.mockRegistryManager.Setup(c => c.GetTwinAsync(It.IsAny<string>()))
|
||||
.ReturnsAsync((string id) => new Twin(id)
|
||||
{
|
||||
ETag = eTag
|
||||
});
|
||||
|
||||
this.mockRegistryManager.Setup(c => c.UpdateTwinAsync(It.IsAny<string>(), It.IsAny<Twin>(), 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<string>()), Times.Exactly(2));
|
||||
this.mockRegistryManager.Verify(c => c.AddDeviceAsync(It.IsAny<Device>()), Times.Exactly(2));
|
||||
this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.IsAny<string>()), Times.Exactly(2));
|
||||
this.mockRegistryManager.Verify(c => c.UpdateTwinAsync(It.IsAny<string>(), It.IsAny<Twin>(), It.IsAny<string>()), Times.Exactly(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeployEndDevicesShouldBeIdempotent()
|
||||
{
|
||||
// Arrange
|
||||
using var manager = CreateManager();
|
||||
|
||||
Dictionary<string, Twin> deviceTwins = new();
|
||||
var eTag = $"{DateTime.Now.Ticks}";
|
||||
|
||||
this.mockRegistryManager.Setup(c => c.GetDeviceAsync(It.IsAny<string>()))
|
||||
.ReturnsAsync((string id) => new Device(id));
|
||||
|
||||
this.mockRegistryManager.Setup(c => c.GetTwinAsync(It.IsAny<string>()))
|
||||
.ReturnsAsync((string id) => new Twin(id)
|
||||
{
|
||||
ETag = eTag
|
||||
});
|
||||
|
||||
this.mockRegistryManager.Setup(c => c.UpdateTwinAsync(It.IsAny<string>(), It.IsAny<Twin>(), 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<string>()), Times.Exactly(2));
|
||||
this.mockRegistryManager.Verify(c => c.GetTwinAsync(It.IsAny<string>()), Times.Exactly(2));
|
||||
this.mockRegistryManager.Verify(c => c.UpdateTwinAsync(It.IsAny<string>(), It.IsAny<Twin>(), It.IsAny<string>()), Times.Exactly(2));
|
||||
}
|
||||
|
||||
private string SetupForEdgeDeployment(string publishingUserName, string publishingPassword,
|
||||
Action<string, ConfigurationContent> onApplyConfigurationContentOnDevice,
|
||||
Func<string, string, Twin, string, Twin> onUpdateLoRaWanNetworkServerModuleTwin)
|
||||
{
|
||||
this.mockHttpClientFactory.Setup(c => c.CreateClient(It.IsAny<string>()))
|
||||
.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<Device>(d => d.Id == deviceId && d.Capabilities.IotEdge)))
|
||||
.ReturnsAsync((Device d) => d);
|
||||
|
||||
this.mockRegistryManager.Setup(c => c.AddModuleAsync(It.Is<Module>(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<string>(deviceId, StringComparer.OrdinalIgnoreCase), It.IsAny<ConfigurationContent>()))
|
||||
.Callback(onApplyConfigurationContentOnDevice)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
_ = this.mockRegistryManager.Setup(c => c.GetTwinAsync(It.Is<string>(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<Twin>(),
|
||||
It.Is(eTag, StringComparer.OrdinalIgnoreCase)))
|
||||
.ReturnsAsync(onUpdateLoRaWanNetworkServerModuleTwin);
|
||||
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
protected virtual ValueTask DisposeAsync(bool disposing)
|
||||
{
|
||||
if (!this.disposedValue)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -113,5 +113,12 @@ namespace LoRaWan.Tools.CLI.Options
|
|||
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; }
|
||||
}
|
||||
}
|
|
@ -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 @@ namespace LoRaWan.Tools.CLI.Helpers
|
|||
|
||||
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;
|
||||
}
|
|
@ -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 @@ namespace LoRaWan.Tools.CLI
|
|||
{
|
||||
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<ListOptions, QueryOptions, VerifyOptions, BulkVerifyOptions, AddOptions, AddGatewayOption, UpdateOptions, RemoveOptions, RotateCertificateOptions, RevokeOptions, UpgradeFirmwareOptions>(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 @@ namespace LoRaWan.Tools.CLI
|
|||
WriteToConsole($"Terminated with error: {ex}.", ConsoleColor.Red);
|
||||
return (int)ExitCode.Error;
|
||||
}
|
||||
}
|
||||
|
||||
static void WriteToConsole(string message, ConsoleColor color)
|
||||
internal static async Task<int> 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<ListOptions, QueryOptions, VerifyOptions, BulkVerifyOptions, AddOptions, AddGatewayOption, UpdateOptions, RemoveOptions, RotateCertificateOptions, RevokeOptions, UpgradeFirmwareOptions>(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 async Task<bool> RunListAndReturnExitCode(ListOptions opts)
|
||||
private static void WriteToConsole(string message, ConsoleColor color)
|
||||
{
|
||||
Console.ForegroundColor = color;
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(message);
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
private static async Task<bool> RunListAndReturnExitCode(ConfigurationHelper configurationHelper, ListOptions opts)
|
||||
{
|
||||
if (!int.TryParse(opts.Page, out var page))
|
||||
page = 10;
|
||||
|
@ -104,14 +109,14 @@ namespace LoRaWan.Tools.CLI
|
|||
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<bool> RunQueryAndReturnExitCode(QueryOptions opts)
|
||||
private static async Task<bool> 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 @@ namespace LoRaWan.Tools.CLI
|
|||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> RunVerifyAndReturnExitCode(VerifyOptions opts)
|
||||
private static async Task<bool> 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 @@ namespace LoRaWan.Tools.CLI
|
|||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> RunBulkVerifyAndReturnExitCode(BulkVerifyOptions opts)
|
||||
private static async Task<bool> 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 @@ namespace LoRaWan.Tools.CLI
|
|||
return isSuccess;
|
||||
}
|
||||
|
||||
private static async Task<bool> RunAddAndReturnExitCode(AddOptions opts)
|
||||
private static async Task<bool> 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 @@ namespace LoRaWan.Tools.CLI
|
|||
|
||||
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<bool> RunAddGatewayAndReturnExitCode(AddGatewayOption opts)
|
||||
private static async Task<bool> RunAddGatewayAndReturnExitCode(ConfigurationHelper configurationHelper, AddGatewayOption opts)
|
||||
{
|
||||
if (true == opts.MonitoringEnabled)
|
||||
{
|
||||
|
@ -216,7 +221,7 @@ namespace LoRaWan.Tools.CLI
|
|||
}
|
||||
|
||||
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 @@ namespace LoRaWan.Tools.CLI
|
|||
}
|
||||
|
||||
var deviceConfigurationContent = await GetEdgeGatewayDeployment(opts);
|
||||
return await IoTDeviceHelper.CreateGatewayTwin(opts, deviceConfigurationContent, ConfigurationHelper);
|
||||
return await IoTDeviceHelper.CreateGatewayTwin(opts, deviceConfigurationContent, configurationHelper);
|
||||
}
|
||||
|
||||
private static async Task<ConfigurationContent> GetEdgeObservabilityDeployment(AddGatewayOption opts)
|
||||
|
@ -251,13 +256,14 @@ namespace LoRaWan.Tools.CLI
|
|||
var tokenReplacements = new Dictionary<string, string>
|
||||
{
|
||||
{ "[$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 @@ namespace LoRaWan.Tools.CLI
|
|||
return JsonConvert.DeserializeObject<ConfigurationContent>(manifest);
|
||||
}
|
||||
|
||||
private static async Task<bool> CreateConcentratorDevice(AddOptions opts)
|
||||
private static async Task<bool> 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 @@ namespace LoRaWan.Tools.CLI
|
|||
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<bool> RunRotateCertificateAndReturnExitCodeAsync(RotateCertificateOptions opts)
|
||||
private static async Task<bool> RunRotateCertificateAndReturnExitCodeAsync(ConfigurationHelper configurationHelper, RotateCertificateOptions opts)
|
||||
{
|
||||
if (!File.Exists(opts.CertificateBundleLocation))
|
||||
{
|
||||
|
@ -307,7 +313,7 @@ namespace LoRaWan.Tools.CLI
|
|||
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 @@ namespace LoRaWan.Tools.CLI
|
|||
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 @@ namespace LoRaWan.Tools.CLI
|
|||
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<bool> RunRevokeAndReturnExitCodeAsync(RevokeOptions opts)
|
||||
private static async Task<bool> 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 @@ namespace LoRaWan.Tools.CLI
|
|||
|
||||
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<bool> RunUpdateAndReturnExitCode(UpdateOptions opts)
|
||||
private static async Task<bool> 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 @@ namespace LoRaWan.Tools.CLI
|
|||
return isSuccess;
|
||||
}
|
||||
|
||||
private static async Task<bool> UploadCertificateBundleAsync(string certificateBundleLocation, string stationEui, Func<uint, Uri, Task<bool>> uploadSuccessActionAsync)
|
||||
private static async Task<bool> UploadCertificateBundleAsync(ConfigurationHelper configurationHelper, string certificateBundleLocation, string stationEui, Func<uint, Uri, Task<bool>> 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 @@ namespace LoRaWan.Tools.CLI
|
|||
Task CleanupAsync() => blobClient.DeleteIfExistsAsync();
|
||||
}
|
||||
|
||||
private static async Task<bool> RunUpgradeFirmwareAndReturnExitCodeAsync(UpgradeFirmwareOptions opts)
|
||||
private static async Task<bool> RunUpgradeFirmwareAndReturnExitCodeAsync(ConfigurationHelper configurationHelper, UpgradeFirmwareOptions opts)
|
||||
{
|
||||
if (!File.Exists(opts.FirmwareLocation))
|
||||
{
|
||||
|
@ -468,9 +474,9 @@ namespace LoRaWan.Tools.CLI
|
|||
}
|
||||
|
||||
// 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 @@ namespace LoRaWan.Tools.CLI
|
|||
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<bool> UploadFirmwareAsync(string firmwareLocation, string stationEui, string package, Func<Uri, Task<bool>> uploadSuccessActionAsync)
|
||||
private static async Task<bool> UploadFirmwareAsync(ConfigurationHelper configurationHelper, string firmwareLocation, string stationEui, string package, Func<Uri, Task<bool>> 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 @@ namespace LoRaWan.Tools.CLI
|
|||
Task CleanupAsync() => blobClient.DeleteIfExistsAsync();
|
||||
}
|
||||
|
||||
private static async Task<bool> RunRemoveAndReturnExitCode(RemoveOptions opts)
|
||||
private static async Task<bool> RunRemoveAndReturnExitCode(ConfigurationHelper configurationHelper, RemoveOptions opts)
|
||||
{
|
||||
return await IoTDeviceHelper.RemoveDevice(opts.DevEui, ConfigurationHelper);
|
||||
return await IoTDeviceHelper.RemoveDevice(opts.DevEui, configurationHelper);
|
||||
}
|
||||
|
||||
private static void WriteAzureLogo()
|
|
@ -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": {
|
|
@ -1,3 +1,8 @@
|
|||
# Device Provisioning
|
||||
|
||||
This file has been moved to: <https://github.com/Azure/iotedge-lorawan-starterkit/blob/docs/main/docs/tools/device-provisioning.md>
|
||||
Main documentation is found here: <https://github.com/Azure/iotedge-lorawan-starterkit/blob/docs/main/docs/tools/device-provisioning.md>.
|
||||
|
||||
## 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).
|
||||
|
|
|
@ -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
|
|
@ -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> 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<RegistryManager>();
|
||||
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<string, object> 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<Dictionary<string, object>>(raw)!;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddABPDevice()
|
||||
{
|
||||
// Arrange
|
||||
var savedTwin = new Twin();
|
||||
|
||||
this.registryManager.Setup(c => c.AddDeviceWithTwinAsync(
|
||||
It.Is<Device>(d => d.Id == DevEUI.ToString()),
|
||||
It.IsNotNull<Twin>()))
|
||||
.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<Device>(d => d.Id == DevEUI.ToString()),
|
||||
It.IsNotNull<Twin>()), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddOTAADevice()
|
||||
{
|
||||
// Arrange
|
||||
var savedTwin = new Twin();
|
||||
|
||||
this.registryManager.Setup(c => c.AddDeviceWithTwinAsync(
|
||||
It.Is<Device>(d => d.Id == DevEUI.ToString()),
|
||||
It.IsNotNull<Twin>()))
|
||||
.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<Device>(d => d.Id == DevEUI.ToString()),
|
||||
It.IsNotNull<Twin>()), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
public async Task WhenBulkOperationFailed_AddDevice_Should_Return_False()
|
||||
{
|
||||
// Arrange
|
||||
this.registryManager.Setup(c => c.AddDeviceWithTwinAsync(It.Is<Device>(d => d.Id == DevEUI.ToString()), It.IsNotNull<Twin>()))
|
||||
.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<ConfigurationContent>()))
|
||||
.Callback((string deviceId, ConfigurationContent c) => actualConfiguration = c);
|
||||
|
||||
this.registryManager.Setup(c => c.AddDeviceWithTwinAsync(It.Is<Device>(d => d.Id == deviceId && d.Capabilities.IotEdge), It.IsNotNull<Twin>()))
|
||||
.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<ConfigurationContent>()), Times.Once);
|
||||
this.registryManager.Verify(c => c.AddDeviceWithTwinAsync(It.Is<Device>(d => d.Id == deviceId && d.Capabilities.IotEdge), It.IsNotNull<Twin>()), Times.Once);
|
||||
|
||||
// Should not deploy monitoring layer
|
||||
this.registryManager.Verify(x => x.AddConfigurationAsync(It.IsNotNull<Configuration>()), 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<ConfigurationContent>()))
|
||||
.Callback((string deviceId, ConfigurationContent c) => actualConfiguration = c);
|
||||
|
||||
this.registryManager.Setup(c => c.AddDeviceWithTwinAsync(It.Is<Device>(d => d.Id == deviceId && d.Capabilities.IotEdge), It.IsNotNull<Twin>()))
|
||||
.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<ConfigurationContent>()), Times.Once);
|
||||
this.registryManager.Verify(c => c.AddDeviceWithTwinAsync(It.Is<Device>(d => d.Id == deviceId && d.Capabilities.IotEdge), It.IsNotNull<Twin>()), 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<Configuration>()))
|
||||
.Callback((Configuration c) => actualConfiguration = c);
|
||||
|
||||
this.registryManager.Setup(c => c.AddDeviceWithTwinAsync(It.Is<Device>(d => d.Id == deviceId && d.Capabilities.IotEdge), It.IsNotNull<Twin>()))
|
||||
.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<Configuration>()), Times.Once);
|
||||
this.registryManager.Verify(c => c.AddDeviceWithTwinAsync(It.Is<Device>(d => d.Id == deviceId && d.Capabilities.IotEdge), It.IsNotNull<Twin>()), 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<Device>(d => d.Id == stationEui),
|
||||
It.IsNotNull<Twin>()))
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Takes somewhat non-conforming JSON
|
||||
/// (<a href="https://github.com/JamesNK/Newtonsoft.Json/issues/646#issuecomment-356194475">as accepted by Json.NET</a>)
|
||||
/// text and re-formats it to be strictly conforming to RFC 7159.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public static string Strictify(string json) =>
|
||||
JToken.Parse(json).ToString(Formatting.None);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RootNamespace>LoRaWan.Tools.CLI.Tests.Unit</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.2" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\LoRaWan.Tools.CLI\LoRaWan.Tools.CLI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -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
|
Загрузка…
Ссылка в новой задаче