Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
108
plugins/inputs/s7comm/README.md
Normal file
108
plugins/inputs/s7comm/README.md
Normal file
|
@ -0,0 +1,108 @@
|
|||
# Siemens S7 Input Plugin
|
||||
|
||||
This plugin reads metrics from Siemens PLCs via the S7 protocol.
|
||||
|
||||
⭐ Telegraf v1.28.0
|
||||
🏷️ hardware
|
||||
💻 all
|
||||
|
||||
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
|
||||
|
||||
In addition to the plugin-specific configuration settings, plugins support
|
||||
additional global and plugin configuration settings. These settings are used to
|
||||
modify metrics, tags, and field or create aliases and configure ordering, etc.
|
||||
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||
|
||||
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
|
||||
|
||||
## Startup error behavior options <!-- @/docs/includes/startup_error_behavior.md -->
|
||||
|
||||
In addition to the plugin-specific and global configuration settings the plugin
|
||||
supports options for specifying the behavior when experiencing startup errors
|
||||
using the `startup_error_behavior` setting. Available values are:
|
||||
|
||||
- `error`: Telegraf with stop and exit in case of startup errors. This is the
|
||||
default behavior.
|
||||
- `ignore`: Telegraf will ignore startup errors for this plugin and disables it
|
||||
but continues processing for all other plugins.
|
||||
- `retry`: Telegraf will try to startup the plugin in every gather or write
|
||||
cycle in case of startup errors. The plugin is disabled until
|
||||
the startup succeeds.
|
||||
- `probe`: Telegraf will probe the plugin's function (if possible) and disables the plugin
|
||||
in case probing fails. If the plugin does not support probing, Telegraf will
|
||||
behave as if `ignore` was set instead.
|
||||
|
||||
## Configuration
|
||||
|
||||
```toml @sample.conf
|
||||
# Plugin for retrieving data from Siemens PLCs via the S7 protocol (RFC1006)
|
||||
[[inputs.s7comm]]
|
||||
## Parameters to contact the PLC (mandatory)
|
||||
## The server is in the <host>[:port] format where the port defaults to 102
|
||||
## if not explicitly specified.
|
||||
server = "127.0.0.1:102"
|
||||
rack = 0
|
||||
slot = 0
|
||||
|
||||
## Connection or drive type of S7 protocol
|
||||
## Available options are "PD" (programming device), "OP" (operator panel) or "basic" (S7 basic communication).
|
||||
# connection_type = "PD"
|
||||
|
||||
## Max count of fields to be bundled in one batch-request. (PDU size)
|
||||
# pdu_size = 20
|
||||
|
||||
## Timeout for requests
|
||||
# timeout = "10s"
|
||||
|
||||
## Log detailed connection messages for tracing issues
|
||||
# log_level = "trace"
|
||||
|
||||
## Metric definition(s)
|
||||
[[inputs.s7comm.metric]]
|
||||
## Name of the measurement
|
||||
# name = "s7comm"
|
||||
|
||||
## Field definitions
|
||||
## name - field name
|
||||
## address - indirect address "<area>.<type><address>[.extra]"
|
||||
## area - e.g. be "DB1" for data-block one
|
||||
## type - supported types are (uppercase)
|
||||
## X -- bit, requires the bit-number as 'extra'
|
||||
## parameter
|
||||
## B -- byte (8 bit)
|
||||
## C -- character (8 bit)
|
||||
## W -- word (16 bit)
|
||||
## DW -- double word (32 bit)
|
||||
## I -- integer (16 bit)
|
||||
## DI -- double integer (32 bit)
|
||||
## R -- IEEE 754 real floating point number (32 bit)
|
||||
## DT -- date-time, always converted to unix timestamp
|
||||
## with nano-second precision
|
||||
## S -- string, requires the maximum length of the
|
||||
## string as 'extra' parameter
|
||||
## address - start address to read if not specified otherwise
|
||||
## in the type field
|
||||
## extra - extra parameter e.g. for the bit and string type
|
||||
fields = [
|
||||
{ name="rpm", address="DB1.R4" },
|
||||
{ name="status_ok", address="DB1.X2.1" },
|
||||
{ name="last_error", address="DB2.S1.32" },
|
||||
{ name="last_error_time", address="DB2.DT2" }
|
||||
]
|
||||
|
||||
## Tags assigned to the metric
|
||||
# [inputs.s7comm.metric.tags]
|
||||
# device = "compressor"
|
||||
# location = "main building"
|
||||
```
|
||||
|
||||
## Example Output
|
||||
|
||||
```text
|
||||
s7comm,host=Hugin rpm=712i,status_ok=true,last_error="empty slot",last_error_time=1611319681000000000i 1611332164000000000
|
||||
```
|
||||
|
||||
## Metrics
|
||||
|
||||
The format of metrics produced by this plugin depends on the metric
|
||||
configuration(s).
|
431
plugins/inputs/s7comm/s7comm.go
Normal file
431
plugins/inputs/s7comm/s7comm.go
Normal file
|
@ -0,0 +1,431 @@
|
|||
//go:generate ../../../tools/readme_config_includer/generator
|
||||
package s7comm
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/maphash"
|
||||
"log" //nolint:depguard // Required for tracing connection issues
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/robinson/gos7"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/internal"
|
||||
"github.com/influxdata/telegraf/metric"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
//go:embed sample.conf
|
||||
var sampleConfig string
|
||||
|
||||
var (
|
||||
regexAddr = regexp.MustCompile(addressRegexp)
|
||||
// Area mapping taken from https://github.com/robinson/gos7/blob/master/client.go
|
||||
areaMap = map[string]int{
|
||||
"PE": 0x81, // process inputs
|
||||
"PA": 0x82, // process outputs
|
||||
"MK": 0x83, // Merkers
|
||||
"DB": 0x84, // DB
|
||||
"C": 0x1C, // counters
|
||||
"T": 0x1D, // timers
|
||||
}
|
||||
// Word-length mapping taken from https://github.com/robinson/gos7/blob/master/client.go
|
||||
wordLenMap = map[string]int{
|
||||
"X": 0x01, // Bit
|
||||
"B": 0x02, // Byte (8 bit)
|
||||
"C": 0x03, // Char (8 bit)
|
||||
"S": 0x03, // String (8 bit)
|
||||
"W": 0x04, // Word (16 bit)
|
||||
"I": 0x05, // Integer (16 bit)
|
||||
"DW": 0x06, // Double Word (32 bit)
|
||||
"DI": 0x07, // Double integer (32 bit)
|
||||
"R": 0x08, // IEEE 754 real (32 bit)
|
||||
// see https://support.industry.siemens.com/cs/document/36479/date_and_time-format-for-s7-?dti=0&lc=en-DE
|
||||
"DT": 0x0F, // Date and time (7 byte)
|
||||
}
|
||||
|
||||
connectionTypeMap = map[string]int{
|
||||
"PD": 1,
|
||||
"OP": 2,
|
||||
"basic": 3,
|
||||
}
|
||||
)
|
||||
|
||||
const addressRegexp = `^(?P<area>[A-Z]+)(?P<no>[0-9]+)\.(?P<type>[A-Z]+)(?P<start>[0-9]+)(?:\.(?P<extra>.*))?$`
|
||||
|
||||
type S7comm struct {
|
||||
Server string `toml:"server"`
|
||||
Rack int `toml:"rack"`
|
||||
Slot int `toml:"slot"`
|
||||
ConnectionType string `toml:"connection_type"`
|
||||
BatchMaxSize int `toml:"pdu_size"`
|
||||
Timeout config.Duration `toml:"timeout"`
|
||||
DebugConnection bool `toml:"debug_connection" deprecated:"1.35.0;use 'log_level' 'trace' instead"`
|
||||
Configs []metricDefinition `toml:"metric"`
|
||||
Log telegraf.Logger `toml:"-"`
|
||||
|
||||
handler *gos7.TCPClientHandler
|
||||
client gos7.Client
|
||||
batches []batch
|
||||
}
|
||||
|
||||
type metricDefinition struct {
|
||||
Name string `toml:"name"`
|
||||
Fields []metricFieldDefinition `toml:"fields"`
|
||||
Tags map[string]string `toml:"tags"`
|
||||
}
|
||||
|
||||
type metricFieldDefinition struct {
|
||||
Name string `toml:"name"`
|
||||
Address string `toml:"address"`
|
||||
}
|
||||
|
||||
type batch struct {
|
||||
items []gos7.S7DataItem
|
||||
mappings []fieldMapping
|
||||
}
|
||||
|
||||
type fieldMapping struct {
|
||||
measurement string
|
||||
field string
|
||||
tags map[string]string
|
||||
convert converterFunc
|
||||
}
|
||||
|
||||
type converterFunc func([]byte) interface{}
|
||||
|
||||
func (*S7comm) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (s *S7comm) Init() error {
|
||||
// Check settings
|
||||
if s.Server == "" {
|
||||
return errors.New("'server' has to be specified")
|
||||
}
|
||||
if s.Rack < 0 {
|
||||
return errors.New("'rack' has to be specified")
|
||||
}
|
||||
if s.Slot < 0 {
|
||||
return errors.New("'slot' has to be specified")
|
||||
}
|
||||
if s.ConnectionType == "" {
|
||||
s.ConnectionType = "PD"
|
||||
}
|
||||
if _, found := connectionTypeMap[s.ConnectionType]; !found {
|
||||
return fmt.Errorf("invalid 'connection_type' %q", s.ConnectionType)
|
||||
}
|
||||
if len(s.Configs) == 0 {
|
||||
return errors.New("no metric defined")
|
||||
}
|
||||
|
||||
// Set default port to 102 if none is given
|
||||
var nerr *net.AddrError
|
||||
if _, _, err := net.SplitHostPort(s.Server); errors.As(err, &nerr) {
|
||||
if !strings.Contains(nerr.Err, "missing port") {
|
||||
return errors.New("invalid 'server' address")
|
||||
}
|
||||
s.Server += ":102"
|
||||
}
|
||||
|
||||
// Create handler for the connection
|
||||
s.handler = gos7.NewTCPClientHandlerWithConnectType(s.Server, s.Rack, s.Slot, connectionTypeMap[s.ConnectionType])
|
||||
s.handler.Timeout = time.Duration(s.Timeout)
|
||||
if s.Log.Level().Includes(telegraf.Trace) || s.DebugConnection { // for backward compatibility
|
||||
s.handler.Logger = log.New(&tracelogger{log: s.Log}, "", 0)
|
||||
}
|
||||
|
||||
// Create the requests
|
||||
return s.createRequests()
|
||||
}
|
||||
|
||||
func (s *S7comm) Start(telegraf.Accumulator) error {
|
||||
s.Log.Debugf("Connecting to %q...", s.Server)
|
||||
if err := s.handler.Connect(); err != nil {
|
||||
return &internal.StartupError{
|
||||
Err: fmt.Errorf("connecting to %q failed: %w", s.Server, err),
|
||||
Retry: true,
|
||||
}
|
||||
}
|
||||
s.client = gos7.NewClient(s.handler)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *S7comm) Gather(acc telegraf.Accumulator) error {
|
||||
timestamp := time.Now()
|
||||
grouper := metric.NewSeriesGrouper()
|
||||
|
||||
for i, b := range s.batches {
|
||||
// Read the batch
|
||||
s.Log.Debugf("Reading batch %d...", i+1)
|
||||
if err := s.client.AGReadMulti(b.items, len(b.items)); err != nil {
|
||||
// Try to reconnect and skip this gather cycle to avoid hammering
|
||||
// the network if the server is down or under load.
|
||||
s.Log.Errorf("reading batch %d failed: %v; reconnecting...", i+1, err)
|
||||
s.Stop()
|
||||
return s.Start(acc)
|
||||
}
|
||||
|
||||
// Dissect the received data into fields
|
||||
for j, m := range b.mappings {
|
||||
// Convert the data
|
||||
buf := b.items[j].Data
|
||||
value := m.convert(buf)
|
||||
s.Log.Debugf(" got %v for field %q @ %d --> %v (%T)", buf, m.field, b.items[j].Start, value, value)
|
||||
|
||||
// Group the data by series
|
||||
grouper.Add(m.measurement, m.tags, timestamp, m.field, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Add the metrics grouped by series to the accumulator
|
||||
for _, x := range grouper.Metrics() {
|
||||
acc.AddMetric(x)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *S7comm) Stop() {
|
||||
if s.handler != nil {
|
||||
s.Log.Debugf("Disconnecting from %q...", s.handler.Address)
|
||||
s.handler.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *S7comm) createRequests() error {
|
||||
seed := maphash.MakeSeed()
|
||||
seenFields := make(map[uint64]bool)
|
||||
s.batches = make([]batch, 0)
|
||||
|
||||
current := batch{}
|
||||
for i, cfg := range s.Configs {
|
||||
// Set the defaults
|
||||
if cfg.Name == "" {
|
||||
cfg.Name = "s7comm"
|
||||
}
|
||||
|
||||
// Check the metric definitions
|
||||
if len(cfg.Fields) == 0 {
|
||||
return fmt.Errorf("no fields defined for metric %q", cfg.Name)
|
||||
}
|
||||
|
||||
// Create requests for all fields and add it to the current slot
|
||||
for _, f := range cfg.Fields {
|
||||
if f.Name == "" {
|
||||
return fmt.Errorf("unnamed field in metric %q", cfg.Name)
|
||||
}
|
||||
|
||||
item, cfunc, err := handleFieldAddress(f.Address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("field %q of metric %q: %w", f.Name, cfg.Name, err)
|
||||
}
|
||||
m := fieldMapping{
|
||||
measurement: cfg.Name,
|
||||
field: f.Name,
|
||||
tags: s.Configs[i].Tags,
|
||||
convert: cfunc,
|
||||
}
|
||||
current.items = append(current.items, *item)
|
||||
current.mappings = append(current.mappings, m)
|
||||
|
||||
// If the batch is full, start a new one
|
||||
if len(current.items) == s.BatchMaxSize {
|
||||
s.batches = append(s.batches, current)
|
||||
current = batch{}
|
||||
}
|
||||
|
||||
// Check for duplicate field definitions
|
||||
id := fieldID(seed, cfg, f)
|
||||
if seenFields[id] {
|
||||
return fmt.Errorf("duplicate field definition field %q in metric %q", f.Name, cfg.Name)
|
||||
}
|
||||
seenFields[id] = true
|
||||
}
|
||||
|
||||
// Update the configuration if changed
|
||||
s.Configs[i] = cfg
|
||||
}
|
||||
|
||||
// Add the last batch if any
|
||||
if len(current.items) > 0 {
|
||||
s.batches = append(s.batches, current)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleFieldAddress(address string) (*gos7.S7DataItem, converterFunc, error) {
|
||||
// Parse the address into the different parts
|
||||
if !regexAddr.MatchString(address) {
|
||||
return nil, nil, fmt.Errorf("invalid address %q", address)
|
||||
}
|
||||
names := regexAddr.SubexpNames()[1:]
|
||||
parts := regexAddr.FindStringSubmatch(address)[1:]
|
||||
if len(names) != len(parts) {
|
||||
return nil, nil, fmt.Errorf("names %v do not match parts %v", names, parts)
|
||||
}
|
||||
groups := make(map[string]string, len(names))
|
||||
for i, n := range names {
|
||||
groups[n] = parts[i]
|
||||
}
|
||||
|
||||
// Check that we do have the required entries in the address
|
||||
if _, found := groups["area"]; !found {
|
||||
return nil, nil, errors.New("area is missing from address")
|
||||
}
|
||||
|
||||
if _, found := groups["no"]; !found {
|
||||
return nil, nil, errors.New("area index is missing from address")
|
||||
}
|
||||
if _, found := groups["type"]; !found {
|
||||
return nil, nil, errors.New("type is missing from address")
|
||||
}
|
||||
if _, found := groups["start"]; !found {
|
||||
return nil, nil, errors.New("start address is missing from address")
|
||||
}
|
||||
dtype := groups["type"]
|
||||
|
||||
// Lookup the item values from names and check the params
|
||||
area, found := areaMap[groups["area"]]
|
||||
if !found {
|
||||
return nil, nil, errors.New("invalid area")
|
||||
}
|
||||
wordlen, found := wordLenMap[dtype]
|
||||
if !found {
|
||||
return nil, nil, errors.New("unknown data type")
|
||||
}
|
||||
areaidx, err := strconv.Atoi(groups["no"])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid area index: %w", err)
|
||||
}
|
||||
start, err := strconv.Atoi(groups["start"])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid start address: %w", err)
|
||||
}
|
||||
|
||||
// Check the amount parameter if any
|
||||
var extra, bit int
|
||||
switch dtype {
|
||||
case "S":
|
||||
// We require an extra parameter
|
||||
x := groups["extra"]
|
||||
if x == "" {
|
||||
return nil, nil, errors.New("extra parameter required")
|
||||
}
|
||||
|
||||
extra, err = strconv.Atoi(x)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid extra parameter: %w", err)
|
||||
}
|
||||
if extra < 1 {
|
||||
return nil, nil, fmt.Errorf("invalid extra parameter %d", extra)
|
||||
}
|
||||
case "X":
|
||||
// We require an extra parameter
|
||||
x := groups["extra"]
|
||||
if x == "" {
|
||||
return nil, nil, errors.New("extra parameter required")
|
||||
}
|
||||
|
||||
bit, err = strconv.Atoi(x)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid extra parameter: %w", err)
|
||||
}
|
||||
if bit < 0 || bit > 7 {
|
||||
// Ensure bit address is valid
|
||||
return nil, nil, fmt.Errorf("invalid extra parameter: bit address %d out of range", bit)
|
||||
}
|
||||
default:
|
||||
if groups["extra"] != "" {
|
||||
return nil, nil, errors.New("extra parameter specified but not used")
|
||||
}
|
||||
}
|
||||
|
||||
// Get the required buffer size
|
||||
amount := 1
|
||||
var buflen int
|
||||
switch dtype {
|
||||
case "X", "B", "C": // 8-bit types
|
||||
buflen = 1
|
||||
case "W", "I": // 16-bit types
|
||||
buflen = 2
|
||||
case "DW", "DI", "R": // 32-bit types
|
||||
buflen = 4
|
||||
case "DT": // 7-byte
|
||||
buflen = 7
|
||||
case "S":
|
||||
amount = extra
|
||||
// Extra bytes as the first byte is the max-length of the string and
|
||||
// the second byte is the actual length of the string.
|
||||
buflen = extra + 2
|
||||
default:
|
||||
return nil, nil, errors.New("invalid data type")
|
||||
}
|
||||
|
||||
// Setup the data item
|
||||
item := &gos7.S7DataItem{
|
||||
Area: area,
|
||||
WordLen: wordlen,
|
||||
Bit: bit,
|
||||
DBNumber: areaidx,
|
||||
Start: start,
|
||||
Amount: amount,
|
||||
Data: make([]byte, buflen),
|
||||
}
|
||||
|
||||
// Determine the type converter function
|
||||
f := determineConversion(dtype)
|
||||
return item, f, nil
|
||||
}
|
||||
|
||||
func fieldID(seed maphash.Seed, def metricDefinition, field metricFieldDefinition) uint64 {
|
||||
var mh maphash.Hash
|
||||
mh.SetSeed(seed)
|
||||
|
||||
mh.WriteString(def.Name)
|
||||
mh.WriteByte(0)
|
||||
mh.WriteString(field.Name)
|
||||
mh.WriteByte(0)
|
||||
|
||||
// Tags
|
||||
for k, v := range def.Tags {
|
||||
mh.WriteString(k)
|
||||
mh.WriteByte('=')
|
||||
mh.WriteString(v)
|
||||
mh.WriteByte(':')
|
||||
}
|
||||
mh.WriteByte(0)
|
||||
|
||||
return mh.Sum64()
|
||||
}
|
||||
|
||||
// Logger for tracing internal messages
|
||||
type tracelogger struct {
|
||||
log telegraf.Logger
|
||||
}
|
||||
|
||||
func (l *tracelogger) Write(b []byte) (n int, err error) {
|
||||
l.log.Trace(string(b))
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
// Add this plugin to telegraf
|
||||
func init() {
|
||||
inputs.Add("s7comm", func() telegraf.Input {
|
||||
return &S7comm{
|
||||
Rack: -1,
|
||||
Slot: -1,
|
||||
BatchMaxSize: 20,
|
||||
Timeout: config.Duration(10 * time.Second),
|
||||
}
|
||||
})
|
||||
}
|
963
plugins/inputs/s7comm/s7comm_test.go
Normal file
963
plugins/inputs/s7comm/s7comm_test.go
Normal file
|
@ -0,0 +1,963 @@
|
|||
package s7comm
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/robinson/gos7"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/internal"
|
||||
"github.com/influxdata/telegraf/models"
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
)
|
||||
|
||||
func TestSampleConfig(t *testing.T) {
|
||||
plugin := &S7comm{}
|
||||
require.NotEmpty(t, plugin.SampleConfig())
|
||||
}
|
||||
|
||||
func TestInitFail(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
server string
|
||||
rack int
|
||||
slot int
|
||||
configs []metricDefinition
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "empty settings",
|
||||
rack: -1, // This is the default in `init()`
|
||||
slot: -1, // This is the default in `init()`
|
||||
expectedError: "'server' has to be specified",
|
||||
},
|
||||
{
|
||||
name: "missing rack",
|
||||
server: "127.0.0.1:102",
|
||||
rack: -1, // This is the default in `init()`
|
||||
slot: -1, // This is the default in `init()`
|
||||
expectedError: "'rack' has to be specified",
|
||||
},
|
||||
{
|
||||
name: "missing slot",
|
||||
server: "127.0.0.1:102",
|
||||
rack: 0,
|
||||
slot: -1, // This is the default in `init()`
|
||||
expectedError: "'slot' has to be specified",
|
||||
},
|
||||
{
|
||||
name: "missing configs",
|
||||
server: "127.0.0.1:102",
|
||||
expectedError: "no metric defined",
|
||||
},
|
||||
{
|
||||
name: "single empty metric",
|
||||
server: "127.0.0.1:102",
|
||||
configs: []metricDefinition{{}},
|
||||
expectedError: "no fields defined for metric",
|
||||
},
|
||||
{
|
||||
name: "single empty metric field",
|
||||
server: "127.0.0.1:102",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{{}},
|
||||
},
|
||||
},
|
||||
expectedError: "unnamed field in metric",
|
||||
},
|
||||
{
|
||||
name: "no address",
|
||||
server: "127.0.0.1:102",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "invalid address",
|
||||
},
|
||||
{
|
||||
name: "invalid address pattern",
|
||||
server: "127.0.0.1:102",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "FOO",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "invalid address",
|
||||
},
|
||||
{
|
||||
name: "invalid address area",
|
||||
server: "127.0.0.1:102",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "FOO1.W2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "invalid area",
|
||||
},
|
||||
{
|
||||
name: "invalid address area index",
|
||||
server: "127.0.0.1:102",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB.W2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "invalid address",
|
||||
},
|
||||
{
|
||||
name: "invalid address type",
|
||||
server: "127.0.0.1:102",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.A2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "unknown data type",
|
||||
},
|
||||
{
|
||||
name: "invalid address start",
|
||||
server: "127.0.0.1:102",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.A",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "invalid address",
|
||||
},
|
||||
{
|
||||
name: "missing extra parameter bit",
|
||||
server: "127.0.0.1:102",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.X1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "extra parameter required",
|
||||
},
|
||||
{
|
||||
name: "missing extra parameter string",
|
||||
server: "127.0.0.1:102",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.S1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "extra parameter required",
|
||||
},
|
||||
{
|
||||
name: "invalid address extra parameter",
|
||||
server: "127.0.0.1:102",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.W1.23",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "extra parameter specified but not used",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
plugin := &S7comm{
|
||||
Server: tt.server,
|
||||
Rack: tt.rack,
|
||||
Slot: tt.slot,
|
||||
Configs: tt.configs,
|
||||
Log: &testutil.Logger{},
|
||||
}
|
||||
require.ErrorContains(t, plugin.Init(), tt.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
plugin := &S7comm{
|
||||
Server: "127.0.0.1:102",
|
||||
Rack: 0,
|
||||
Slot: 0,
|
||||
Configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.W2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Log: &testutil.Logger{},
|
||||
}
|
||||
require.NoError(t, plugin.Init())
|
||||
}
|
||||
|
||||
func TestFieldMappings(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
configs []metricDefinition
|
||||
expected []batch
|
||||
}{
|
||||
{
|
||||
name: "single field bit",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Name: "test",
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB5.X3.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []batch{
|
||||
{
|
||||
items: []gos7.S7DataItem{
|
||||
{
|
||||
Area: 0x84,
|
||||
WordLen: 0x01,
|
||||
Bit: 2,
|
||||
DBNumber: 5,
|
||||
Start: 3,
|
||||
Amount: 1,
|
||||
Data: make([]byte, 1),
|
||||
},
|
||||
},
|
||||
mappings: []fieldMapping{
|
||||
{
|
||||
measurement: "test",
|
||||
field: "foo",
|
||||
convert: func([]byte) interface{} { return false },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single field byte",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Name: "test",
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB5.B3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []batch{
|
||||
{
|
||||
items: []gos7.S7DataItem{
|
||||
{
|
||||
Area: 0x84,
|
||||
WordLen: 0x02,
|
||||
DBNumber: 5,
|
||||
Start: 3,
|
||||
Amount: 1,
|
||||
Data: make([]byte, 1),
|
||||
},
|
||||
},
|
||||
mappings: []fieldMapping{
|
||||
{
|
||||
measurement: "test",
|
||||
field: "foo",
|
||||
convert: func([]byte) interface{} { return byte(0) },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single field char",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Name: "test",
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB5.C3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []batch{
|
||||
{
|
||||
items: []gos7.S7DataItem{
|
||||
{
|
||||
Area: 0x84,
|
||||
WordLen: 0x03,
|
||||
DBNumber: 5,
|
||||
Start: 3,
|
||||
Amount: 1,
|
||||
Data: make([]byte, 1),
|
||||
},
|
||||
},
|
||||
mappings: []fieldMapping{
|
||||
{
|
||||
measurement: "test",
|
||||
field: "foo",
|
||||
convert: func([]byte) interface{} { return string([]byte{0}) },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single field string",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Name: "test",
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB5.S3.10",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []batch{
|
||||
{
|
||||
items: []gos7.S7DataItem{
|
||||
{
|
||||
Area: 0x84,
|
||||
WordLen: 0x03,
|
||||
DBNumber: 5,
|
||||
Start: 3,
|
||||
Amount: 10,
|
||||
Data: make([]byte, 12),
|
||||
},
|
||||
},
|
||||
mappings: []fieldMapping{
|
||||
{
|
||||
measurement: "test",
|
||||
field: "foo",
|
||||
convert: func([]byte) interface{} { return "" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single field word",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Name: "test",
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB5.W3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []batch{
|
||||
{
|
||||
items: []gos7.S7DataItem{
|
||||
{
|
||||
Area: 0x84,
|
||||
WordLen: 0x04,
|
||||
DBNumber: 5,
|
||||
Start: 3,
|
||||
Amount: 1,
|
||||
Data: make([]byte, 2),
|
||||
},
|
||||
},
|
||||
mappings: []fieldMapping{
|
||||
{
|
||||
measurement: "test",
|
||||
field: "foo",
|
||||
convert: func([]byte) interface{} { return uint16(0) },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single field integer",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Name: "test",
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB5.I3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []batch{
|
||||
{
|
||||
items: []gos7.S7DataItem{
|
||||
{
|
||||
Area: 0x84,
|
||||
WordLen: 0x05,
|
||||
DBNumber: 5,
|
||||
Start: 3,
|
||||
Amount: 1,
|
||||
Data: make([]byte, 2),
|
||||
},
|
||||
},
|
||||
mappings: []fieldMapping{
|
||||
{
|
||||
measurement: "test",
|
||||
field: "foo",
|
||||
convert: func([]byte) interface{} { return int16(0) },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single field double word",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Name: "test",
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB5.DW3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []batch{
|
||||
{
|
||||
items: []gos7.S7DataItem{
|
||||
{
|
||||
Area: 0x84,
|
||||
WordLen: 0x06,
|
||||
DBNumber: 5,
|
||||
Start: 3,
|
||||
Amount: 1,
|
||||
Data: make([]byte, 4),
|
||||
},
|
||||
},
|
||||
mappings: []fieldMapping{
|
||||
{
|
||||
measurement: "test",
|
||||
field: "foo",
|
||||
convert: func([]byte) interface{} { return uint32(0) },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single field double integer",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Name: "test",
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB5.DI3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []batch{
|
||||
{
|
||||
items: []gos7.S7DataItem{
|
||||
{
|
||||
Area: 0x84,
|
||||
WordLen: 0x07,
|
||||
DBNumber: 5,
|
||||
Start: 3,
|
||||
Amount: 1,
|
||||
Data: make([]byte, 4),
|
||||
},
|
||||
},
|
||||
mappings: []fieldMapping{
|
||||
{
|
||||
measurement: "test",
|
||||
field: "foo",
|
||||
convert: func([]byte) interface{} { return int32(0) },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single field float",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Name: "test",
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB5.R3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []batch{
|
||||
{
|
||||
items: []gos7.S7DataItem{
|
||||
{
|
||||
Area: 0x84,
|
||||
WordLen: 0x08,
|
||||
DBNumber: 5,
|
||||
Start: 3,
|
||||
Amount: 1,
|
||||
Data: make([]byte, 4),
|
||||
},
|
||||
},
|
||||
mappings: []fieldMapping{
|
||||
{
|
||||
measurement: "test",
|
||||
field: "foo",
|
||||
convert: func([]byte) interface{} { return float32(0) },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
plugin := &S7comm{
|
||||
Server: "127.0.0.1:102",
|
||||
Rack: 0,
|
||||
Slot: 2,
|
||||
Configs: tt.configs,
|
||||
Log: &testutil.Logger{},
|
||||
}
|
||||
require.NoError(t, plugin.Init())
|
||||
|
||||
// Check the length
|
||||
require.Len(t, plugin.batches, len(tt.expected))
|
||||
// Check the actual content
|
||||
for i, eb := range tt.expected {
|
||||
ab := plugin.batches[i]
|
||||
require.Len(t, ab.items, len(eb.items))
|
||||
require.Len(t, ab.mappings, len(eb.mappings))
|
||||
require.EqualValues(t, eb.items, plugin.batches[i].items, "different items")
|
||||
for j, em := range eb.mappings {
|
||||
am := ab.mappings[j]
|
||||
require.Equal(t, em.measurement, am.measurement)
|
||||
require.Equal(t, em.field, am.field)
|
||||
buf := ab.items[j].Data
|
||||
require.Equal(t, em.convert(buf), am.convert(buf))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricCollisions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
configs []metricDefinition
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "duplicate fields same config",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.W1",
|
||||
},
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.B1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "duplicate field definition",
|
||||
},
|
||||
{
|
||||
name: "duplicate fields different config",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.B1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.B1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "duplicate field definition",
|
||||
},
|
||||
{
|
||||
name: "same fields different name",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.B1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "bar",
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.B1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "same fields different tags",
|
||||
configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.B1",
|
||||
},
|
||||
},
|
||||
Tags: map[string]string{"device": "foo"},
|
||||
},
|
||||
{
|
||||
Name: "bar",
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.B1",
|
||||
},
|
||||
},
|
||||
Tags: map[string]string{"device": "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
plugin := &S7comm{
|
||||
Server: "127.0.0.1:102",
|
||||
Rack: 0,
|
||||
Slot: 2,
|
||||
Configs: tt.configs,
|
||||
Log: &testutil.Logger{},
|
||||
}
|
||||
err := plugin.Init()
|
||||
if tt.expectedError != "" {
|
||||
require.ErrorContains(t, err, tt.expectedError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionLoss(t *testing.T) {
|
||||
// Create fake S7 comm server that can accept connects
|
||||
server, err := newMockServer()
|
||||
require.NoError(t, err)
|
||||
defer server.close()
|
||||
server.start()
|
||||
|
||||
// Create the plugin and attempt a connection
|
||||
plugin := &S7comm{
|
||||
Server: server.addr(),
|
||||
Rack: 0,
|
||||
Slot: 2,
|
||||
DebugConnection: true,
|
||||
Timeout: config.Duration(100 * time.Millisecond),
|
||||
Configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.W2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Log: &testutil.Logger{},
|
||||
}
|
||||
require.NoError(t, plugin.Init())
|
||||
|
||||
var acc testutil.Accumulator
|
||||
require.NoError(t, plugin.Start(&acc))
|
||||
require.NoError(t, plugin.Gather(&acc))
|
||||
require.NoError(t, plugin.Gather(&acc))
|
||||
plugin.Stop()
|
||||
server.close()
|
||||
|
||||
require.Equal(t, uint32(3), server.connectionAttempts.Load())
|
||||
}
|
||||
|
||||
func TestStartupErrorBehaviorError(t *testing.T) {
|
||||
// Create fake S7 comm server that can accept connects
|
||||
server, err := newMockServer()
|
||||
require.NoError(t, err)
|
||||
defer server.close()
|
||||
|
||||
// Setup the plugin and the model to be able to use the startup retry strategy
|
||||
plugin := &S7comm{
|
||||
Server: server.addr(),
|
||||
Rack: 0,
|
||||
Slot: 2,
|
||||
DebugConnection: true,
|
||||
Timeout: config.Duration(100 * time.Millisecond),
|
||||
Configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.W2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Log: &testutil.Logger{},
|
||||
}
|
||||
model := models.NewRunningInput(
|
||||
plugin,
|
||||
&models.InputConfig{
|
||||
Name: "s7comm",
|
||||
Alias: "error-test", // required to get a unique error stats instance
|
||||
},
|
||||
)
|
||||
model.StartupErrors.Set(0)
|
||||
require.NoError(t, model.Init())
|
||||
|
||||
// Starting the plugin will fail with an error because the server does not listen
|
||||
var acc testutil.Accumulator
|
||||
require.ErrorContains(t, model.Start(&acc), "connecting to \""+server.addr()+"\" failed")
|
||||
}
|
||||
|
||||
func TestStartupErrorBehaviorIgnore(t *testing.T) {
|
||||
// Create fake S7 comm server that can accept connects
|
||||
server, err := newMockServer()
|
||||
require.NoError(t, err)
|
||||
defer server.close()
|
||||
|
||||
// Setup the plugin and the model to be able to use the startup retry strategy
|
||||
plugin := &S7comm{
|
||||
Server: server.addr(),
|
||||
Rack: 0,
|
||||
Slot: 2,
|
||||
DebugConnection: true,
|
||||
Timeout: config.Duration(100 * time.Millisecond),
|
||||
Configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.W2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Log: &testutil.Logger{},
|
||||
}
|
||||
model := models.NewRunningInput(
|
||||
plugin,
|
||||
&models.InputConfig{
|
||||
Name: "s7comm",
|
||||
Alias: "ignore-test", // required to get a unique error stats instance
|
||||
StartupErrorBehavior: "ignore",
|
||||
},
|
||||
)
|
||||
model.StartupErrors.Set(0)
|
||||
require.NoError(t, model.Init())
|
||||
|
||||
// Starting the plugin will fail because the server does not accept connections.
|
||||
// The model code should convert it to a fatal error for the agent to remove
|
||||
// the plugin.
|
||||
var acc testutil.Accumulator
|
||||
err = model.Start(&acc)
|
||||
require.ErrorContains(t, err, "connecting to \""+server.addr()+"\" failed")
|
||||
var fatalErr *internal.FatalError
|
||||
require.ErrorAs(t, err, &fatalErr)
|
||||
}
|
||||
|
||||
func TestStartupErrorBehaviorRetry(t *testing.T) {
|
||||
// Create fake S7 comm server that can accept connects
|
||||
server, err := newMockServer()
|
||||
require.NoError(t, err)
|
||||
defer server.close()
|
||||
|
||||
// Setup the plugin and the model to be able to use the startup retry strategy
|
||||
plugin := &S7comm{
|
||||
Server: server.addr(),
|
||||
Rack: 0,
|
||||
Slot: 2,
|
||||
DebugConnection: true,
|
||||
Timeout: config.Duration(100 * time.Millisecond),
|
||||
Configs: []metricDefinition{
|
||||
{
|
||||
Fields: []metricFieldDefinition{
|
||||
{
|
||||
Name: "foo",
|
||||
Address: "DB1.W2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Log: &testutil.Logger{},
|
||||
}
|
||||
model := models.NewRunningInput(
|
||||
plugin,
|
||||
&models.InputConfig{
|
||||
Name: "s7comm",
|
||||
Alias: "retry-test", // required to get a unique error stats instance
|
||||
StartupErrorBehavior: "retry",
|
||||
},
|
||||
)
|
||||
model.StartupErrors.Set(0)
|
||||
require.NoError(t, model.Init())
|
||||
|
||||
// Starting the plugin will return no error because the plugin will
|
||||
// retry to connect in every gather cycle.
|
||||
var acc testutil.Accumulator
|
||||
require.NoError(t, model.Start(&acc))
|
||||
|
||||
// The gather should fail as the server does not accept connections (yet)
|
||||
require.Empty(t, acc.GetTelegrafMetrics())
|
||||
require.ErrorIs(t, model.Gather(&acc), internal.ErrNotConnected)
|
||||
require.Equal(t, int64(2), model.StartupErrors.Get())
|
||||
|
||||
// Allow connection in the server, now the connection should succeed
|
||||
server.start()
|
||||
defer model.Stop()
|
||||
require.NoError(t, model.Gather(&acc))
|
||||
}
|
||||
|
||||
type mockServer struct {
|
||||
connectionAttempts atomic.Uint32
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
func newMockServer() (*mockServer, error) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mockServer{listener: l}, nil
|
||||
}
|
||||
|
||||
func (s *mockServer) addr() string {
|
||||
return s.listener.Addr().String()
|
||||
}
|
||||
|
||||
func (s *mockServer) close() error {
|
||||
if s.listener != nil {
|
||||
return s.listener.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockServer) start() {
|
||||
go func() {
|
||||
defer s.listener.Close()
|
||||
for {
|
||||
conn, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := conn.SetDeadline(time.Now().Add(time.Second)); err != nil {
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Count the number of connection attempts
|
||||
s.connectionAttempts.Add(1)
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
|
||||
// Wait for ISO connection telegram
|
||||
if _, err := io.ReadAtLeast(conn, buf, 22); err != nil {
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Send fake response
|
||||
response := make([]byte, 22)
|
||||
response[5] = 0xD0
|
||||
binary.BigEndian.PutUint16(response[2:4], uint16(len(response)))
|
||||
if _, err := conn.Write(response); err != nil {
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for PDU negotiation telegram
|
||||
if _, err := io.ReadAtLeast(conn, buf, 25); err != nil {
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Send fake response
|
||||
response = make([]byte, 27)
|
||||
binary.BigEndian.PutUint16(response[2:4], uint16(len(response)))
|
||||
binary.BigEndian.PutUint16(response[25:27], uint16(480))
|
||||
if _, err := conn.Write(response); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Always close after connection is established
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
}
|
59
plugins/inputs/s7comm/sample.conf
Normal file
59
plugins/inputs/s7comm/sample.conf
Normal file
|
@ -0,0 +1,59 @@
|
|||
# Plugin for retrieving data from Siemens PLCs via the S7 protocol (RFC1006)
|
||||
[[inputs.s7comm]]
|
||||
## Parameters to contact the PLC (mandatory)
|
||||
## The server is in the <host>[:port] format where the port defaults to 102
|
||||
## if not explicitly specified.
|
||||
server = "127.0.0.1:102"
|
||||
rack = 0
|
||||
slot = 0
|
||||
|
||||
## Connection or drive type of S7 protocol
|
||||
## Available options are "PD" (programming device), "OP" (operator panel) or "basic" (S7 basic communication).
|
||||
# connection_type = "PD"
|
||||
|
||||
## Max count of fields to be bundled in one batch-request. (PDU size)
|
||||
# pdu_size = 20
|
||||
|
||||
## Timeout for requests
|
||||
# timeout = "10s"
|
||||
|
||||
## Log detailed connection messages for tracing issues
|
||||
# log_level = "trace"
|
||||
|
||||
## Metric definition(s)
|
||||
[[inputs.s7comm.metric]]
|
||||
## Name of the measurement
|
||||
# name = "s7comm"
|
||||
|
||||
## Field definitions
|
||||
## name - field name
|
||||
## address - indirect address "<area>.<type><address>[.extra]"
|
||||
## area - e.g. be "DB1" for data-block one
|
||||
## type - supported types are (uppercase)
|
||||
## X -- bit, requires the bit-number as 'extra'
|
||||
## parameter
|
||||
## B -- byte (8 bit)
|
||||
## C -- character (8 bit)
|
||||
## W -- word (16 bit)
|
||||
## DW -- double word (32 bit)
|
||||
## I -- integer (16 bit)
|
||||
## DI -- double integer (32 bit)
|
||||
## R -- IEEE 754 real floating point number (32 bit)
|
||||
## DT -- date-time, always converted to unix timestamp
|
||||
## with nano-second precision
|
||||
## S -- string, requires the maximum length of the
|
||||
## string as 'extra' parameter
|
||||
## address - start address to read if not specified otherwise
|
||||
## in the type field
|
||||
## extra - extra parameter e.g. for the bit and string type
|
||||
fields = [
|
||||
{ name="rpm", address="DB1.R4" },
|
||||
{ name="status_ok", address="DB1.X2.1" },
|
||||
{ name="last_error", address="DB2.S1.32" },
|
||||
{ name="last_error_time", address="DB2.DT2" }
|
||||
]
|
||||
|
||||
## Tags assigned to the metric
|
||||
# [inputs.s7comm.metric.tags]
|
||||
# device = "compressor"
|
||||
# location = "main building"
|
67
plugins/inputs/s7comm/type_conversions.go
Normal file
67
plugins/inputs/s7comm/type_conversions.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package s7comm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math"
|
||||
|
||||
"github.com/robinson/gos7"
|
||||
)
|
||||
|
||||
var helper = &gos7.Helper{}
|
||||
|
||||
func determineConversion(dtype string) converterFunc {
|
||||
switch dtype {
|
||||
case "X":
|
||||
return func(buf []byte) interface{} {
|
||||
return buf[0] != 0
|
||||
}
|
||||
case "B":
|
||||
return func(buf []byte) interface{} {
|
||||
return buf[0]
|
||||
}
|
||||
case "C":
|
||||
return func(buf []byte) interface{} {
|
||||
return string(buf[0])
|
||||
}
|
||||
case "S":
|
||||
return func(buf []byte) interface{} {
|
||||
if len(buf) <= 2 {
|
||||
return ""
|
||||
}
|
||||
// Get the length of the encoded string
|
||||
length := int(buf[1])
|
||||
// Clip the string if we do not fill the whole buffer
|
||||
if length < len(buf)-2 {
|
||||
return string(buf[2 : 2+length])
|
||||
}
|
||||
return string(buf[2:])
|
||||
}
|
||||
case "W":
|
||||
return func(buf []byte) interface{} {
|
||||
return binary.BigEndian.Uint16(buf)
|
||||
}
|
||||
case "I":
|
||||
return func(buf []byte) interface{} {
|
||||
return int16(binary.BigEndian.Uint16(buf))
|
||||
}
|
||||
case "DW":
|
||||
return func(buf []byte) interface{} {
|
||||
return binary.BigEndian.Uint32(buf)
|
||||
}
|
||||
case "DI":
|
||||
return func(buf []byte) interface{} {
|
||||
return int32(binary.BigEndian.Uint32(buf))
|
||||
}
|
||||
case "R":
|
||||
return func(buf []byte) interface{} {
|
||||
x := binary.BigEndian.Uint32(buf)
|
||||
return math.Float32frombits(x)
|
||||
}
|
||||
case "DT":
|
||||
return func(buf []byte) interface{} {
|
||||
return helper.GetDateTimeAt(buf, 0).UnixNano()
|
||||
}
|
||||
}
|
||||
|
||||
panic("Unknown type! Please file an issue on https://github.com/influxdata/telegraf including your config.")
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue