fix: Accept non-standard OPC UA OK status by implementing a configurable workaround (#10384)
Thanks!
This commit is contained in:
parent
48b981bd4e
commit
515715fee3
|
|
@ -89,6 +89,11 @@ Plugin minimum tested version: 1.16
|
||||||
# {name="", namespace="", identifier_type="", identifier=""},
|
# {name="", namespace="", identifier_type="", identifier=""},
|
||||||
# {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
|
## Node Configuration
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -18,23 +19,28 @@ import (
|
||||||
"github.com/influxdata/telegraf/selfstat"
|
"github.com/influxdata/telegraf/selfstat"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type OpcuaWorkarounds struct {
|
||||||
|
AdditionalValidStatusCodes []string `toml:"additional_valid_status_codes"`
|
||||||
|
}
|
||||||
|
|
||||||
// OpcUA type
|
// OpcUA type
|
||||||
type OpcUA struct {
|
type OpcUA struct {
|
||||||
MetricName string `toml:"name"`
|
MetricName string `toml:"name"`
|
||||||
Endpoint string `toml:"endpoint"`
|
Endpoint string `toml:"endpoint"`
|
||||||
SecurityPolicy string `toml:"security_policy"`
|
SecurityPolicy string `toml:"security_policy"`
|
||||||
SecurityMode string `toml:"security_mode"`
|
SecurityMode string `toml:"security_mode"`
|
||||||
Certificate string `toml:"certificate"`
|
Certificate string `toml:"certificate"`
|
||||||
PrivateKey string `toml:"private_key"`
|
PrivateKey string `toml:"private_key"`
|
||||||
Username string `toml:"username"`
|
Username string `toml:"username"`
|
||||||
Password string `toml:"password"`
|
Password string `toml:"password"`
|
||||||
Timestamp string `toml:"timestamp"`
|
Timestamp string `toml:"timestamp"`
|
||||||
AuthMethod string `toml:"auth_method"`
|
AuthMethod string `toml:"auth_method"`
|
||||||
ConnectTimeout config.Duration `toml:"connect_timeout"`
|
ConnectTimeout config.Duration `toml:"connect_timeout"`
|
||||||
RequestTimeout config.Duration `toml:"request_timeout"`
|
RequestTimeout config.Duration `toml:"request_timeout"`
|
||||||
RootNodes []NodeSettings `toml:"nodes"`
|
RootNodes []NodeSettings `toml:"nodes"`
|
||||||
Groups []GroupSettings `toml:"group"`
|
Groups []GroupSettings `toml:"group"`
|
||||||
Log telegraf.Logger `toml:"-"`
|
Workarounds OpcuaWorkarounds `toml:"workarounds"`
|
||||||
|
Log telegraf.Logger `toml:"-"`
|
||||||
|
|
||||||
nodes []Node
|
nodes []Node
|
||||||
nodeData []OPCData
|
nodeData []OPCData
|
||||||
|
|
@ -50,6 +56,7 @@ type OpcUA struct {
|
||||||
client *opcua.Client
|
client *opcua.Client
|
||||||
req *ua.ReadRequest
|
req *ua.ReadRequest
|
||||||
opts []opcua.Option
|
opts []opcua.Option
|
||||||
|
codes []ua.StatusCode
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeSettings struct {
|
type NodeSettings struct {
|
||||||
|
|
@ -180,6 +187,11 @@ const sampleConfig = `
|
||||||
# {name="", namespace="", identifier_type="", identifier=""},
|
# {name="", namespace="", identifier_type="", identifier=""},
|
||||||
# {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
|
// Description will appear directly above the plugin definition in the config file
|
||||||
|
|
@ -216,6 +228,11 @@ func (o *OpcUA) Init() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = o.setupWorkarounds()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
tags := map[string]string{
|
tags := map[string]string{
|
||||||
"endpoint": o.Endpoint,
|
"endpoint": o.Endpoint,
|
||||||
}
|
}
|
||||||
|
|
@ -480,6 +497,28 @@ func (o *OpcUA) setupOptions() error {
|
||||||
return err
|
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 {
|
func (o *OpcUA) getData() error {
|
||||||
resp, err := o.client.Read(o.req)
|
resp, err := o.client.Read(o.req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -489,7 +528,7 @@ func (o *OpcUA) getData() error {
|
||||||
o.ReadSuccess.Incr(1)
|
o.ReadSuccess.Incr(1)
|
||||||
for i, d := range resp.Results {
|
for i, d := range resp.Results {
|
||||||
o.nodeData[i].Quality = d.Status
|
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)
|
o.Log.Errorf("status not OK for node %v: %v", o.nodes[i].tag.FieldName, d.Status)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -553,7 +592,7 @@ func (o *OpcUA) Gather(acc telegraf.Accumulator) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, n := range o.nodes {
|
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{})
|
fields := make(map[string]interface{})
|
||||||
tags := map[string]string{
|
tags := map[string]string{
|
||||||
"id": n.idStr,
|
"id": n.idStr,
|
||||||
|
|
@ -593,6 +632,7 @@ func init() {
|
||||||
Certificate: "/etc/telegraf/cert.pem",
|
Certificate: "/etc/telegraf/cert.pem",
|
||||||
PrivateKey: "/etc/telegraf/key.pem",
|
PrivateKey: "/etc/telegraf/key.pem",
|
||||||
AuthMethod: "Anonymous",
|
AuthMethod: "Anonymous",
|
||||||
|
codes: []ua.StatusCode{ua.StatusOK},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/gopcua/opcua/ua"
|
||||||
"github.com/influxdata/telegraf/config"
|
"github.com/influxdata/telegraf/config"
|
||||||
"github.com/influxdata/telegraf/testutil"
|
"github.com/influxdata/telegraf/testutil"
|
||||||
)
|
)
|
||||||
|
|
@ -43,6 +44,7 @@ func TestClient1Integration(t *testing.T) {
|
||||||
o.RequestTimeout = config.Duration(1 * time.Second)
|
o.RequestTimeout = config.Duration(1 * time.Second)
|
||||||
o.SecurityPolicy = "None"
|
o.SecurityPolicy = "None"
|
||||||
o.SecurityMode = "None"
|
o.SecurityMode = "None"
|
||||||
|
o.codes = []ua.StatusCode{ua.StatusOK}
|
||||||
o.Log = testutil.Logger{}
|
o.Log = testutil.Logger{}
|
||||||
for _, tags := range testopctags {
|
for _, tags := range testopctags {
|
||||||
o.RootNodes = append(o.RootNodes, MapOPCTag(tags))
|
o.RootNodes = append(o.RootNodes, MapOPCTag(tags))
|
||||||
|
|
@ -108,6 +110,9 @@ namespace = "0"
|
||||||
identifier_type = "i"
|
identifier_type = "i"
|
||||||
tags = [["tag1", "val1"], ["tag2", "val2"]]
|
tags = [["tag1", "val1"], ["tag2", "val2"]]
|
||||||
nodes = [{name="name4", identifier="4000", tags=[["tag1", "override"]]}]
|
nodes = [{name="name4", identifier="4000", tags=[["tag1", "override"]]}]
|
||||||
|
|
||||||
|
[inputs.opcua.workarounds]
|
||||||
|
additional_valid_status_codes = ["0xC0"]
|
||||||
`
|
`
|
||||||
|
|
||||||
c := config.NewConfig()
|
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, 4)
|
||||||
require.Len(t, o.nodes[2].metricTags, 3)
|
require.Len(t, o.nodes[2].metricTags, 3)
|
||||||
require.Len(t, o.nodes[3].metricTags, 2)
|
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) {
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue