Opcua cleanup (#8076)

This commit is contained in:
reimda 2020-09-03 21:48:00 -06:00 committed by GitHub
parent f4e5b0ef63
commit b2ac8b4ac6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 188 additions and 150 deletions

View File

@ -9,52 +9,56 @@ Plugin minimum tested version: 1.16
```toml ```toml
[[inputs.opcua]] [[inputs.opcua]]
## This plugin supports connections to PLCs via OPC UA. ## Device name
## # name = "localhost"
## Device name #
name = "localhost" ## OPC UA Endpoint URL
# # endpoint = "opc.tcp://localhost:4840"
## OPC UA Endpoint URL #
endpoint = "opc.tcp://localhost:4840" ## Maximum time allowed to establish a connect to the endpoint.
# # connect_timeout = "10s"
## Read Timeout. Add an arbitrary timeout (seconds) to demonstrate how to stop a subscription with a context. #
timeout = 30 ## Maximum time allowed for a request over the estabilished connection.
# # request_timeout = "5s"
## Time Interval (Default = 10s) #
time_interval = "10s" ## Security policy, one of "None", "Basic128Rsa15", "Basic256",
# ## "Basic256Sha256", or "auto"
## Security policy: None, Basic128Rsa15, Basic256, Basic256Sha256. # security_policy = "auto"
security_policy = "None" #
# ## Security mode, one of "None", "Sign", "SignAndEncrypt", or "auto"
## Security mode: None, Sign, SignAndEncrypt. # security_mode = "auto"
security_mode = "None" #
# ## Path to cert.pem. Required when security mode or policy isn't "None".
## Path to cert.pem. Required for security mode/policy != None. If cert path is not supplied, self-signed cert and key will be generated. ## If cert path is not supplied, self-signed cert and key will be generated.
## certificate = "/etc/telegraf/cert.pem" # certificate = "/etc/telegraf/cert.pem"
# #
## Path to private key.pem. Required for security mode/policy != None. If key path is not supplied, self-signed cert and key will be generated. ## Path to private key.pem. Required when security mode or policy isn't "None".
## private_key = "/etc/telegraf/key.pem" ## If key path is not supplied, self-signed cert and key will be generated.
# # private_key = "/etc/telegraf/key.pem"
## To authenticate using a specific ID, select 'Certificate' or 'UserName'. Default is "Anonymous" #
## auth_method = "Anonymous" ## Authentication Method, one of "Certificate", "UserName", or "Anonymous". To
# ## authenticate using a specific ID, select 'Certificate' or 'UserName'
## Required for auth_method = "UserName" # auth_method = "Anonymous"
## username = "myusername" #
# ## Username. Required for auth_method = "UserName"
## Required for auth_method = "UserName" # username = ""
## password = "mypassword" #
# ## Password. Required for auth_method = "UserName"
## Node ID configuration # password = ""
## name - the variable name #
## namespace - integer value 0 thru 3 ## Node ID configuration
## identifier_type - s=string, i=numeric, g=guid, b=opaque ## name - the variable name
## identifier - tag as shown in opcua browser ## namespace - integer value 0 thru 3
## data_type - boolean, byte, short, int, uint, uint16, int16, uint32, int32, float, double, string, datetime, number ## identifier_type - s=string, i=numeric, g=guid, b=opaque
## Example: {name="ProductUri", namespace="0", identifier_type="i", identifier="2262", data_type="string", description="http://open62541.org"} ## identifier - tag as shown in opcua browser
nodes = [ ## data_type - boolean, byte, short, int, uint, uint16, int16,
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""}, ## uint32, int32, float, double, string, datetime, number
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""}, ## Example:
] ## {name="ProductUri", namespace="0", identifier_type="i", identifier="2262", data_type="string", description="http://open62541.org"}
nodes = [
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""},
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""},
]
``` ```
### Example Node Configuration ### Example Node Configuration

View File

@ -11,38 +11,39 @@ import (
"github.com/gopcua/opcua" "github.com/gopcua/opcua"
"github.com/gopcua/opcua/ua" "github.com/gopcua/opcua/ua"
"github.com/influxdata/telegraf" "github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/plugins/inputs" "github.com/influxdata/telegraf/plugins/inputs"
) )
// OpcUA type // OpcUA type
type OpcUA struct { type OpcUA struct {
Name string `toml:"name"` Name 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"`
AuthMethod string `toml:"auth_method"` AuthMethod string `toml:"auth_method"`
Interval string `toml:"time_interval"` ConnectTimeout config.Duration `toml:"connect_timeout"`
TimeOut int `toml:"timeout"` RequestTimeout config.Duration `toml:"request_timeout"`
NodeList []OPCTag `toml:"nodes"` NodeList []OPCTag `toml:"nodes"`
Nodes []string
NodeData []OPCData Nodes []string `toml:"-"`
NodeIDs []*ua.NodeID NodeData []OPCData `toml:"-"`
NodeIDerror []error NodeIDs []*ua.NodeID `toml:"-"`
state ConnectionState NodeIDerror []error `toml:"-"`
state ConnectionState
// status // status
ReadSuccess int ReadSuccess int `toml:"-"`
ReadError int ReadError int `toml:"-"`
NumberOfTags int NumberOfTags int `toml:"-"`
// internal values // internal values
client *opcua.Client client *opcua.Client
req *ua.ReadRequest req *ua.ReadRequest
ctx context.Context
opts []opcua.Option opts []opcua.Option
} }
@ -80,67 +81,57 @@ const (
const description = `Retrieve data from OPCUA devices` const description = `Retrieve data from OPCUA devices`
const sampleConfig = ` const sampleConfig = `
# ## Connection Configuration [[inputs.opcua]]
# ## ## Device name
# ## The plugin supports connections to PLCs via OPCUA # name = "localhost"
# ## #
# ## Device name ## OPC UA Endpoint URL
name = "opcua_rocks" # endpoint = "opc.tcp://localhost:4840"
# #
# # OPC UA Endpoint URL ## Maximum time allowed to establish a connect to the endpoint.
endpoint = "opc.tcp://opcua.rocks:4840" # connect_timeout = "10s"
# #
# ## Read Timeout ## Maximum time allowed for a request over the estabilished connection.
# ## add an arbitrary timeout (seconds) to demonstrate how to stop a subscription # request_timeout = "5s"
# ## with a context. #
timeout = 30 ## Security policy, one of "None", "Basic128Rsa15", "Basic256",
# ## "Basic256Sha256", or "auto"
# # Time Inteval, default = 10s # security_policy = "auto"
time_interval = "5s" #
# ## Security mode, one of "None", "Sign", "SignAndEncrypt", or "auto"
# # Security policy: None, Basic128Rsa15, Basic256, Basic256Sha256. Default: auto # security_mode = "auto"
security_policy = "None" #
# ## Path to cert.pem. Required when security mode or policy isn't "None".
# # Security mode: None, Sign, SignAndEncrypt. Default: auto ## If cert path is not supplied, self-signed cert and key will be generated.
security_mode = "None" # certificate = "/etc/telegraf/cert.pem"
# #
# # Path to cert.pem. Required for security mode/policy != None. If cert path is not supplied, self-signed cert and key will be generated. ## Path to private key.pem. Required when security mode or policy isn't "None".
# # certificate = "/etc/telegraf/cert.pem" ## If key path is not supplied, self-signed cert and key will be generated.
# # private_key = "/etc/telegraf/key.pem"
# # Path to private key.pem. Required for security mode/policy != None. If key path is not supplied, self-signed cert and key will be generated. #
# # private_key = "/etc/telegraf/key.pem" ## Authentication Method, one of "Certificate", "UserName", or "Anonymous". To
# ## authenticate using a specific ID, select 'Certificate' or 'UserName'
# # To authenticate using a specific ID, select chosen method from 'Certificate' or 'UserName'. Else use 'Anonymous.' Defaults to 'Anonymous' if not provided. # auth_method = "Anonymous"
# # auth_method = "Anonymous" #
# ## Username. Required for auth_method = "UserName"
# # Required for auth_method = "UserName" # username = ""
# # username = "myusername" #
# ## Password. Required for auth_method = "UserName"
# # Required for auth_method = "UserName" # password = ""
# # password = "mypassword" #
# ## Node ID configuration
# ## Measurements ## name - the variable name
# ## node id to subscribe to ## namespace - integer value 0 thru 3
# ## name - the variable name ## identifier_type - s=string, i=numeric, g=guid, b=opaque
# ## namespace - integer value 0 thru 3 ## identifier - tag as shown in opcua browser
# ## identifier_type - s=string, i=numeric, g=guid, b=opaque ## data_type - boolean, byte, short, int, uint, uint16, int16,
# ## identifier - tag as shown in opcua browser ## uint32, int32, float, double, string, datetime, number
# ## data_type - boolean, byte, short, int, uint, uint16, int16, uint32, int32, float, double, string, datetime, number ## Example:
# ## Template - {name="", namespace="", identifier_type="", identifier="", data_type="", description=""}, ## {name="ProductUri", namespace="0", identifier_type="i", identifier="2262", data_type="string", description="http://open62541.org"}
nodes = [ nodes = [
{name="ProductName", namespace="0", identifier_type="i", identifier="2261", data_type="string", description="open62541 OPC UA Server"}, {name="", namespace="", identifier_type="", identifier="", data_type="", description=""},
{name="ProductUri", namespace="0", identifier_type="i", identifier="2262", data_type="string", description="http://open62541.org"}, {name="", namespace="", identifier_type="", identifier="", data_type="", description=""},
{name="ManufacturerName", namespace="0", identifier_type="i", identifier="2263", data_type="string", description="open62541"}, ]
]
## Guide:
## An OPC UA node ID may resemble: "n=3,s=Temperature"
## In this example, n=3 is indicating the namespace is '3'.
## s=Temperature is indicting that the identifier type is a 'string' and the indentifier value is 'Temperature'
## This temperature node may have a current value of 79.0, which would possibly make the value a 'float'.
## To gather data from this node you would need to enter the following line into 'nodes' property above:
## {name="SomeLabel", namespace="3", identifier_type="s", identifier="Temperature", data_type="float", description="Some description."},
` `
// Description will appear directly above the plugin definition in the config file // Description will appear directly above the plugin definition in the config file
@ -157,8 +148,6 @@ func (o *OpcUA) SampleConfig() string {
func (o *OpcUA) Init() error { func (o *OpcUA) Init() error {
o.state = Disconnected o.state = Disconnected
o.ctx = context.Background()
err := o.validateEndpoint() err := o.validateEndpoint()
if err != nil { if err != nil {
return err return err
@ -177,13 +166,12 @@ func (o *OpcUA) Init() error {
} }
func (o *OpcUA) validateEndpoint() error { func (o *OpcUA) validateEndpoint() error {
//check device name
if o.Name == "" { if o.Name == "" {
return fmt.Errorf("device name is empty") return fmt.Errorf("device name is empty")
} }
//check device name
if o.Endpoint == "" { if o.Endpoint == "" {
return fmt.Errorf("device name is empty") return fmt.Errorf("endpoint url is empty")
} }
_, err := url.Parse(o.Endpoint) _, err := url.Parse(o.Endpoint)
@ -191,15 +179,6 @@ func (o *OpcUA) validateEndpoint() error {
return fmt.Errorf("endpoint url is invalid") return fmt.Errorf("endpoint url is invalid")
} }
if o.Interval == "" {
o.Interval = opcua.DefaultSubscriptionInterval.String()
}
_, err = time.ParseDuration(o.Interval)
if err != nil {
return fmt.Errorf("fatal error with time interval")
}
//search security policy type //search security policy type
switch o.SecurityPolicy { switch o.SecurityPolicy {
case "None", "Basic128Rsa15", "Basic256", "Basic256Sha256", "auto": case "None", "Basic128Rsa15", "Basic256", "Basic256Sha256", "auto":
@ -294,7 +273,9 @@ func Connect(o *OpcUA) error {
} }
o.client = opcua.NewClient(o.Endpoint, o.opts...) o.client = opcua.NewClient(o.Endpoint, o.opts...)
if err := o.client.Connect(o.ctx); err != nil { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(o.ConnectTimeout))
defer cancel()
if err := o.client.Connect(ctx); err != nil {
return fmt.Errorf("Error in Client Connection: %s", err) return fmt.Errorf("Error in Client Connection: %s", err)
} }
@ -336,7 +317,7 @@ func (o *OpcUA) setupOptions() error {
} }
} }
o.opts = generateClientOpts(endpoints, o.Certificate, o.PrivateKey, o.SecurityPolicy, o.SecurityMode, o.AuthMethod, o.Username, o.Password) o.opts = generateClientOpts(endpoints, o.Certificate, o.PrivateKey, o.SecurityPolicy, o.SecurityMode, o.AuthMethod, o.Username, o.Password, time.Duration(o.RequestTimeout))
return nil return nil
} }
@ -427,9 +408,17 @@ func (o *OpcUA) Gather(acc telegraf.Accumulator) error {
// Add this plugin to telegraf // Add this plugin to telegraf
func init() { func init() {
inputs.Add("opcua_client", func() telegraf.Input { inputs.Add("opcua", func() telegraf.Input {
return &OpcUA{ return &OpcUA{
AuthMethod: "Anonymous", Name: "localhost",
Endpoint: "opc.tcp://localhost:4840",
SecurityPolicy: "auto",
SecurityMode: "auto",
RequestTimeout: config.Duration(5 * time.Second),
ConnectTimeout: config.Duration(10 * time.Second),
Certificate: "/etc/telegraf/cert.pem",
PrivateKey: "/etc/telegraf/key.pem",
AuthMethod: "Anonymous",
} }
}) })
} }

View File

@ -4,6 +4,10 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"testing" "testing"
"time"
"github.com/influxdata/telegraf/config"
"github.com/stretchr/testify/require"
) )
type OPCTags struct { type OPCTags struct {
@ -16,6 +20,10 @@ type OPCTags struct {
} }
func TestClient1(t *testing.T) { func TestClient1(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
var testopctags = []OPCTags{ var testopctags = []OPCTags{
{"ProductName", "0", "i", "2261", "string", "open62541 OPC UA Server"}, {"ProductName", "0", "i", "2261", "string", "open62541 OPC UA Server"},
{"ProductUri", "0", "i", "2262", "string", "http://open62541.org"}, {"ProductUri", "0", "i", "2262", "string", "http://open62541.org"},
@ -27,8 +35,9 @@ func TestClient1(t *testing.T) {
o.Name = "testing" o.Name = "testing"
o.Endpoint = "opc.tcp://opcua.rocks:4840" o.Endpoint = "opc.tcp://opcua.rocks:4840"
o.Interval = "10ms" o.AuthMethod = "Anonymous"
o.TimeOut = 30 o.ConnectTimeout = config.Duration(10 * time.Second)
o.RequestTimeout = config.Duration(1 * time.Second)
o.SecurityPolicy = "None" o.SecurityPolicy = "None"
o.SecurityMode = "None" o.SecurityMode = "None"
for _, tags := range testopctags { for _, tags := range testopctags {
@ -40,7 +49,7 @@ func TestClient1(t *testing.T) {
} }
err = Connect(&o) err = Connect(&o)
if err != nil { if err != nil {
t.Logf("Connect Error: %s", err) t.Fatalf("Connect Error: %s", err)
} }
for i, v := range o.NodeData { for i, v := range o.NodeData {
@ -65,3 +74,37 @@ func MapOPCTag(tags OPCTags) (out OPCTag) {
out.DataType = tags.DataType out.DataType = tags.DataType
return out return out
} }
func TestConfig(t *testing.T) {
toml := `
[[inputs.opcua]]
name = "localhost"
endpoint = "opc.tcp://localhost:4840"
connect_timeout = "10s"
request_timeout = "5s"
security_policy = "auto"
security_mode = "auto"
certificate = "/etc/telegraf/cert.pem"
private_key = "/etc/telegraf/key.pem"
auth_method = "Anonymous"
username = ""
password = ""
nodes = [
{name="name", namespace="", identifier_type="", identifier="", data_type="", description=""},
{name="name2", namespace="", identifier_type="", identifier="", data_type="", description=""},
]
`
c := config.NewConfig()
err := c.LoadConfigData([]byte(toml))
require.NoError(t, err)
require.Len(t, c.Inputs, 1)
o, ok := c.Inputs[0].Input.(*OpcUA)
require.True(t, ok)
require.Len(t, o.NodeList, 2)
require.Equal(t, o.NodeList[0].Name, "name")
require.Equal(t, o.NodeList[1].Name, "name2")
}

View File

@ -147,7 +147,7 @@ func pemBlockForKey(priv interface{}) *pem.Block {
// OPT FUNCTIONS // OPT FUNCTIONS
func generateClientOpts(endpoints []*ua.EndpointDescription, certFile, keyFile, policy, mode, auth, username, password string) []opcua.Option { func generateClientOpts(endpoints []*ua.EndpointDescription, certFile, keyFile, policy, mode, auth, username, password string, requestTimeout time.Duration) []opcua.Option {
opts := []opcua.Option{} opts := []opcua.Option{}
appuri := "urn:telegraf:gopcua:client" appuri := "urn:telegraf:gopcua:client"
appname := "Telegraf" appname := "Telegraf"
@ -156,6 +156,8 @@ func generateClientOpts(endpoints []*ua.EndpointDescription, certFile, keyFile,
opts = append(opts, opcua.ApplicationURI(appuri)) opts = append(opts, opcua.ApplicationURI(appuri))
opts = append(opts, opcua.ApplicationName(appname)) opts = append(opts, opcua.ApplicationName(appname))
opts = append(opts, opcua.RequestTimeout(requestTimeout))
if certFile == "" && keyFile == "" { if certFile == "" && keyFile == "" {
if policy != "None" || mode != "None" { if policy != "None" || mode != "None" {
certFile, keyFile = generateCert(appuri, 2048, certFile, keyFile, (365 * 24 * time.Hour)) certFile, keyFile = generateCert(appuri, 2048, certFile, keyFile, (365 * 24 * time.Hour))