From 664025d60c3cd0eeca3da5dad7aba8c6b0a7905f Mon Sep 17 00:00:00 2001 From: Alex Schmitz Date: Sun, 11 Aug 2019 22:52:16 -0500 Subject: [PATCH] Scrape cooling_device state Signed-off-by: Alex Schmitz --- CHANGELOG.md | 1 + README.md | 1 + collector/fixtures/e2e-64k-page-output.txt | 6 + collector/fixtures/e2e-output.txt | 6 + collector/fixtures/sys.ttar | 21 +++ collector/thermal_zone_linux.go | 44 +++++- vendor/github.com/prometheus/procfs/arp.go | 85 ++++++++++++ vendor/github.com/prometheus/procfs/crypto.go | 131 ++++++++++++++++++ .../prometheus/procfs/net_softnet.go | 91 ++++++++++++ .../procfs/sysfs/class_cooling_device.go | 90 ++++++++++++ .../prometheus/procfs/sysfs/vulnerability.go | 84 +++++++++++ 11 files changed, 557 insertions(+), 3 deletions(-) create mode 100644 vendor/github.com/prometheus/procfs/arp.go create mode 100644 vendor/github.com/prometheus/procfs/crypto.go create mode 100644 vendor/github.com/prometheus/procfs/net_softnet.go create mode 100644 vendor/github.com/prometheus/procfs/sysfs/class_cooling_device.go create mode 100644 vendor/github.com/prometheus/procfs/sysfs/vulnerability.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ecffe9ab..c6257b88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ * [BUGFIX] Fix netdev nil reference on Darwin #1414 * [BUGFIX] Strip path.rootfs from mountpoint labels #1421 * [FEATURE] Add new thermal_zone collector #1425 +* [FEATURE] Add new cooling_device metrics to thermal zone collector #1445 ## 0.18.1 / 2019-06-04 diff --git a/README.md b/README.md index f38bcd01..7a4b9f40 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ schedstat | Exposes task scheduler statistics from `/proc/schedstat`. | Linux sockstat | Exposes various statistics from `/proc/net/sockstat`. | Linux stat | Exposes various statistics from `/proc/stat`. This includes boot time, forks and interrupts. | Linux textfile | Exposes statistics read from local disk. The `--collector.textfile.directory` flag must be set. | _any_ +thermal\_zone | Exposes thermal zone & cooling device statistics from `/sys/class/thermal`. | Linux time | Exposes the current system time. | _any_ timex | Exposes selected adjtimex(2) system call stats. | Linux uname | Exposes system information as provided by the uname system call. | Darwin, FreeBSD, Linux, OpenBSD diff --git a/collector/fixtures/e2e-64k-page-output.txt b/collector/fixtures/e2e-64k-page-output.txt index 2737a01c..6b2e6bd2 100644 --- a/collector/fixtures/e2e-64k-page-output.txt +++ b/collector/fixtures/e2e-64k-page-output.txt @@ -178,6 +178,12 @@ node_buddyinfo_blocks{node="0",size="9",zone="Normal"} 0 # HELP node_context_switches_total Total number of context switches. # TYPE node_context_switches_total counter node_context_switches_total 3.8014093e+07 +# HELP node_cooling_device_cur_state Current throttle state of the cooling device +# TYPE node_cooling_device_cur_state gauge +node_cooling_device_cur_state{name="0",type="Processor"} 0 +# HELP node_cooling_device_max_state Maximum throttle state of the cooling device +# TYPE node_cooling_device_max_state gauge +node_cooling_device_max_state{name="0",type="Processor"} 3 # HELP node_cpu_core_throttles_total Number of times this cpu core has been throttled. # TYPE node_cpu_core_throttles_total counter node_cpu_core_throttles_total{core="0",package="0"} 5 diff --git a/collector/fixtures/e2e-output.txt b/collector/fixtures/e2e-output.txt index 0a3291dc..7651f53b 100644 --- a/collector/fixtures/e2e-output.txt +++ b/collector/fixtures/e2e-output.txt @@ -178,6 +178,12 @@ node_buddyinfo_blocks{node="0",size="9",zone="Normal"} 0 # HELP node_context_switches_total Total number of context switches. # TYPE node_context_switches_total counter node_context_switches_total 3.8014093e+07 +# HELP node_cooling_device_cur_state Current throttle state of the cooling device +# TYPE node_cooling_device_cur_state gauge +node_cooling_device_cur_state{name="0",type="Processor"} 0 +# HELP node_cooling_device_max_state Maximum throttle state of the cooling device +# TYPE node_cooling_device_max_state gauge +node_cooling_device_max_state{name="0",type="Processor"} 3 # HELP node_cpu_core_throttles_total Number of times this cpu core has been throttled. # TYPE node_cpu_core_throttles_total counter node_cpu_core_throttles_total{core="0",package="0"} 5 diff --git a/collector/fixtures/sys.ttar b/collector/fixtures/sys.ttar index 2880390e..41e062da 100644 --- a/collector/fixtures/sys.ttar +++ b/collector/fixtures/sys.ttar @@ -926,6 +926,9 @@ Mode: 755 Path: sys/class/thermal/thermal_zone0 SymlinkTo: ../../devices/virtual/thermal/thermal_zone0 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: sys/class/thermal/cooling_device0 +SymlinkTo: ../../devices/virtual/thermal/cooling_device0 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Directory: sys/devices Mode: 755 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -2472,6 +2475,24 @@ Mode: 755 Directory: sys/devices/virtual/thermal Mode: 755 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: sys/devices/virtual/thermal/cooling_device0 +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: sys/devices/virtual/thermal/cooling_device0/cur_state +Lines: 1 +0 +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: sys/devices/virtual/thermal/cooling_device0/max_state +Lines: 1 +3 +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: sys/devices/virtual/thermal/cooling_device0/type +Lines: 1 +Processor +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Directory: sys/devices/virtual/thermal/thermal_zone0 Mode: 755 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/collector/thermal_zone_linux.go b/collector/thermal_zone_linux.go index 16fe61c6..13606daa 100644 --- a/collector/thermal_zone_linux.go +++ b/collector/thermal_zone_linux.go @@ -22,9 +22,14 @@ import ( "github.com/prometheus/procfs/sysfs" ) +const coolingDevice = "cooling_device" +const thermalZone = "thermal_zone" + type thermalZoneCollector struct { - fs sysfs.FS - zoneTemp *prometheus.Desc + fs sysfs.FS + coolingDeviceCurState *prometheus.Desc + coolingDeviceMaxState *prometheus.Desc + zoneTemp *prometheus.Desc } func init() { @@ -41,10 +46,20 @@ func NewThermalZoneCollector() (Collector, error) { return &thermalZoneCollector{ fs: fs, zoneTemp: prometheus.NewDesc( - prometheus.BuildFQName(namespace, "thermal_zone", "temp"), + prometheus.BuildFQName(namespace, thermalZone, "temp"), "Zone temperature in Celsius", []string{"zone", "type"}, nil, ), + coolingDeviceCurState: prometheus.NewDesc( + prometheus.BuildFQName(namespace, coolingDevice, "cur_state"), + "Current throttle state of the cooling device", + []string{"name", "type"}, nil, + ), + coolingDeviceMaxState: prometheus.NewDesc( + prometheus.BuildFQName(namespace, coolingDevice, "max_state"), + "Maximum throttle state of the cooling device", + []string{"name", "type"}, nil, + ), }, nil } @@ -64,5 +79,28 @@ func (c *thermalZoneCollector) Update(ch chan<- prometheus.Metric) error { ) } + coolingDevices, err := c.fs.ClassCoolingDeviceStats() + if err != nil { + return err + } + + for _, stats := range coolingDevices { + ch <- prometheus.MustNewConstMetric( + c.coolingDeviceCurState, + prometheus.GaugeValue, + float64(stats.CurState), + stats.Name, + stats.Type, + ) + + ch <- prometheus.MustNewConstMetric( + c.coolingDeviceMaxState, + prometheus.GaugeValue, + float64(stats.MaxState), + stats.Name, + stats.Type, + ) + } + return nil } diff --git a/vendor/github.com/prometheus/procfs/arp.go b/vendor/github.com/prometheus/procfs/arp.go new file mode 100644 index 00000000..916c9182 --- /dev/null +++ b/vendor/github.com/prometheus/procfs/arp.go @@ -0,0 +1,85 @@ +// Copyright 2019 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package procfs + +import ( + "fmt" + "io/ioutil" + "net" + "strings" +) + +// ARPEntry contains a single row of the columnar data represented in +// /proc/net/arp. +type ARPEntry struct { + // IP address + IPAddr net.IP + // MAC address + HWAddr net.HardwareAddr + // Name of the device + Device string +} + +// GatherARPEntries retrieves all the ARP entries, parse the relevant columns, +// and then return a slice of ARPEntry's. +func (fs FS) GatherARPEntries() ([]ARPEntry, error) { + data, err := ioutil.ReadFile(fs.proc.Path("net/arp")) + if err != nil { + return nil, fmt.Errorf("error reading arp %s: %s", fs.proc.Path("net/arp"), err) + } + + return parseARPEntries(data) +} + +func parseARPEntries(data []byte) ([]ARPEntry, error) { + lines := strings.Split(string(data), "\n") + entries := make([]ARPEntry, 0) + var err error + const ( + expectedDataWidth = 6 + expectedHeaderWidth = 9 + ) + for _, line := range lines { + columns := strings.Fields(line) + width := len(columns) + + if width == expectedHeaderWidth || width == 0 { + continue + } else if width == expectedDataWidth { + entry, err := parseARPEntry(columns) + if err != nil { + return []ARPEntry{}, fmt.Errorf("failed to parse ARP entry: %s", err) + } + entries = append(entries, entry) + } else { + return []ARPEntry{}, fmt.Errorf("%d columns were detected, but %d were expected", width, expectedDataWidth) + } + + } + + return entries, err +} + +func parseARPEntry(columns []string) (ARPEntry, error) { + ip := net.ParseIP(columns[0]) + mac := net.HardwareAddr(columns[3]) + + entry := ARPEntry{ + IPAddr: ip, + HWAddr: mac, + Device: columns[5], + } + + return entry, nil +} diff --git a/vendor/github.com/prometheus/procfs/crypto.go b/vendor/github.com/prometheus/procfs/crypto.go new file mode 100644 index 00000000..19d4041b --- /dev/null +++ b/vendor/github.com/prometheus/procfs/crypto.go @@ -0,0 +1,131 @@ +// Copyright 2019 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package procfs + +import ( + "bytes" + "fmt" + "io/ioutil" + "strconv" + "strings" + + "github.com/prometheus/procfs/internal/util" +) + +// Crypto holds info parsed from /proc/crypto. +type Crypto struct { + Alignmask *uint64 + Async bool + Blocksize *uint64 + Chunksize *uint64 + Ctxsize *uint64 + Digestsize *uint64 + Driver string + Geniv string + Internal string + Ivsize *uint64 + Maxauthsize *uint64 + MaxKeysize *uint64 + MinKeysize *uint64 + Module string + Name string + Priority *int64 + Refcnt *int64 + Seedsize *uint64 + Selftest string + Type string + Walksize *uint64 +} + +// Crypto parses an crypto-file (/proc/crypto) and returns a slice of +// structs containing the relevant info. More information available here: +// https://kernel.readthedocs.io/en/sphinx-samples/crypto-API.html +func (fs FS) Crypto() ([]Crypto, error) { + data, err := ioutil.ReadFile(fs.proc.Path("crypto")) + if err != nil { + return nil, fmt.Errorf("error parsing crypto %s: %s", fs.proc.Path("crypto"), err) + } + crypto, err := parseCrypto(data) + if err != nil { + return nil, fmt.Errorf("error parsing crypto %s: %s", fs.proc.Path("crypto"), err) + } + return crypto, nil +} + +func parseCrypto(cryptoData []byte) ([]Crypto, error) { + crypto := []Crypto{} + + cryptoBlocks := bytes.Split(cryptoData, []byte("\n\n")) + + for _, block := range cryptoBlocks { + var newCryptoElem Crypto + + lines := strings.Split(string(block), "\n") + for _, line := range lines { + if strings.TrimSpace(line) == "" || line[0] == ' ' { + continue + } + fields := strings.Split(line, ":") + key := strings.TrimSpace(fields[0]) + value := strings.TrimSpace(fields[1]) + vp := util.NewValueParser(value) + + switch strings.TrimSpace(key) { + case "async": + b, err := strconv.ParseBool(value) + if err == nil { + newCryptoElem.Async = b + } + case "blocksize": + newCryptoElem.Blocksize = vp.PUInt64() + case "chunksize": + newCryptoElem.Chunksize = vp.PUInt64() + case "digestsize": + newCryptoElem.Digestsize = vp.PUInt64() + case "driver": + newCryptoElem.Driver = value + case "geniv": + newCryptoElem.Geniv = value + case "internal": + newCryptoElem.Internal = value + case "ivsize": + newCryptoElem.Ivsize = vp.PUInt64() + case "maxauthsize": + newCryptoElem.Maxauthsize = vp.PUInt64() + case "max keysize": + newCryptoElem.MaxKeysize = vp.PUInt64() + case "min keysize": + newCryptoElem.MinKeysize = vp.PUInt64() + case "module": + newCryptoElem.Module = value + case "name": + newCryptoElem.Name = value + case "priority": + newCryptoElem.Priority = vp.PInt64() + case "refcnt": + newCryptoElem.Refcnt = vp.PInt64() + case "seedsize": + newCryptoElem.Seedsize = vp.PUInt64() + case "selftest": + newCryptoElem.Selftest = value + case "type": + newCryptoElem.Type = value + case "walksize": + newCryptoElem.Walksize = vp.PUInt64() + } + } + crypto = append(crypto, newCryptoElem) + } + return crypto, nil +} diff --git a/vendor/github.com/prometheus/procfs/net_softnet.go b/vendor/github.com/prometheus/procfs/net_softnet.go new file mode 100644 index 00000000..6fcad20a --- /dev/null +++ b/vendor/github.com/prometheus/procfs/net_softnet.go @@ -0,0 +1,91 @@ +// Copyright 2019 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package procfs + +import ( + "fmt" + "io/ioutil" + "strconv" + "strings" +) + +// For the proc file format details, +// see https://elixir.bootlin.com/linux/v4.17/source/net/core/net-procfs.c#L162 +// and https://elixir.bootlin.com/linux/v4.17/source/include/linux/netdevice.h#L2810. + +// SoftnetEntry contains a single row of data from /proc/net/softnet_stat +type SoftnetEntry struct { + // Number of processed packets + Processed uint + // Number of dropped packets + Dropped uint + // Number of times processing packets ran out of quota + TimeSqueezed uint +} + +// GatherSoftnetStats reads /proc/net/softnet_stat, parse the relevant columns, +// and then return a slice of SoftnetEntry's. +func (fs FS) GatherSoftnetStats() ([]SoftnetEntry, error) { + data, err := ioutil.ReadFile(fs.proc.Path("net/softnet_stat")) + if err != nil { + return nil, fmt.Errorf("error reading softnet %s: %s", fs.proc.Path("net/softnet_stat"), err) + } + + return parseSoftnetEntries(data) +} + +func parseSoftnetEntries(data []byte) ([]SoftnetEntry, error) { + lines := strings.Split(string(data), "\n") + entries := make([]SoftnetEntry, 0) + var err error + const ( + expectedColumns = 11 + ) + for _, line := range lines { + columns := strings.Fields(line) + width := len(columns) + if width == 0 { + continue + } + if width != expectedColumns { + return []SoftnetEntry{}, fmt.Errorf("%d columns were detected, but %d were expected", width, expectedColumns) + } + var entry SoftnetEntry + if entry, err = parseSoftnetEntry(columns); err != nil { + return []SoftnetEntry{}, err + } + entries = append(entries, entry) + } + + return entries, nil +} + +func parseSoftnetEntry(columns []string) (SoftnetEntry, error) { + var err error + var processed, dropped, timeSqueezed uint64 + if processed, err = strconv.ParseUint(columns[0], 16, 32); err != nil { + return SoftnetEntry{}, fmt.Errorf("Unable to parse column 0: %s", err) + } + if dropped, err = strconv.ParseUint(columns[1], 16, 32); err != nil { + return SoftnetEntry{}, fmt.Errorf("Unable to parse column 1: %s", err) + } + if timeSqueezed, err = strconv.ParseUint(columns[2], 16, 32); err != nil { + return SoftnetEntry{}, fmt.Errorf("Unable to parse column 2: %s", err) + } + return SoftnetEntry{ + Processed: uint(processed), + Dropped: uint(dropped), + TimeSqueezed: uint(timeSqueezed), + }, nil +} diff --git a/vendor/github.com/prometheus/procfs/sysfs/class_cooling_device.go b/vendor/github.com/prometheus/procfs/sysfs/class_cooling_device.go new file mode 100644 index 00000000..8927b02a --- /dev/null +++ b/vendor/github.com/prometheus/procfs/sysfs/class_cooling_device.go @@ -0,0 +1,90 @@ +// Copyright 2019 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build !windows + +package sysfs + +import ( + "path/filepath" + "strconv" + "strings" + + "github.com/prometheus/procfs/internal/util" +) + +// ClassCoolingDeviceStats contains info from files in /sys/class/thermal/cooling_device[0-9]* +// for a single device. +// https://www.kernel.org/doc/Documentation/thermal/sysfs-api.txt +type ClassCoolingDeviceStats struct { + Name string // The name of the cooling device. + Type string // Type of the cooling device(processor/fan/...) + MaxState int64 // Maximum cooling state of the cooling device + CurState int64 // Current cooling state of the cooling device +} + +func (fs FS) ClassCoolingDeviceStats() ([]ClassCoolingDeviceStats, error) { + cds, err := filepath.Glob(fs.sys.Path("class/thermal/cooling_device[0-9]*")) + if err != nil { + return []ClassCoolingDeviceStats{}, err + } + + var coolingDeviceStats = ClassCoolingDeviceStats{} + stats := make([]ClassCoolingDeviceStats, len(cds)) + for i, cd := range cds { + cdName := strings.TrimPrefix(filepath.Base(cd), "cooling_device") + + coolingDeviceStats, err = parseCoolingDeviceStats(cd) + if err != nil { + return []ClassCoolingDeviceStats{}, err + } + + coolingDeviceStats.Name = cdName + stats[i] = coolingDeviceStats + } + return stats, nil +} + +func parseCoolingDeviceStats(cd string) (ClassCoolingDeviceStats, error) { + cdType, err := util.SysReadFile(filepath.Join(cd, "type")) + if err != nil { + return ClassCoolingDeviceStats{}, err + } + + cdMaxStateString, err := util.SysReadFile(filepath.Join(cd, "max_state")) + if err != nil { + return ClassCoolingDeviceStats{}, err + } + cdMaxStateInt, err := strconv.ParseInt(cdMaxStateString, 10, 64) + if err != nil { + return ClassCoolingDeviceStats{}, err + } + + // cur_state can be -1, eg intel powerclamp + // https://www.kernel.org/doc/Documentation/thermal/intel_powerclamp.txt + cdCurStateString, err := util.SysReadFile(filepath.Join(cd, "cur_state")) + if err != nil { + return ClassCoolingDeviceStats{}, err + } + + cdCurStateInt, err := strconv.ParseInt(cdCurStateString, 10, 64) + if err != nil { + return ClassCoolingDeviceStats{}, err + } + + return ClassCoolingDeviceStats{ + Type: cdType, + MaxState: cdMaxStateInt, + CurState: cdCurStateInt, + }, nil +} diff --git a/vendor/github.com/prometheus/procfs/sysfs/vulnerability.go b/vendor/github.com/prometheus/procfs/sysfs/vulnerability.go new file mode 100644 index 00000000..d41ecfb0 --- /dev/null +++ b/vendor/github.com/prometheus/procfs/sysfs/vulnerability.go @@ -0,0 +1,84 @@ +// Copyright 2019 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sysfs + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "strings" +) + +const ( + notAffected = "Not Affected" + vulnerable = "Vulnerable" + mitigation = "Mitigation" +) + +// CPUVulnerabilities retrieves a map of vulnerability names to their mitigations. +func (fs FS) CPUVulnerabilities() ([]Vulnerability, error) { + matches, err := filepath.Glob(fs.sys.Path("devices/system/cpu/vulnerabilities/*")) + if err != nil { + return nil, err + } + + vulnerabilities := make([]Vulnerability, 0, len(matches)) + for _, match := range matches { + name := filepath.Base(match) + + value, err := ioutil.ReadFile(match) + if err != nil { + return nil, err + } + + v, err := parseVulnerability(name, string(value)) + if err != nil { + return nil, err + } + + vulnerabilities = append(vulnerabilities, v) + } + + return vulnerabilities, nil +} + +// Vulnerability represents a single vulnerability extracted from /sys/devices/system/cpu/vulnerabilities/ +type Vulnerability struct { + CodeName string + State string + Mitigation string +} + +func parseVulnerability(name, value string) (Vulnerability, error) { + v := Vulnerability{CodeName: name} + value = strings.TrimSpace(value) + if value == notAffected { + v.State = notAffected + return v, nil + } + + if strings.HasPrefix(value, vulnerable) { + v.State = vulnerable + v.Mitigation = strings.TrimPrefix(strings.TrimPrefix(value, vulnerable), ": ") + return v, nil + } + + if strings.HasPrefix(value, mitigation) { + v.State = mitigation + v.Mitigation = strings.TrimPrefix(strings.TrimPrefix(value, mitigation), ": ") + return v, nil + } + + return v, fmt.Errorf("unknown vulnerability state for %s: %s", name, value) +}