Skip to content

Commit

Permalink
dbus: fix support for CentOS 7 by using introspection
Browse files Browse the repository at this point in the history
Fix extracted from: elastic/beats#16902
  • Loading branch information
vooon committed Apr 3, 2020
1 parent 46fdc21 commit 3a1e1fe
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 38 deletions.
182 changes: 182 additions & 0 deletions dbus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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 !netbsd

package main

import (
"encoding/xml"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/coreos/go-systemd/v22/dbus"
dbusRaw "github.com/godbus/dbus"
"github.com/pkg/errors"
)

type unitFetcher func(conn *dbus.Conn, states, patterns []string) ([]dbus.UnitStatus, error)

// instrospectForUnitMethods determines what methods are available via dbus for listing systemd units.
// We have a number of functions, some better than others, for getting and filtering unit lists.
// This will attempt to find the most optimal method, and move down to methods that require more work.
func instrospectForUnitMethods() (unitFetcher, error) {
//setup a dbus connection
conn, err := dbusRaw.SystemBusPrivate()
if err != nil {
return nil, errors.Wrap(err, "error getting connection to system bus")
}

auth := dbusRaw.AuthExternal(strconv.Itoa(os.Getuid()))
err = conn.Auth([]dbusRaw.Auth{auth})
if err != nil {
return nil, errors.Wrap(err, "error authenticating")
}

err = conn.Hello()
if err != nil {
return nil, errors.Wrap(err, "error in Hello")
}

var props string

//call "introspect" on the systemd1 path to see what ListUnit* methods are available
obj := conn.Object("org.freedesktop.systemd1", dbusRaw.ObjectPath("/org/freedesktop/systemd1"))
err = obj.Call("org.freedesktop.DBus.Introspectable.Introspect", 0).Store(&props)
if err != nil {
return nil, errors.Wrap(err, "error calling dbus")
}

unitMap, err := parseXMLAndReturnMethods(props)
if err != nil {
return nil, errors.Wrap(err, "error handling XML")
}

//return a function callback ordered by desirability
if _, ok := unitMap["ListUnitsByPatterns"]; ok {
return listUnitsByPatternWrapper, nil
} else if _, ok := unitMap["ListUnitsFiltered"]; ok {
return listUnitsFilteredWrapper, nil
} else if _, ok := unitMap["ListUnits"]; ok {
return listUnitsWrapper, nil
}
return nil, fmt.Errorf("no supported list Units function: %v", unitMap)
}

func parseXMLAndReturnMethods(str string) (map[string]bool, error) {

type Method struct {
Name string `xml:"name,attr"`
}

type Iface struct {
Name string `xml:"name,attr"`
Method []Method `xml:"method"`
}

type IntrospectData struct {
XMLName xml.Name `xml:"node"`
Interface []Iface `xml:"interface"`
}

methods := IntrospectData{}

err := xml.Unmarshal([]byte(str), &methods)
if err != nil {
return nil, errors.Wrap(err, "error unmarshalling XML")
}

if len(methods.Interface) == 0 {
return nil, errors.Wrap(err, "no methods found on introspect")
}
methodMap := make(map[string]bool)
for _, iface := range methods.Interface {
for _, method := range iface.Method {
if strings.Contains(method.Name, "ListUnits") {
methodMap[method.Name] = true
}
}
}

return methodMap, nil
}

// listUnitsByPatternWrapper is a bare wrapper for the unitFetcher type
func listUnitsByPatternWrapper(conn *dbus.Conn, states, patterns []string) ([]dbus.UnitStatus, error) {
return conn.ListUnitsByPatterns(states, patterns)
}

//listUnitsFilteredWrapper wraps the dbus ListUnitsFiltered method
func listUnitsFilteredWrapper(conn *dbus.Conn, states, patterns []string) ([]dbus.UnitStatus, error) {
units, err := conn.ListUnitsFiltered(states)
if err != nil {
return nil, errors.Wrap(err, "ListUnitsFiltered error")
}

return matchUnitPatterns(patterns, units)
}

// listUnitsWrapper wraps the dbus ListUnits method
func listUnitsWrapper(conn *dbus.Conn, states, patterns []string) ([]dbus.UnitStatus, error) {
units, err := conn.ListUnits()
if err != nil {
return nil, errors.Wrap(err, "ListUnits error")
}
if len(patterns) > 0 {
units, err = matchUnitPatterns(patterns, units)
if err != nil {
return nil, errors.Wrap(err, "error matching unit patterns")
}
}

if len(states) > 0 {
var finalUnits []dbus.UnitStatus
for _, unit := range units {
for _, state := range states {
if unit.LoadState == state || unit.ActiveState == state || unit.SubState == state {
finalUnits = append(finalUnits, unit)
break
}
}
}
return finalUnits, nil
}

return units, nil
}

// matchUnitPatterns returns a list of units that match the pattern list.
// This algo, including filepath.Match, is designed to (somewhat) emulate the behavior of ListUnitsByPatterns, which uses `fnmatch`.
func matchUnitPatterns(patterns []string, units []dbus.UnitStatus) ([]dbus.UnitStatus, error) {
var matchUnits []dbus.UnitStatus
for _, unit := range units {
for _, pattern := range patterns {
match, err := filepath.Match(pattern, unit.Name)
if err != nil {
return nil, errors.Wrapf(err, "error matching with pattern %s", pattern)
}
if match {
matchUnits = append(matchUnits, unit)
break
}
}
}
return matchUnits, nil
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ require (
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
github.com/coreos/go-systemd/v22 v22.0.0
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/godbus/dbus v4.1.0+incompatible // indirect
github.com/godbus/dbus v4.1.0+incompatible
github.com/golang/protobuf v1.3.5 // indirect
github.com/mitchellh/mapstructure v1.2.2 // indirect
github.com/pelletier/go-toml v1.7.0 // indirect
github.com/pkg/errors v0.8.1
github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sensu-community/sensu-plugin-sdk v0.6.0
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAv
github.com/pierrec/cmdflag v0.0.2/go.mod h1:a3zKGZ3cdQUfxjd0RGMLZr8xI3nvpJOB+m6o/1X5BmU=
github.com/pierrec/lz4/v3 v3.0.1/go.mod h1:280XNCGS8jAcG++AHdd6SeWnzyJ1w9oow2vbORyey8Q=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down
54 changes: 17 additions & 37 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import (
// Config represents the check plugin config.
type Config struct {
sensu.PluginConfig
Units []string
UseByNameSearch bool
UnitPatterns []string
}

var (
Expand All @@ -31,17 +30,8 @@ var (
Env: "SYSTEMD_UNIT",
Argument: "unit",
Shorthand: "s",
Usage: "Systemd unit(s) to check",
Value: &plugin.Units,
},
&sensu.PluginConfigOption{
Path: "use_by_name_search",
Env: "SYSTEMD_USE_BY_NAME_SEARCH",
Argument: "by-name",
Shorthand: "n",
Usage: "use by name search (use with systemd > 230)",
Value: &plugin.UseByNameSearch,
Default: false,
Usage: "Systemd unit(s) pattern to check",
Value: &plugin.UnitPatterns,
},
}
)
Expand All @@ -51,18 +41,8 @@ func main() {
check.Execute()
}

func ssContains(s string, ss []string) bool {
for _, sl := range ss {
if s == sl {
return true
}
}

return false
}

func checkArgs(event *types.Event) (int, error) {
if len(plugin.Units) == 0 {
if len(plugin.UnitPatterns) == 0 {
return sensu.CheckStateWarning, fmt.Errorf("--unit or SYSTEMD_UNIT environment variable is required")
}

Expand All @@ -76,25 +56,25 @@ func executeCheck(event *types.Event) (int, error) {
}
defer conn.Close()

var unitStats []dbus.UnitStatus

if plugin.UseByNameSearch {
unitStats, err = conn.ListUnitsByNames(plugin.Units)
} else {
unitStats, err = conn.ListUnitsByPatterns(nil, plugin.Units)
unitFetcher, err := instrospectForUnitMethods()
if err != nil {
return sensu.CheckStateUnknown, fmt.Errorf("could not introspect systemd dbus: %w", err)
}

unitStats, err := unitFetcher(conn, nil, plugin.UnitPatterns)
if err != nil {
return sensu.CheckStateUnknown, fmt.Errorf("list units error: %w", err)
}

if len(unitStats) < len(plugin.Units) {
if len(unitStats) < len(plugin.UnitPatterns) {
err = nil
foundUnits := make([]string, 0, len(unitStats))
for _, unit := range unitStats {
foundUnits = append(foundUnits, unit.Name)
}
for _, unit := range plugin.Units {
if !ssContains(unit, foundUnits) {
for _, unit := range plugin.UnitPatterns {
matched, err := matchUnitPatterns([]string{unit}, unitStats)
if err != nil {
fmt.Printf("CRITICAL: %s: match error: %v\n", unit, err)
err = multierr.Append(err, fmt.Errorf("%s: match error: %w", unit, err))
}
if len(matched) == 0 {
fmt.Printf("CRITICAL: %s: not present\n", unit)
err = multierr.Append(err, fmt.Errorf("%s: not present", unit))
}
Expand Down

0 comments on commit 3a1e1fe

Please sign in to comment.