Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Running LoRaWAN Network Server decoupled from IoT Edge #1746

Merged
merged 25 commits into from
Jul 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3c8345c
Adapt CI for temporary branch (#1696)
bastbu May 17, 2022
cac5e88
feat: split module connection host (#1695)
bastbu May 17, 2022
5a01d02
Adding EdgeDeviceGetter and unit tests (#1697)
danigian May 17, 2022
a3445b7
OCW2022 Decouple LoRaWAN Starter Kit from IoTEdge (#1698)
andrewDoing May 17, 2022
357a328
Add Redis Connection String to LNS Configuration and StackExchange.Re…
danigian May 17, 2022
818c004
Fix integration tests (#1701)
bastbu May 17, 2022
c16fcf4
Redis listener hosted service (#1702)
bastbu May 17, 2022
f3b6b85
OCW2022 Decouple LoRaWAN Starter Kit from IoTEdge - 1632 (#1700)
mkcomer May 17, 2022
a5d0828
Providing alternatives to IOTEDGE_* environment variables (#1704)
danigian May 17, 2022
1b352c9
Track unhandled exceptions in Redis listener (#1706)
bastbu May 18, 2022
f633835
Handling alternative paths for direct method invocations from Azure F…
danigian May 18, 2022
25fe964
Bug/1708 flaky lo ra device cache test test (#1709)
p-schuler May 18, 2022
e705fab
Bringing latest changes of dev branch in ocw-edge/dev (#1718)
danigian May 19, 2022
ff5916a
Adding Clear Cache endpoint to Facade function (#1716)
danigian May 19, 2022
6895ede
Merge branch 'dev' into ocw-edge/dev
danigian Jun 29, 2022
0e72e71
Reverting changes related to CI and ocw-edge/dev branch
danigian Jun 29, 2022
924769a
Bringing E2E tests to ocw-edge/dev branch (#1750)
danigian Jun 30, 2022
a5cdb9c
Merge branch 'dev' into ocw-edge/dev
danigian Jul 1, 2022
a601d32
Remove useless assignment
danigian Jul 1, 2022
fe8acc9
Remove useless assignment and rename
danigian Jul 1, 2022
9931c91
Fixing possible null reference
danigian Jul 1, 2022
bd674c5
ArgumentNullException in integration tests
danigian Jul 1, 2022
a0f4578
Get rid of useless assignment
danigian Jul 1, 2022
85fffa4
Dereferenced variable may be null
danigian Jul 2, 2022
5f4c6b7
Merge branch 'dev' into ocw-edge/dev
danigian Jul 4, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 129 additions & 6 deletions .github/workflows/e2e-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ on: # rebuild any PRs and main branch changes
description: 'Include Load Tests in current run'
default: 'false'
TestsToRun:
default: '[SensorDecodingTest,OTAAJoinTest,ABPTest,OTAATest,MacTest,ClassCTest,C2DMessageTest,MultiGatewayTest,MultiConcentratorTest,CupsTest,LnsDiscoveryTest]'
default: '[SensorDecodingTest,OTAAJoinTest,ABPTest,OTAATest,MacTest,ClassCTest,C2DMessageTest,MultiGatewayTest,MultiConcentratorTest,CupsTest,LnsDiscoveryTest,CloudDeploymentTest]'
description: 'tests to run'
TxPower:
description: 'TXPower value to use in E2E tests'
Expand Down Expand Up @@ -58,10 +58,10 @@ jobs:
echo "::set-output name=E2ETestsToRun::${{ github.event.inputs.TestsToRun }}"
elif [ ${{ github.event_name }} == 'pull_request' ]; then
echo "Set up for pull request"
echo "::set-output name=E2ETestsToRun::[SensorDecodingTest,OTAAJoinTest,ABPTest,OTAATest,MacTest,ClassCTest,C2DMessageTest,MultiGatewayTest,MultiConcentratorTest,CupsTest,LnsDiscoveryTest]"
echo "::set-output name=E2ETestsToRun::[SensorDecodingTest,OTAAJoinTest,ABPTest,OTAATest,MacTest,ClassCTest,C2DMessageTest,MultiGatewayTest,MultiConcentratorTest,CupsTest,LnsDiscoveryTest,CloudDeploymentTest]"
else
echo "Set up for cron trigger"
echo "::set-output name=E2ETestsToRun::[SensorDecodingTest,OTAAJoinTest,ABPTest,OTAATest,MacTest,ClassCTest,C2DMessageTest,MultiGatewayTest,MultiConcentratorTest,CupsTest,LnsDiscoveryTest]"
echo "::set-output name=E2ETestsToRun::[SensorDecodingTest,OTAAJoinTest,ABPTest,OTAATest,MacTest,ClassCTest,C2DMessageTest,MultiGatewayTest,MultiConcentratorTest,CupsTest,LnsDiscoveryTest,CloudDeploymentTest]"
fi

- id: check-if-run
Expand Down Expand Up @@ -170,7 +170,8 @@ jobs:
with:
fetch-depth: '2'

- run: |
- id: image_tag_definition
run: |
if [ ${{ github.ref }} = "refs/heads/dev" ]; then
echo "dev"
IMAGE_TAG="$DEV_IMAGE_TAG"
Expand All @@ -189,6 +190,7 @@ jobs:
echo "Using image tag $IMAGE_TAG"
echo "::set-env name=NET_SRV_VERSION::$IMAGE_TAG"
echo "::set-env name=LBS_VERSION::$IMAGE_TAG"
echo "::set-output name=imagetag::$IMAGE_TAG"
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true

Expand Down Expand Up @@ -216,6 +218,9 @@ jobs:
env:
CONTAINER_REGISTRY_ADDRESS: ${{ env.CONTAINER_REGISTRY_ADDRESS }}

outputs:
imagetag: ${{ steps.image_tag_definition.outputs.imagetag }}

# Generate root and server certificates and copy required files to RPi
certificates_job :
timeout-minutes: 5
Expand Down Expand Up @@ -287,6 +292,36 @@ jobs:
clientfwdigest: ${{ steps.generate_step.outputs.clientfwdigest }}
clientfwversion: ${{ steps.generate_step.outputs.clientfwversion }}

# Deploy Cloud based LoRaWAN Network Server
deploy_cloud_lns:
needs:
- env_var
- build_push_docker_images
- certificates_job
runs-on: ubuntu-latest
if: needs.env_var.outputs.RunE2ETestsOnly != 'true' && needs.env_var.outputs.StopFullCi != 'true'
name: Deploy Cloud based LNS
steps:
- name: "Deploy container instance"
id: "deploycloudlns"
shell: bash
run: |
az login --service-principal -u ${{ secrets.AZURE_SP_CLIENTID }} -p ${{ secrets.AZURE_SP_SECRET }} --tenant ${{ secrets.AZURE_TENANTID }}
az container create --resource-group ${{ secrets.AZURE_RG }} --ip-address Private --location westeurope --name cloudlns \
--environment-variables LOG_TO_TCP_ADDRESS=${{ needs.certificates_job.outputs.itestupip }} LOG_TO_TCP_PORT=6100 LOG_TO_TCP=true LOG_LEVEL=1 IOTHUBHOSTNAME=${{secrets.IOTHUB_HOSTNAME}} ENABLE_GATEWAY=false CLOUD_DEPLOYMENT=true \
--image ${{ env.CONTAINER_REGISTRY_ADDRESS }}/lorawannetworksrvmodule:${{needs.build_push_docker_images.outputs.imagetag}}-amd64 \
--ports 5000 \
--protocol TCP \
--registry-username ${{ env.CONTAINER_REGISTRY_USERNAME }} \
--registry-password ${{ env.CONTAINER_REGISTRY_PASSWORD }} \
--restart-policy Never \
--secure-environment-variables FACADE_AUTH_CODE=${{ secrets.FUNCTION_FACADE_AUTH_CODE }} FACADE_SERVER_URL=${{ secrets.FUNCTION_FACADE_SERVER_URL }} REDIS_CONNECTION_STRING=${{ secrets.REDIS_HOSTNAME }}.redis.cache.windows.net:6380,password=${{ secrets.REDIS_PASSWORD }},ssl=True,abortConnect=False \
--subnet ${{ secrets.AZURE_SUBNET_NAME }} \
--vnet ${{ secrets.AZURE_VNET_NAME }} --output none
echo "::set-output name=cloudlnsprivateip::$(az container show --name cloudlns --resource-group ${{ secrets.AZURE_RG }} --query ipAddress.ip -o tsv)"
outputs:
cloudlnsprivateip: ${{ steps.deploycloudlns.outputs.cloudlnsprivateip }}

# Deploy IoT Edge solution to ARM gateway
deploy_arm_gw_iot_edge:
timeout-minutes: 20
Expand Down Expand Up @@ -433,7 +468,9 @@ jobs:
INTEGRATIONTEST_LoadTestLnsEndpoints: ${{ secrets.LOAD_TEST_LNS_ENDPOINTS }}
INTEGRATIONTEST_NumberOfLoadTestDevices: 10
INTEGRATIONTEST_NumberOfLoadTestConcentrators: 4

INTEGRATIONTEST_FunctionAppCode: ${{ secrets.FUNCTION_FACADE_AUTH_CODE }}
INTEGRATIONTEST_FunctionAppBaseUrl: ${{ secrets.FUNCTION_FACADE_SERVER_URL }}
INTEGRATIONTEST_TcpLogPort: 6000
steps:
- uses: actions/checkout@v3
name: Checkout current branch
Expand All @@ -460,7 +497,7 @@ jobs:
shell: bash
run: |
dotnet test --logger trx --no-build --configuration ${{ env.BUILD_CONFIGURATION }} \
-r ${{ env.TESTS_RESULTS_FOLDER }}/LoadTest/ \
-r ${{ env.TESTS_RESULTS_FOLDER }}/LoadTest/ --filter "SimulatedLoadTests" \
${{ env.TESTS_FOLDER }}/Simulation/LoRaWan.Tests.Simulation.csproj

# Upload test results as artifact
Expand All @@ -470,6 +507,77 @@ jobs:
name: load-test-results
path: ${{ env.TESTS_RESULTS_FOLDER }}/LoadTest/

cloud_test_job:
timeout-minutes: 150
name: Run cloud only deployment Tests
environment:
name: CI_AZURE_ENVIRONMENT
url: ${{ needs.env_var.outputs.CheckSuiteUrl }}
if: always() && needs.deploy_cloud_lns.result == 'success' && needs.deploy_facade_function.result == 'success' && needs.env_var.outputs.StopFullCi != 'true' && contains(needs.env_var.outputs.E2ETestsToRun, 'CloudDeploymentTest') && !(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'CloudDeploymentTest'))
needs:
- deploy_facade_function
- deploy_cloud_lns
- env_var
- certificates_job
runs-on: [ self-hosted, x64 ]
env:
INTEGRATIONTEST_IoTHubEventHubConnectionString: ${{ secrets.IOTHUB_EVENT_HUB_CONNECTION_STRING }}
INTEGRATIONTEST_IoTHubEventHubConsumerGroup: ${{ secrets.IOTHUB_CI_CONSUMER_GROUP }}
INTEGRATIONTEST_IoTHubConnectionString: ${{ secrets.IOTHUB_OWNER_CONNECTION_STRING }}
INTEGRATIONTEST_LeafDeviceGatewayID: itestarm1
INTEGRATIONTEST_DevicePrefix: '12'
INTEGRATIONTEST_RunningInCI: true
INTEGRATIONTEST_LoadTestLnsEndpoints: "{ \\\"1\\\": \\\"ws://${{ needs.deploy_cloud_lns.outputs.cloudlnsprivateip }}:5000\\\" }"
INTEGRATIONTEST_NumberOfLoadTestDevices: 1
INTEGRATIONTEST_NumberOfLoadTestConcentrators: 2
INTEGRATIONTEST_FunctionAppCode: ${{ secrets.FUNCTION_FACADE_AUTH_CODE }}
INTEGRATIONTEST_FunctionAppBaseUrl: ${{ secrets.FUNCTION_FACADE_SERVER_URL }}
INTEGRATIONTEST_TcpLogPort: 6100

steps:
- uses: actions/checkout@v2
name: Checkout current branch

- name: Setup .NET
uses: actions/[email protected]
with:
dotnet-version: '6.0.x' # SDK Version to use.

- name: .NET SDK Information
run:
dotnet --info

- name: Configuration for simulated cloud tests
uses: cschleiden/replace-tokens@v1
with:
files: '${{ env.TESTS_FOLDER }}/Simulation/appsettings.json'

- name: Build simulated cloud tests
run: |
dotnet build --configuration ${{ env.BUILD_CONFIGURATION }} ${{ env.TESTS_FOLDER }}/Simulation/LoRaWan.Tests.Simulation.csproj

- name: Runs simulated cloud tests
shell: bash
run: |
dotnet test --logger trx --no-build --configuration ${{ env.BUILD_CONFIGURATION }} \
-r ${{ env.TESTS_RESULTS_FOLDER }}/LoadTest/ --filter "SimulatedCloudTests" \
${{ env.TESTS_FOLDER }}/Simulation/LoRaWan.Tests.Simulation.csproj

- name: Add CloudDeploymentTest Test Label
uses: buildsville/add-remove-label@v1
if: github.event_name == 'pull_request' && success()
with:
token: ${{ github.token }}
label: 'CloudDeploymentTest'
type: add

# Upload simulated cloud results as artifact
- uses: actions/upload-artifact@v1
if: always()
with:
name: simulated-cloud-test-results
path: ${{ env.TESTS_RESULTS_FOLDER }}/LoadTest/

# Runs E2E tests in dedicated agent, while having modules deployed into PI (arm32v7)
e2e_tests_job:
timeout-minutes: 150
Expand Down Expand Up @@ -647,3 +755,18 @@ jobs:
AZURE_SP_CLIENTID: ${{ secrets.AZURE_SP_CLIENTID }}
AZURE_SP_SECRET: ${{ secrets.AZURE_SP_SECRET }}
AZURE_TENANTID: ${{ secrets.AZURE_TENANTID }}

delete_cloud_lns:
name: Delete cloud LNS
if: always()
runs-on: ubuntu-latest
needs:
- e2e_tests_job
steps:
- uses: actions/checkout@v2
- name: "Delete azure container instance for cloudlns"
shell: bash
run: |
az login --service-principal -u ${{ secrets.AZURE_SP_CLIENTID }} -p ${{ secrets.AZURE_SP_SECRET }} --tenant ${{ secrets.AZURE_TENANTID }}
az container logs -g ${{ secrets.AZURE_RG }} --name cloudlns
az container delete --yes --resource-group ${{ secrets.AZURE_RG }} --name cloudlns --output none
1 change: 1 addition & 0 deletions AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
[assembly: InternalsVisibleTo("LoRaWan.Tests.Integration")]
[assembly: InternalsVisibleTo("LoRaWan.Tests.E2E")]
[assembly: InternalsVisibleTo("LoRaWan.Tests.Common")]
[assembly: InternalsVisibleTo("LoRaWan.Tests.Simulation")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
29 changes: 29 additions & 0 deletions LoRaEngine/LoraKeysManagerFacade/ RedisChannelPublisher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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 StackExchange.Redis;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using LoRaTools;
using System.Text.Json;

public class RedisChannelPublisher : IChannelPublisher
{
private readonly ConnectionMultiplexer redis;
private readonly ILogger logger;

public RedisChannelPublisher(ConnectionMultiplexer redis, ILogger<RedisChannelPublisher> logger)
{
this.redis = redis;
this.logger = logger;
}

public async Task PublishAsync(string channel, LnsRemoteCall lnsRemoteCall)
{
this.logger.LogDebug("Publishing message to channel '{Channel}'.", channel);
_ = await this.redis.GetSubscriber().PublishAsync(channel, JsonSerializer.Serialize(lnsRemoteCall));
}
}
}
96 changes: 96 additions & 0 deletions LoRaEngine/LoraKeysManagerFacade/ClearLnsCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#nullable enable

namespace LoraKeysManagerFacade
{
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using LoRaTools;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Devices;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;

public sealed class ClearLnsCache
{
private readonly IEdgeDeviceGetter edgeDeviceGetter;
private readonly IServiceClient serviceClient;
private readonly IChannelPublisher channelPublisher;
private readonly ILogger<ClearLnsCache> logger;

public ClearLnsCache(IEdgeDeviceGetter edgeDeviceGetter,
IServiceClient serviceClient,
IChannelPublisher channelPublisher,
ILogger<ClearLnsCache> logger)
{
this.edgeDeviceGetter = edgeDeviceGetter;
this.serviceClient = serviceClient;
this.channelPublisher = channelPublisher;
this.logger = logger;
}

[FunctionName(nameof(ClearNetworkServerCache))]
public async Task<IActionResult> ClearNetworkServerCache([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, CancellationToken cancellationToken)
{
if (req is null) throw new ArgumentNullException(nameof(req));

try
{
VersionValidator.Validate(req);
}
catch (IncompatibleVersionException ex)
{
return new BadRequestObjectResult(ex.Message);
}

await ClearLnsCacheInternalAsync(cancellationToken);

return new AcceptedResult();
}

internal async Task ClearLnsCacheInternalAsync(CancellationToken cancellationToken)
{
this.logger.LogInformation("Clearing device cache for all edge and Pub/Sub channel based Network Servers.");
// Edge device discovery for invoking direct methods
var edgeDevices = await this.edgeDeviceGetter.ListEdgeDevicesAsync(cancellationToken);
if (this.logger.IsEnabled(LogLevel.Debug))
{
this.logger.LogDebug("Invoking clear cache direct method for following devices: {deviceList}", string.Join(',', edgeDevices));
}
var tasks = edgeDevices.Select(e => InvokeClearViaDirectMethodAsync(e, cancellationToken)).ToArray();
// Publishing a single message for all cloud based LNSes
await PublishClearMessageAsync();
await Task.WhenAll(tasks);
}

internal async Task PublishClearMessageAsync()
{
await this.channelPublisher.PublishAsync(LoraKeysManagerFacadeConstants.ClearCacheMethodName, new LnsRemoteCall(RemoteCallKind.ClearCache, null));
this.logger.LogInformation("Cache clear message published on Pub/Sub channel");
}

internal async Task InvokeClearViaDirectMethodAsync(string lnsId, CancellationToken cancellationToken)
{
//Reason why the yield is needed is to avoid any potential "synchronous" code that might fail the publishing of a message on the pub/sub channel
await Task.Yield();
var res = await this.serviceClient.InvokeDeviceMethodAsync(lnsId,
LoraKeysManagerFacadeConstants.NetworkServerModuleId,
new CloudToDeviceMethod(LoraKeysManagerFacadeConstants.ClearCacheMethodName),
cancellationToken);
if (HttpUtilities.IsSuccessStatusCode(res.Status))
{
this.logger.LogInformation("Cache cleared for {gatewayID} via direct method", lnsId);
}
else
{
throw new InvalidOperationException($"Direct method call to {lnsId} failed with {res.Status}. Response: {res.GetPayloadAsJson()}");
}
}
}
}
Loading