Get time info from timedated

This commit is contained in:
Andrew Reed
2021-02-10 20:01:15 +00:00
parent f2e4127111
commit 9984fe2caa
12 changed files with 571 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
apiVersion: troubleshoot.sh/v1beta2
kind: HostPreflight
metadata:
name: ntp
spec:
collectors:
- time: {}
analyzers:
- time:
outcomes:
- fail:
when: "ntp == unsynchronized+inactive"
message: System clock not synchronized
- warn:
when: "ntp == unsynchronized+active"
message: System clock not yet synchronized
- warn:
when: "ntp == synchronized+inactive"
message: NTP not active
- pass:
when: "ntp == synchronized+active"
message: System clock is synchronized
+15
View File
@@ -0,0 +1,15 @@
apiVersion: troubleshoot.sh/v1beta2
kind: HostPreflight
metadata:
name: timezone
spec:
collectors:
- time: {}
analyzers:
- time:
outcomes:
- pass:
when: "timezone == UTC"
message: Timezone is UTC
- fail:
message: Timezone is not UTC
+1
View File
@@ -15,6 +15,7 @@ require (
github.com/go-redis/redis/v7 v7.2.0
github.com/go-sql-driver/mysql v1.5.0
github.com/gobwas/glob v0.2.3
github.com/godbus/dbus v4.1.0+incompatible
github.com/google/go-cmp v0.3.1 // indirect
github.com/google/gofuzz v1.1.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
+2
View File
@@ -189,6 +189,8 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
+7
View File
@@ -83,6 +83,13 @@ func HostAnalyze(hostAnalyzer *troubleshootv1beta2.HostAnalyze, getFile getColle
}
return []*AnalyzeResult{result}, nil
}
if hostAnalyzer.Time != nil {
result, err := analyzeHostTime(hostAnalyzer.Time, getFile)
if err != nil {
return nil, err
}
return []*AnalyzeResult{result}, nil
}
return nil, errors.New("invalid analyzer")
}
+144
View File
@@ -0,0 +1,144 @@
package analyzer
import (
"encoding/json"
"fmt"
"strings"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
)
const (
SynchronizedActive = "synchronized+active"
SynchronizedInactive = "synchronized+inactive"
UnsynchronizedActive = "unsynchronized+active"
UnsynchronizedInactive = "unsynchronized+inactive"
)
func analyzeHostTime(hostAnalyzer *troubleshootv1beta2.TimeAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
contents, err := getCollectedFileContents("system/time.json")
if err != nil {
return nil, errors.Wrap(err, "failed to get collected file")
}
timeInfo := collect.TimeInfo{}
if err := json.Unmarshal(contents, &timeInfo); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal time info")
}
result := AnalyzeResult{}
title := hostAnalyzer.CheckName
if title == "" {
title = "Time"
}
result.Title = title
for _, outcome := range hostAnalyzer.Outcomes {
if outcome.Fail != nil {
if outcome.Fail.When == "" {
result.IsFail = true
result.Message = outcome.Fail.Message
result.URI = outcome.Fail.URI
return &result, nil
}
isMatch, err := compareHostTimeStatusToActual(outcome.Fail.When, timeInfo)
if err != nil {
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When)
}
if isMatch {
result.IsFail = true
result.Message = outcome.Fail.Message
result.URI = outcome.Fail.URI
return &result, nil
}
} else if outcome.Warn != nil {
if outcome.Warn.When == "" {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI
return &result, nil
}
isMatch, err := compareHostTimeStatusToActual(outcome.Warn.When, timeInfo)
if err != nil {
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When)
}
if isMatch {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI
return &result, nil
}
} else if outcome.Pass != nil {
if outcome.Pass.When == "" {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI
return &result, nil
}
isMatch, err := compareHostTimeStatusToActual(outcome.Pass.When, timeInfo)
if err != nil {
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When)
}
if isMatch {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI
return &result, nil
}
}
}
return &result, nil
}
func compareHostTimeStatusToActual(status string, timeInfo collect.TimeInfo) (res bool, err error) {
parts := strings.Split(status, " ")
if len(parts) != 3 {
return false, fmt.Errorf("Expected exactly 3 parts, got %d", len(parts))
}
if parts[0] == "timezone" {
if parts[1] != "=" && parts[1] != "==" && parts[1] != "===" && parts[1] != "!=" {
return false, errors.New(`Only supported operators are "==" and "!="`)
}
if parts[1] == "!=" {
return parts[2] != timeInfo.Timezone, nil
}
return parts[2] == timeInfo.Timezone, nil
}
if parts[0] == "ntp" {
if parts[1] != "=" && parts[1] != "==" && parts[1] != "===" {
return false, errors.New(`Only supported operator is "=="`)
}
switch parts[2] {
case SynchronizedActive:
return timeInfo.NTPSynchronized && timeInfo.NTPActive, nil
case SynchronizedInactive:
return timeInfo.NTPSynchronized && !timeInfo.NTPActive, nil
case UnsynchronizedActive:
return !timeInfo.NTPSynchronized && timeInfo.NTPActive, nil
case UnsynchronizedInactive:
return !timeInfo.NTPSynchronized && !timeInfo.NTPActive, nil
default:
return false, fmt.Errorf("Unknown status %q. Allowed values are %q, %q, %q, or %q", parts[2], SynchronizedActive, SynchronizedInactive, UnsynchronizedActive, UnsynchronizedInactive)
}
}
return false, fmt.Errorf("Unknown keyword: %s", parts[0])
}
+232
View File
@@ -0,0 +1,232 @@
package analyzer
import (
"encoding/json"
"testing"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAnalyzeHostTime(t *testing.T) {
tests := []struct {
name string
timeInfo *collect.TimeInfo
hostAnalyzer *troubleshootv1beta2.TimeAnalyze
result *AnalyzeResult
expectErr bool
}{
{
name: "ntp == synchronized+active",
timeInfo: &collect.TimeInfo{
Timezone: "UTC",
NTPSynchronized: true,
NTPActive: true,
},
hostAnalyzer: &troubleshootv1beta2.TimeAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "ntp == unsynchronized+inactive",
Message: "System clock not synchronized",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "ntp == synchronized+active",
Message: "System clock synchronized and NTP is active",
},
},
},
},
result: &AnalyzeResult{
Title: "Time",
IsPass: true,
Message: "System clock synchronized and NTP is active",
},
},
{
name: "ntp == unsynchronized+inactive",
timeInfo: &collect.TimeInfo{
Timezone: "UTC",
NTPSynchronized: false,
NTPActive: false,
},
hostAnalyzer: &troubleshootv1beta2.TimeAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "ntp == unsynchronized+inactive",
Message: "System clock not synchronized",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "ntp == synchronized+active",
Message: "System clock synchronized and NTP is active",
},
},
},
},
result: &AnalyzeResult{
Title: "Time",
IsFail: true,
Message: "System clock not synchronized",
},
},
{
name: "ntp == unsynchronized+active",
timeInfo: &collect.TimeInfo{
Timezone: "UTC",
NTPSynchronized: false,
NTPActive: true,
},
hostAnalyzer: &troubleshootv1beta2.TimeAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "ntp == unsynchronized+inactive",
Message: "System clock not synchronized",
},
},
{
Warn: &troubleshootv1beta2.SingleOutcome{
When: "ntp == unsynchronized+active",
Message: "System clock not yet synchronized",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "ntp == synchronized+active",
Message: "System clock synchronized and NTP is active",
},
},
},
},
result: &AnalyzeResult{
Title: "Time",
IsWarn: true,
Message: "System clock not yet synchronized",
},
},
{
name: "ntp == synchronized+inactive",
timeInfo: &collect.TimeInfo{
Timezone: "UTC",
NTPSynchronized: true,
NTPActive: false,
},
hostAnalyzer: &troubleshootv1beta2.TimeAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "ntp == unsynchronized+inactive",
Message: "System clock not synchronized",
},
},
{
Warn: &troubleshootv1beta2.SingleOutcome{
When: "ntp == unsynchronized+active",
Message: "System clock not yet synchronized",
},
},
{
Warn: &troubleshootv1beta2.SingleOutcome{
When: "ntp == synchronized+inactive",
Message: "System clock synchronized for now",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "ntp == synchronized+active",
Message: "System clock synchronized and NTP is active",
},
},
},
},
result: &AnalyzeResult{
Title: "Time",
IsWarn: true,
Message: "System clock synchronized for now",
},
},
{
name: "timezone",
timeInfo: &collect.TimeInfo{
Timezone: "UTC",
NTPSynchronized: true,
NTPActive: true,
},
hostAnalyzer: &troubleshootv1beta2.TimeAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "timezone == UTC",
Message: "Timezone is set to UTC",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "timezone not set to UTC",
},
},
},
},
result: &AnalyzeResult{
Title: "Time",
IsPass: true,
Message: "Timezone is set to UTC",
},
},
{
name: "timezone is not UTC",
timeInfo: &collect.TimeInfo{
Timezone: "PST",
NTPSynchronized: true,
NTPActive: true,
},
hostAnalyzer: &troubleshootv1beta2.TimeAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "timezone != UTC",
Message: "Timezone is not set to UTC",
},
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "Timezone is set to UTC",
},
},
},
},
result: &AnalyzeResult{
Title: "Time",
IsFail: true,
Message: "Timezone is not set to UTC",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)
b, err := json.Marshal(test.timeInfo)
if err != nil {
t.Fatal(err)
}
getCollectedFileContents := func(filename string) ([]byte, error) {
return b, nil
}
result, err := analyzeHostTime(test.hostAnalyzer, getCollectedFileContents)
if test.expectErr {
req.Error(err)
} else {
req.NoError(err)
}
assert.Equal(t, test.result, result)
})
}
}
@@ -34,6 +34,11 @@ type HTTPAnalyze struct {
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type TimeAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type HostAnalyze struct {
CPU *CPUAnalyze `json:"cpu,omitempty" yaml:"cpu,omitempty"`
//
@@ -46,4 +51,6 @@ type HostAnalyze struct {
TCPPortStatus *TCPPortStatusAnalyze `json:"tcpPortStatus,omitempty" yaml:"tcpPortStatus,omitempty"`
HTTP *HTTPAnalyze `json:"http" yaml:"http"`
Time *TimeAnalyze `json:"time" yaml:"time"`
}
@@ -59,6 +59,10 @@ type HostHTTP struct {
Put *Put `json:"put,omitempty" yaml:"put,omitempty"`
}
type HostTime struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
}
type HostCollect struct {
CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"`
Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"`
@@ -69,6 +73,7 @@ type HostCollect struct {
IPV4Interfaces *IPV4Interfaces `json:"ipv4Interfaces,omitempty" yaml:"ipv4Interfaces,omitempty"`
DiskUsage *DiskUsage `json:"diskUsage,omitempty" yaml:"diskUsage,omitempty"`
HTTP *HostHTTP `json:"http,omitempty" yaml:"http,omitempty"`
Time *HostTime `json:"time,omitempty" yaml:"time,omitempty"`
}
func (c *HostCollect) GetName() string {
@@ -1103,6 +1103,11 @@ func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) {
*out = new(HTTPAnalyze)
(*in).DeepCopyInto(*out)
}
if in.Time != nil {
in, out := &in.Time, &out.Time
*out = new(TimeAnalyze)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAnalyze.
@@ -1163,6 +1168,11 @@ func (in *HostCollect) DeepCopyInto(out *HostCollect) {
*out = new(HostHTTP)
(*in).DeepCopyInto(*out)
}
if in.Time != nil {
in, out := &in.Time, &out.Time
*out = new(HostTime)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollect.
@@ -1333,6 +1343,22 @@ func (in *HostPreflightStatus) DeepCopy() *HostPreflightStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostTime) DeepCopyInto(out *HostTime) {
*out = *in
out.HostCollectorMeta = in.HostCollectorMeta
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostTime.
func (in *HostTime) DeepCopy() *HostTime {
if in == nil {
return nil
}
out := new(HostTime)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IPV4Interfaces) DeepCopyInto(out *IPV4Interfaces) {
*out = *in
@@ -2348,3 +2374,30 @@ func (in *TextAnalyze) DeepCopy() *TextAnalyze {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TimeAnalyze) DeepCopyInto(out *TimeAnalyze) {
*out = *in
out.AnalyzeMeta = in.AnalyzeMeta
if in.Outcomes != nil {
in, out := &in.Outcomes, &out.Outcomes
*out = make([]*Outcome, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(Outcome)
(*in).DeepCopyInto(*out)
}
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TimeAnalyze.
func (in *TimeAnalyze) DeepCopy() *TimeAnalyze {
if in == nil {
return nil
}
out := new(TimeAnalyze)
in.DeepCopyInto(out)
return out
}
+2
View File
@@ -30,6 +30,8 @@ func (c *HostCollector) RunCollectorSync() (result map[string][]byte, err error)
result, err = HostTCPPortStatus(c)
} else if c.Collect.HTTP != nil {
result, err = HostHTTP(c)
} else if c.Collect.Time != nil {
result, err = HostTime(c)
} else {
err = errors.New("no spec found to run")
return
+81
View File
@@ -0,0 +1,81 @@
package collect
import (
"encoding/json"
"fmt"
"log"
"strings"
"github.com/godbus/dbus"
"github.com/pkg/errors"
)
type NTPStatus string
type TimeInfo struct {
Timezone string `json:"timezone"`
NTPSynchronized bool `json:"ntp_synchronized"`
NTPActive bool `json:"ntp_active"`
}
func HostTime(c *HostCollector) (map[string][]byte, error) {
timeInfo := TimeInfo{}
conn, err := dbus.SystemBus()
if err != nil {
return nil, errors.Wrap(err, "failed to connect to dbus")
}
defer func() {
if err := conn.Close(); err != nil {
log.Printf("Failed to close dbus connection: %v\n", err)
}
}()
prop := "org.freedesktop.timedate1.Timezone"
variant, err := conn.Object("org.freedesktop.timedate1", "/org/freedesktop/timedate1").GetProperty(prop)
if err != nil {
return nil, errors.Wrapf(err, "failed to read property %s", prop)
}
timeInfo.Timezone = strings.Trim(variant.String(), `"`)
// UTC is reported as Etc/UTC on Ubuntu
if strings.ToLower(timeInfo.Timezone) == "etc/utc" {
timeInfo.Timezone = "UTC"
}
prop = "org.freedesktop.timedate1.NTPSynchronized"
variant, err = conn.Object("org.freedesktop.timedate1", "/org/freedesktop/timedate1").GetProperty(prop)
if err != nil {
return nil, errors.Wrapf(err, "failed to read property %s", prop)
}
switch variant.String() {
case "true":
timeInfo.NTPSynchronized = true
case "false":
timeInfo.NTPSynchronized = false
default:
return nil, fmt.Errorf("Unexpected value for property %s: %s", prop, variant.String())
}
prop = "org.freedesktop.timedate1.NTP"
variant, err = conn.Object("org.freedesktop.timedate1", "/org/freedesktop/timedate1").GetProperty(prop)
if err != nil {
return nil, errors.Wrapf(err, "failed to read property %s", prop)
}
switch variant.String() {
case "true":
timeInfo.NTPActive = true
case "false":
timeInfo.NTPActive = false
default:
return nil, fmt.Errorf("Unexpected value for property %s: %s", prop, variant.String())
}
b, err := json.Marshal(timeInfo)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal time info")
}
return map[string][]byte{
"system/time.json": b,
}, nil
}