From 1ca5812617a7bf5d711aaced22bd9ba524baad78 Mon Sep 17 00:00:00 2001 From: s-themis <50443372+s-themis@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:09:28 +0200 Subject: [PATCH 1/3] Snap with dotnet plugin (#392) * chore: Add snapcraft.yaml and run script * fix: override appsettings.json location to $SNAP * fix: override uanodes file locations to $SNAP * chore: Add snap hooks and update run script --------- Co-authored-by: Mateus Rodrigues de Morais --- scripts/run | 16 +++++++++ snap/hooks/configure | 15 ++++++++ snap/hooks/default-configure | 3 ++ snap/snapcraft.yaml | 36 +++++++++++++++++++ src/CompanionSpecs/DI/DiNodeManager.cs | 11 +++++- src/OpcPlcServer.cs | 10 +++++- src/PluginNodes/Boiler2PluginNodes.cs | 13 +++++-- .../ComplexTypeBoilerPluginNode.cs | 21 +++++++---- src/SimpleEvent/SimpleEventsNodeManager.cs | 11 +++++- 9 files changed, 125 insertions(+), 11 deletions(-) create mode 100755 scripts/run create mode 100644 snap/hooks/configure create mode 100644 snap/hooks/default-configure create mode 100644 snap/snapcraft.yaml diff --git a/scripts/run b/scripts/run new file mode 100755 index 00000000..15f4d771 --- /dev/null +++ b/scripts/run @@ -0,0 +1,16 @@ +#!/bin/sh -e + +trustedcertbase64="$(snapctl get trustedcertbase64)" + +cmd="\"$SNAP\"/opcplc --pn=50000 --sn=10 --sr=10 --st=uint --fn=10 --fr=1 --ft=uint --gn=10 \ + --appcertstorepath=\"$SNAP_USER_DATA/pki/own\" \ + --trustedcertstorepath=\"$SNAP_USER_DATA/pki/trusted\" \ + --rejectedcertstorepath=\"$SNAP_USER_DATA/pki/rejected\" \ + --issuercertstorepath=\"$SNAP_USER_DATA/pki/issuer\" \ + --logfile=\"$SNAP_USER_DATA/hostname-port-plc.log\"" + +if [ -n "$trustedcertbase64" ]; then + cmd="$cmd --addtrustedcertfile=\"$SNAP_DATA/config/pki/trusted/certs/cert_1.crt\"" +fi + +eval "$cmd" diff --git a/snap/hooks/configure b/snap/hooks/configure new file mode 100644 index 00000000..ab9111da --- /dev/null +++ b/snap/hooks/configure @@ -0,0 +1,15 @@ +#!/bin/sh -e + +# Supported keys: +# - trustedcertbase64 (string) +# Certificate in base64 string format to be trusted by the server. + +handle_trustedcertbase64() +{ + trustedcertbase64="$(snapctl get trustedcertbase64)" + if [ -n "$trustedcertbase64" ]; then + echo "$trustedcertbase64" > "$SNAP_DATA/config/pki/trusted/certs/cert_1.crt" + fi +} + +handle_trustedcertbase64 diff --git a/snap/hooks/default-configure b/snap/hooks/default-configure new file mode 100644 index 00000000..bd508243 --- /dev/null +++ b/snap/hooks/default-configure @@ -0,0 +1,3 @@ +#!/bin/sh -e + +mkdir -p "$SNAP_DATA/config/pki/trusted/certs" diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 00000000..4d85439c --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,36 @@ +name: iot-edge-opc-plc +base: core22 +version: '0.1' +summary: Sample OPC UA server +description: | + Sample OPC UA server with nodes that generate random + and increasing data, anomalies and much more. + +grade: stable +confinement: strict # use 'strict' once you have the right plugs and slots + +parts: + opc-plc: + plugin: dotnet + dotnet-build-configuration: Release + dotnet-self-contained-runtime-identifier: linux-x64 + source: . + build-packages: + - dotnet-sdk-8.0 + scripts: + plugin: dump + source: scripts/ + organize: + '*' : scripts/ + appsettings: + plugin: dump + source: src/ + prime: + - appsettings.json + +apps: + opc-plc: + command: scripts/run + plugs: + - network + - network-bind diff --git a/src/CompanionSpecs/DI/DiNodeManager.cs b/src/CompanionSpecs/DI/DiNodeManager.cs index 542844db..cc74cbaf 100644 --- a/src/CompanionSpecs/DI/DiNodeManager.cs +++ b/src/CompanionSpecs/DI/DiNodeManager.cs @@ -2,6 +2,8 @@ using Opc.Ua; using Opc.Ua.Server; +using System; +using System.IO; using System.Reflection; /// @@ -29,9 +31,16 @@ public DiNodeManager(IServerInternal server, ApplicationConfiguration _) /// protected override NodeStateCollection LoadPredefinedNodes(ISystemContext context) { + var uanodesPath = "CompanionSpecs/DI/Opc.Ua.DI.PredefinedNodes.uanodes"; + var snapLocation = Environment.GetEnvironmentVariable("SNAP"); + if (!string.IsNullOrWhiteSpace(snapLocation)) + { + // Aplication running as a snap + uanodesPath = Path.Join(snapLocation, uanodesPath); + } var predefinedNodes = new NodeStateCollection(); predefinedNodes.LoadFromBinaryResource(context, - "CompanionSpecs/DI/Opc.Ua.DI.PredefinedNodes.uanodes", // CopyToOutputDirectory -> PreserveNewest. + uanodesPath, // CopyToOutputDirectory -> PreserveNewest. typeof(DiNodeManager).GetTypeInfo().Assembly, updateTables: true); diff --git a/src/OpcPlcServer.cs b/src/OpcPlcServer.cs index eaec0b69..aa459cff 100644 --- a/src/OpcPlcServer.cs +++ b/src/OpcPlcServer.cs @@ -374,9 +374,17 @@ private void InitLogging() /// public IHost CreateHostBuilder(string[] args) { + var contentRoot = Directory.GetCurrentDirectory(); + var snapLocation = Environment.GetEnvironmentVariable("SNAP"); + if (!string.IsNullOrWhiteSpace(snapLocation)) + { + // The application is running as a snap + contentRoot = snapLocation; + } + var host = Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { - webBuilder.UseContentRoot(Directory.GetCurrentDirectory()); // Avoid System.InvalidOperationException. + webBuilder.UseContentRoot(contentRoot); // Avoid System.InvalidOperationException. webBuilder.UseUrls($"http://*:{Config.WebServerPort}"); webBuilder.UseStartup(); }).Build(); diff --git a/src/PluginNodes/Boiler2PluginNodes.cs b/src/PluginNodes/Boiler2PluginNodes.cs index 95e386ec..3c1f6a01 100644 --- a/src/PluginNodes/Boiler2PluginNodes.cs +++ b/src/PluginNodes/Boiler2PluginNodes.cs @@ -7,6 +7,7 @@ namespace OpcPlc.PluginNodes; using OpcPlc.PluginNodes.Models; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using System.Threading; @@ -41,7 +42,7 @@ public class Boiler2PluginNodes(TimeService timeService, ILogger logger) : Plugi private TimeSpan _overheatInterval = TimeSpan.FromSeconds(120); // 2 min. private bool _isOverheated; - private readonly SemaphoreSlim _lock = new(1, 1); + private readonly SemaphoreSlim _lock = new(1, 1); public void AddOptions(Mono.Options.OptionSet optionSet) { @@ -154,10 +155,18 @@ private void AddNodes() /// private NodeStateCollection LoadPredefinedNodes(ISystemContext context) { + var uanodesPath = "Boilers/Boiler2/BoilerModel2.PredefinedNodes.uanodes"; + var snapLocation = Environment.GetEnvironmentVariable("SNAP"); + if (!string.IsNullOrWhiteSpace(snapLocation)) + { + // Aplication running as a snap + uanodesPath = Path.Join(snapLocation, uanodesPath); + } + var predefinedNodes = new NodeStateCollection(); predefinedNodes.LoadFromBinaryResource(context, - "Boilers/Boiler2/BoilerModel2.PredefinedNodes.uanodes", // CopyToOutputDirectory -> PreserveNewest. + uanodesPath, // CopyToOutputDirectory -> PreserveNewest. typeof(PlcNodeManager).GetTypeInfo().Assembly, updateTables: true); diff --git a/src/PluginNodes/ComplexTypeBoilerPluginNode.cs b/src/PluginNodes/ComplexTypeBoilerPluginNode.cs index 7a9a795d..7969978f 100644 --- a/src/PluginNodes/ComplexTypeBoilerPluginNode.cs +++ b/src/PluginNodes/ComplexTypeBoilerPluginNode.cs @@ -7,6 +7,7 @@ namespace OpcPlc.PluginNodes; using OpcPlc.PluginNodes.Models; using System; using System.Collections.Generic; +using System.IO; using System.Reflection; using System.Timers; @@ -17,7 +18,7 @@ public class ComplexTypeBoilerPluginNode(TimeService timeService, ILogger logger { private PlcNodeManager _plcNodeManager; private Boiler1State _node; - private ITimer _nodeGenerator; + private ITimer _nodeGenerator; public void AddOptions(Mono.Options.OptionSet optionSet) { @@ -58,12 +59,12 @@ private void AddNodes(FolderState methodsFolder) // Convert to node that can be manipulated within the server. _node = new Boiler1State(null); - _node.Create(_plcNodeManager.SystemContext, passiveBoiler1Node); - _node.BoilerStatus.Value = new BoilerDataType { - Pressure = 99_000, + _node.Create(_plcNodeManager.SystemContext, passiveBoiler1Node); + _node.BoilerStatus.Value = new BoilerDataType { + Pressure = 99_000, Temperature = new BoilerTemperatureType { Bottom = 100, Top = 95 }, HeaterState = BoilerHeaterStateType.On, - }; + }; _node.BoilerStatus.ClearChangeMasks(_plcNodeManager.SystemContext, includeChildren: true); // Put Boiler #2 into Boilers folder. @@ -90,10 +91,18 @@ private void AddNodes(FolderState methodsFolder) /// private NodeStateCollection LoadPredefinedNodes(ISystemContext context) { + var uanodesPath = "Boilers/Boiler1/BoilerModel1.PredefinedNodes.uanodes"; + var snapLocation = Environment.GetEnvironmentVariable("SNAP"); + if (!string.IsNullOrWhiteSpace(snapLocation)) + { + // Aplication running as a snap + uanodesPath = Path.Join(snapLocation, uanodesPath); + } + var predefinedNodes = new NodeStateCollection(); predefinedNodes.LoadFromBinaryResource(context, - "Boilers/Boiler1/BoilerModel1.PredefinedNodes.uanodes", // CopyToOutputDirectory -> PreserveNewest. + uanodesPath, // CopyToOutputDirectory -> PreserveNewest. typeof(PlcNodeManager).GetTypeInfo().Assembly, updateTables: true); diff --git a/src/SimpleEvent/SimpleEventsNodeManager.cs b/src/SimpleEvent/SimpleEventsNodeManager.cs index fac8873c..f80de32d 100644 --- a/src/SimpleEvent/SimpleEventsNodeManager.cs +++ b/src/SimpleEvent/SimpleEventsNodeManager.cs @@ -34,6 +34,7 @@ namespace SimpleEvents; using OpcPlc.SimpleEvent; using System; using System.Collections.Generic; +using System.IO; using System.Reflection; using System.Threading; @@ -88,9 +89,17 @@ public override NodeId New(ISystemContext context, NodeState node) /// protected override NodeStateCollection LoadPredefinedNodes(ISystemContext context) { + var uanodesPath = "SimpleEvent/SimpleEvents.PredefinedNodes.uanodes"; + var snapLocation = Environment.GetEnvironmentVariable("SNAP"); + if (!string.IsNullOrWhiteSpace(snapLocation)) + { + // Aplication running as a snap + uanodesPath = Path.Join(snapLocation, uanodesPath); + } + var predefinedNodes = new NodeStateCollection(); predefinedNodes.LoadFromBinaryResource(context, - "SimpleEvent/SimpleEvents.PredefinedNodes.uanodes", + uanodesPath, typeof(SimpleEventsNodeManager).GetTypeInfo().Assembly, updateTables: true); return predefinedNodes; From d88eb5303092a0dd9e6f49e3ad11c1a65f2ed9f5 Mon Sep 17 00:00:00 2001 From: Luis Cantero Date: Wed, 11 Sep 2024 14:13:51 +0200 Subject: [PATCH 2/3] Fix typo --- src/CompanionSpecs/DI/DiNodeManager.cs | 2 +- src/PluginNodes/Boiler2PluginNodes.cs | 2 +- src/PluginNodes/ComplexTypeBoilerPluginNode.cs | 2 +- src/SimpleEvent/SimpleEventsNodeManager.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CompanionSpecs/DI/DiNodeManager.cs b/src/CompanionSpecs/DI/DiNodeManager.cs index cc74cbaf..26ae2701 100644 --- a/src/CompanionSpecs/DI/DiNodeManager.cs +++ b/src/CompanionSpecs/DI/DiNodeManager.cs @@ -35,7 +35,7 @@ protected override NodeStateCollection LoadPredefinedNodes(ISystemContext contex var snapLocation = Environment.GetEnvironmentVariable("SNAP"); if (!string.IsNullOrWhiteSpace(snapLocation)) { - // Aplication running as a snap + // Application running as a snap uanodesPath = Path.Join(snapLocation, uanodesPath); } var predefinedNodes = new NodeStateCollection(); diff --git a/src/PluginNodes/Boiler2PluginNodes.cs b/src/PluginNodes/Boiler2PluginNodes.cs index 3c1f6a01..83620adb 100644 --- a/src/PluginNodes/Boiler2PluginNodes.cs +++ b/src/PluginNodes/Boiler2PluginNodes.cs @@ -159,7 +159,7 @@ private NodeStateCollection LoadPredefinedNodes(ISystemContext context) var snapLocation = Environment.GetEnvironmentVariable("SNAP"); if (!string.IsNullOrWhiteSpace(snapLocation)) { - // Aplication running as a snap + // Application running as a snap uanodesPath = Path.Join(snapLocation, uanodesPath); } diff --git a/src/PluginNodes/ComplexTypeBoilerPluginNode.cs b/src/PluginNodes/ComplexTypeBoilerPluginNode.cs index 7969978f..66f0a15c 100644 --- a/src/PluginNodes/ComplexTypeBoilerPluginNode.cs +++ b/src/PluginNodes/ComplexTypeBoilerPluginNode.cs @@ -95,7 +95,7 @@ private NodeStateCollection LoadPredefinedNodes(ISystemContext context) var snapLocation = Environment.GetEnvironmentVariable("SNAP"); if (!string.IsNullOrWhiteSpace(snapLocation)) { - // Aplication running as a snap + // Application running as a snap uanodesPath = Path.Join(snapLocation, uanodesPath); } diff --git a/src/SimpleEvent/SimpleEventsNodeManager.cs b/src/SimpleEvent/SimpleEventsNodeManager.cs index f80de32d..3d10bf29 100644 --- a/src/SimpleEvent/SimpleEventsNodeManager.cs +++ b/src/SimpleEvent/SimpleEventsNodeManager.cs @@ -93,7 +93,7 @@ protected override NodeStateCollection LoadPredefinedNodes(ISystemContext contex var snapLocation = Environment.GetEnvironmentVariable("SNAP"); if (!string.IsNullOrWhiteSpace(snapLocation)) { - // Aplication running as a snap + // Application running as a snap uanodesPath = Path.Join(snapLocation, uanodesPath); } From bbe2002b526c1a10d19aa876e7a1dff8e4091c90 Mon Sep 17 00:00:00 2001 From: Luis Cantero Date: Wed, 11 Sep 2024 14:15:26 +0200 Subject: [PATCH 3/3] Cosmetic --- src/CompanionSpecs/DI/DiNodeManager.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CompanionSpecs/DI/DiNodeManager.cs b/src/CompanionSpecs/DI/DiNodeManager.cs index 26ae2701..d62f831c 100644 --- a/src/CompanionSpecs/DI/DiNodeManager.cs +++ b/src/CompanionSpecs/DI/DiNodeManager.cs @@ -1,4 +1,4 @@ -namespace OpcPlc.CompanionSpecs.DI; +namespace OpcPlc.CompanionSpecs.DI; using Opc.Ua; using Opc.Ua.Server; @@ -37,7 +37,8 @@ protected override NodeStateCollection LoadPredefinedNodes(ISystemContext contex { // Application running as a snap uanodesPath = Path.Join(snapLocation, uanodesPath); - } + } + var predefinedNodes = new NodeStateCollection(); predefinedNodes.LoadFromBinaryResource(context, uanodesPath, // CopyToOutputDirectory -> PreserveNewest.