From 515715fee3aa59420b1d60fc1fb4b0b63455bb70 Mon Sep 17 00:00:00 2001 From: R290 <46033588+R290@users.noreply.github.com> Date: Tue, 11 Jan 2022 23:39:18 +0100 Subject: [PATCH] fix: Accept non-standard OPC UA OK status by implementing a configurable workaround (#10384) Thanks! --- plugins/inputs/opcua/README.md | 5 ++ plugins/inputs/opcua/opcua_client.go | 74 +++++++++++++++++------ plugins/inputs/opcua/opcua_client_test.go | 29 +++++++++ 3 files changed, 91 insertions(+), 17 deletions(-) diff --git a/plugins/inputs/opcua/README.md b/plugins/inputs/opcua/README.md index edd9b77c9..7ae752fe1 100644 --- a/plugins/inputs/opcua/README.md +++ b/plugins/inputs/opcua/README.md @@ -89,6 +89,11 @@ Plugin minimum tested version: 1.16 # {name="", namespace="", identifier_type="", identifier=""}, # {name="", namespace="", identifier_type="", identifier=""}, #] + + ## Enable workarounds required by some devices to work correctly + # [inputs.opcua.workarounds] + ## Set additional valid status codes, StatusOK (0x0) is always considered valid + # additional_valid_status_codes = ["0xC0"] ``` ## Node Configuration diff --git a/plugins/inputs/opcua/opcua_client.go b/plugins/inputs/opcua/opcua_client.go index 14315e5fe..9d64216c8 100644 --- a/plugins/inputs/opcua/opcua_client.go +++ b/plugins/inputs/opcua/opcua_client.go @@ -5,6 +5,7 @@ import ( "fmt" "net/url" "sort" + "strconv" "strings" "time" @@ -18,23 +19,28 @@ import ( "github.com/influxdata/telegraf/selfstat" ) +type OpcuaWorkarounds struct { + AdditionalValidStatusCodes []string `toml:"additional_valid_status_codes"` +} + // OpcUA type type OpcUA struct { - MetricName string `toml:"name"` - Endpoint string `toml:"endpoint"` - SecurityPolicy string `toml:"security_policy"` - SecurityMode string `toml:"security_mode"` - Certificate string `toml:"certificate"` - PrivateKey string `toml:"private_key"` - Username string `toml:"username"` - Password string `toml:"password"` - Timestamp string `toml:"timestamp"` - AuthMethod string `toml:"auth_method"` - ConnectTimeout config.Duration `toml:"connect_timeout"` - RequestTimeout config.Duration `toml:"request_timeout"` - RootNodes []NodeSettings `toml:"nodes"` - Groups []GroupSettings `toml:"group"` - Log telegraf.Logger `toml:"-"` + MetricName string `toml:"name"` + Endpoint string `toml:"endpoint"` + SecurityPolicy string `toml:"security_policy"` + SecurityMode string `toml:"security_mode"` + Certificate string `toml:"certificate"` + PrivateKey string `toml:"private_key"` + Username string `toml:"username"` + Password string `toml:"password"` + Timestamp string `toml:"timestamp"` + AuthMethod string `toml:"auth_method"` + ConnectTimeout config.Duration `toml:"connect_timeout"` + RequestTimeout config.Duration `toml:"request_timeout"` + RootNodes []NodeSettings `toml:"nodes"` + Groups []GroupSettings `toml:"group"` + Workarounds OpcuaWorkarounds `toml:"workarounds"` + Log telegraf.Logger `toml:"-"` nodes []Node nodeData []OPCData @@ -50,6 +56,7 @@ type OpcUA struct { client *opcua.Client req *ua.ReadRequest opts []opcua.Option + codes []ua.StatusCode } type NodeSettings struct { @@ -180,6 +187,11 @@ const sampleConfig = ` # {name="", namespace="", identifier_type="", identifier=""}, # {name="", namespace="", identifier_type="", identifier=""}, #] + + ## Enable workarounds required by some devices to work correctly + # [inputs.opcua.workarounds] + ## Set additional valid status codes, StatusOK (0x0) is always considered valid + # additional_valid_status_codes = ["0xC0"] ` // Description will appear directly above the plugin definition in the config file @@ -216,6 +228,11 @@ func (o *OpcUA) Init() error { return err } + err = o.setupWorkarounds() + if err != nil { + return err + } + tags := map[string]string{ "endpoint": o.Endpoint, } @@ -480,6 +497,28 @@ func (o *OpcUA) setupOptions() error { return err } +func (o *OpcUA) setupWorkarounds() error { + if len(o.Workarounds.AdditionalValidStatusCodes) != 0 { + for _, c := range o.Workarounds.AdditionalValidStatusCodes { + val, err := strconv.ParseInt(c, 0, 32) // setting 32 bits to allow for safe conversion + if err != nil { + return err + } + o.codes = append(o.codes, ua.StatusCode(uint32(val))) + } + } + return nil +} + +func (o *OpcUA) checkStatusCode(code ua.StatusCode) bool { + for _, val := range o.codes { + if val == code { + return true + } + } + return false +} + func (o *OpcUA) getData() error { resp, err := o.client.Read(o.req) if err != nil { @@ -489,7 +528,7 @@ func (o *OpcUA) getData() error { o.ReadSuccess.Incr(1) for i, d := range resp.Results { o.nodeData[i].Quality = d.Status - if d.Status != ua.StatusOK { + if !o.checkStatusCode(d.Status) { o.Log.Errorf("status not OK for node %v: %v", o.nodes[i].tag.FieldName, d.Status) continue } @@ -553,7 +592,7 @@ func (o *OpcUA) Gather(acc telegraf.Accumulator) error { } for i, n := range o.nodes { - if o.nodeData[i].Quality == ua.StatusOK { + if o.checkStatusCode(o.nodeData[i].Quality) { fields := make(map[string]interface{}) tags := map[string]string{ "id": n.idStr, @@ -593,6 +632,7 @@ func init() { Certificate: "/etc/telegraf/cert.pem", PrivateKey: "/etc/telegraf/key.pem", AuthMethod: "Anonymous", + codes: []ua.StatusCode{ua.StatusOK}, } }) } diff --git a/plugins/inputs/opcua/opcua_client_test.go b/plugins/inputs/opcua/opcua_client_test.go index 27bfc1ecf..b3a11ac87 100644 --- a/plugins/inputs/opcua/opcua_client_test.go +++ b/plugins/inputs/opcua/opcua_client_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/gopcua/opcua/ua" "github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/testutil" ) @@ -43,6 +44,7 @@ func TestClient1Integration(t *testing.T) { o.RequestTimeout = config.Duration(1 * time.Second) o.SecurityPolicy = "None" o.SecurityMode = "None" + o.codes = []ua.StatusCode{ua.StatusOK} o.Log = testutil.Logger{} for _, tags := range testopctags { o.RootNodes = append(o.RootNodes, MapOPCTag(tags)) @@ -108,6 +110,9 @@ namespace = "0" identifier_type = "i" tags = [["tag1", "val1"], ["tag2", "val2"]] nodes = [{name="name4", identifier="4000", tags=[["tag1", "override"]]}] + +[inputs.opcua.workarounds] +additional_valid_status_codes = ["0xC0"] ` c := config.NewConfig() @@ -132,6 +137,9 @@ nodes = [{name="name4", identifier="4000", tags=[["tag1", "override"]]}] require.Len(t, o.nodes, 4) require.Len(t, o.nodes[2].metricTags, 3) require.Len(t, o.nodes[3].metricTags, 2) + + require.Len(t, o.Workarounds.AdditionalValidStatusCodes, 1) + require.Equal(t, o.Workarounds.AdditionalValidStatusCodes[0], "0xC0") } func TestTagsSliceToMap(t *testing.T) { @@ -260,3 +268,24 @@ func TestValidateOPCTags(t *testing.T) { }) } } + +func TestSetupWorkarounds(t *testing.T) { + var o OpcUA + o.codes = []ua.StatusCode{ua.StatusOK} + + o.Workarounds.AdditionalValidStatusCodes = []string{"0xC0", "0x00AA0000"} + + err := o.setupWorkarounds() + require.NoError(t, err) + + require.Len(t, o.codes, 3) + require.Equal(t, o.codes[0], ua.StatusCode(0)) + require.Equal(t, o.codes[1], ua.StatusCode(192)) + require.Equal(t, o.codes[2], ua.StatusCode(11141120)) +} + +func TestCheckStatusCode(t *testing.T) { + var o OpcUA + o.codes = []ua.StatusCode{ua.StatusCode(0), ua.StatusCode(192), ua.StatusCode(11141120)} + require.Equal(t, o.checkStatusCode(ua.StatusCode(192)), true) +}