diff --git a/agent/unit_state_test.go b/agent/unit_state_test.go index 710571404..12c47eeb3 100644 --- a/agent/unit_state_test.go +++ b/agent/unit_state_test.go @@ -819,7 +819,7 @@ func TestMarshalJSON(t *testing.T) { if err != nil { t.Fatalf("unexpected error marshalling: %v", err) } - want = `{"Cache":{"bar.service":{"LoadState":"","ActiveState":"inactive","SubState":"","MachineID":"asdf","UnitHash":"","UnitName":"bar.service"},"foo.service":{"LoadState":"","ActiveState":"active","SubState":"","MachineID":"asdf","UnitHash":"","UnitName":"foo.service"}},"ToPublish":{"woof.service":{"LoadState":"","ActiveState":"active","SubState":"","MachineID":"asdf","UnitHash":"","UnitName":"woof.service"}}}` + want = `{"Cache":{"bar.service":{"LoadState":"","ActiveState":"inactive","SubState":"","MachineID":"asdf","UnitHash":"","UnitName":"bar.service","ActiveEnterTimestamp":0},"foo.service":{"LoadState":"","ActiveState":"active","SubState":"","MachineID":"asdf","UnitHash":"","UnitName":"foo.service","ActiveEnterTimestamp":0}},"ToPublish":{"woof.service":{"LoadState":"","ActiveState":"active","SubState":"","MachineID":"asdf","UnitHash":"","UnitName":"woof.service","ActiveEnterTimestamp":0}}}` if string(got) != want { t.Fatalf("Bad JSON representation: got\n%s\n\nwant\n%s", string(got), want) } diff --git a/api/state_test.go b/api/state_test.go index 4107c31b1..898177594 100644 --- a/api/state_test.go +++ b/api/state_test.go @@ -33,10 +33,10 @@ func TestUnitStateList(t *testing.T) { us2 := unit.UnitState{UnitName: "BBB", ActiveState: "inactive", MachineID: "XXX"} us3 := unit.UnitState{UnitName: "CCC", ActiveState: "active", MachineID: "XXX"} us4 := unit.UnitState{UnitName: "CCC", ActiveState: "inactive", MachineID: "YYY"} - sus1 := &schema.UnitState{Name: "AAA", SystemdActiveState: "active"} - sus2 := &schema.UnitState{Name: "BBB", SystemdActiveState: "inactive", MachineID: "XXX"} - sus3 := &schema.UnitState{Name: "CCC", SystemdActiveState: "active", MachineID: "XXX"} - sus4 := &schema.UnitState{Name: "CCC", SystemdActiveState: "inactive", MachineID: "YYY"} + sus1 := &schema.UnitState{Name: "AAA", SystemdActiveState: "active", SystemdActiveEnterTimestamp: "0"} + sus2 := &schema.UnitState{Name: "BBB", SystemdActiveState: "inactive", MachineID: "XXX", SystemdActiveEnterTimestamp: "0"} + sus3 := &schema.UnitState{Name: "CCC", SystemdActiveState: "active", MachineID: "XXX", SystemdActiveEnterTimestamp: "0"} + sus4 := &schema.UnitState{Name: "CCC", SystemdActiveState: "inactive", MachineID: "YYY", SystemdActiveEnterTimestamp: "0"} for i, tt := range []struct { url string @@ -163,12 +163,12 @@ func TestUnitStateList(t *testing.T) { return } - expect1 := &schema.UnitState{Name: "XXX", SystemdActiveState: "active"} + expect1 := &schema.UnitState{Name: "XXX", SystemdActiveState: "active", SystemdActiveEnterTimestamp: "0"} if !reflect.DeepEqual(expect1, page.States[0]) { t.Errorf("expected first entity %#v, got %#v", expect1, page.States[0]) } - expect2 := &schema.UnitState{Name: "YYY", SystemdActiveState: "inactive"} + expect2 := &schema.UnitState{Name: "YYY", SystemdActiveState: "inactive", SystemdActiveEnterTimestamp: "0"} if !reflect.DeepEqual(expect2, page.States[1]) { t.Errorf("expected first entity %#v, got %#v", expect2, page.States[1]) } diff --git a/fleetctl/list_units.go b/fleetctl/list_units.go index 2b362f5a4..9b32b60b4 100644 --- a/fleetctl/list_units.go +++ b/fleetctl/list_units.go @@ -17,7 +17,9 @@ package main import ( "fmt" "sort" + "strconv" "strings" + "time" "github.com/spf13/cobra" @@ -26,7 +28,8 @@ import ( ) const ( - defaultListUnitsFields = "unit,machine,active,sub" + defaultListUnitsFields = "unit,machine,active,sub,uptime" + tmFormatString = "2006-01-02 03:04:05 PM MST" ) var ( @@ -75,6 +78,17 @@ var ( } return us.Hash }, + "uptime": func(us *schema.UnitState, full bool) string { + if us == nil || us.SystemdActiveState != "active" { + return "-" + } + // SystemdActiveEnterTimestamp is in microseconds, while time.Unix + // requires the 2nd parameter as value in nanoseconds. + ts, _ := strconv.Atoi(us.SystemdActiveEnterTimestamp) + tm := time.Unix(0, int64(ts)*1000) + duration := time.Now().Sub(tm) + return fmt.Sprintf("%s, Since %ss", tm.Format(tmFormatString), strings.Split(duration.String(), ".")[0]) + }, } ) diff --git a/functional/systemd_test.go b/functional/systemd_test.go index d1a37448e..cc884c1bd 100644 --- a/functional/systemd_test.go +++ b/functional/systemd_test.go @@ -73,7 +73,7 @@ ExecStart=/usr/bin/sleep 3000 t.Fatalf("Expected [hello.service], got %v", units) } - err = waitForUnitState(mgr, name, unit.UnitState{"loaded", "inactive", "dead", "", hash, ""}) + err = waitForUnitState(mgr, name, unit.UnitState{"loaded", "inactive", "dead", "", hash, "", 0}) if err != nil { t.Error(err) } @@ -83,7 +83,7 @@ ExecStart=/usr/bin/sleep 3000 t.Error(err) } - err = waitForUnitState(mgr, name, unit.UnitState{"loaded", "active", "running", "", hash, ""}) + err = waitForUnitState(mgr, name, unit.UnitState{"loaded", "active", "running", "", hash, "", 0}) if err != nil { t.Error(err) } @@ -119,8 +119,19 @@ func waitForUnitState(mgr unit.UnitManager, name string, want unit.UnitState) er return err } - if reflect.DeepEqual(want, *got) { + if isEqualUnitState(want, *got) { return nil } } } + +// isEqualUnitState checks if both units are the same, +// excluding ActiveEnterTimestamp field of each unit state. +func isEqualUnitState(src, dst unit.UnitState) bool { + return src.LoadState == dst.LoadState && + src.ActiveState == dst.ActiveState && + src.SubState == dst.SubState && + src.MachineID == dst.MachineID && + src.UnitHash == dst.UnitHash && + src.UnitName == dst.UnitName +} diff --git a/registry/unit_state.go b/registry/unit_state.go index 40d2f40a7..f891a014c 100644 --- a/registry/unit_state.go +++ b/registry/unit_state.go @@ -239,11 +239,12 @@ func (r *EtcdRegistry) RemoveUnitState(jobName string) error { } type unitStateModel struct { - LoadState string `json:"loadState"` - ActiveState string `json:"activeState"` - SubState string `json:"subState"` - MachineState *machine.MachineState `json:"machineState"` - UnitHash string `json:"unitHash"` + LoadState string `json:"loadState"` + ActiveState string `json:"activeState"` + SubState string `json:"subState"` + MachineState *machine.MachineState `json:"machineState"` + UnitHash string `json:"unitHash"` + ActiveEnterTimestamp uint64 `json:"activeEnterTimestamp"` } func modelToUnitState(usm *unitStateModel, name string) *unit.UnitState { @@ -252,11 +253,12 @@ func modelToUnitState(usm *unitStateModel, name string) *unit.UnitState { } us := unit.UnitState{ - LoadState: usm.LoadState, - ActiveState: usm.ActiveState, - SubState: usm.SubState, - UnitHash: usm.UnitHash, - UnitName: name, + LoadState: usm.LoadState, + ActiveState: usm.ActiveState, + SubState: usm.SubState, + UnitHash: usm.UnitHash, + UnitName: name, + ActiveEnterTimestamp: usm.ActiveEnterTimestamp, } if usm.MachineState != nil { @@ -277,10 +279,11 @@ func unitStateToModel(us *unit.UnitState) *unitStateModel { } usm := unitStateModel{ - LoadState: us.LoadState, - ActiveState: us.ActiveState, - SubState: us.SubState, - UnitHash: us.UnitHash, + LoadState: us.LoadState, + ActiveState: us.ActiveState, + SubState: us.SubState, + UnitHash: us.UnitHash, + ActiveEnterTimestamp: us.ActiveEnterTimestamp, } if us.MachineID != "" { diff --git a/registry/unit_state_test.go b/registry/unit_state_test.go index 46972624b..bdaeb1fd7 100644 --- a/registry/unit_state_test.go +++ b/registry/unit_state_test.go @@ -101,7 +101,7 @@ func TestSaveUnitState(t *testing.T) { r := &EtcdRegistry{kAPI: e, keyPrefix: "/fleet/"} j := "foo.service" mID := "mymachine" - us := unit.NewUnitState("abc", "def", "ghi", mID) + us := unit.NewUnitState("abc", "def", "ghi", mID, 1234567890) // Saving nil unit state should fail r.SaveUnitState(j, nil, time.Second) @@ -122,7 +122,7 @@ func TestSaveUnitState(t *testing.T) { us.UnitHash = "quickbrownfox" r.SaveUnitState(j, us, time.Second) - json := `{"loadState":"abc","activeState":"def","subState":"ghi","machineState":{"ID":"mymachine","PublicIP":"","Metadata":null,"Capabilities":null,"Version":""},"unitHash":"quickbrownfox"}` + json := `{"loadState":"abc","activeState":"def","subState":"ghi","machineState":{"ID":"mymachine","PublicIP":"","Metadata":null,"Capabilities":null,"Version":""},"unitHash":"quickbrownfox","activeEnterTimestamp":1234567890}` p1 := "/fleet/state/foo.service" p2 := "/fleet/states/foo.service/mymachine" want := []action{ @@ -196,48 +196,53 @@ func TestUnitStateToModel(t *testing.T) { { // Unit state with no hash and no machineID is not OK in: &unit.UnitState{ - LoadState: "foo", - ActiveState: "bar", - SubState: "baz", - MachineID: "", - UnitHash: "", - UnitName: "name", + LoadState: "foo", + ActiveState: "bar", + SubState: "baz", + MachineID: "", + UnitHash: "", + UnitName: "name", + ActiveEnterTimestamp: 0, }, want: nil, }, { // Unit state with hash but no machineID is OK in: &unit.UnitState{ - LoadState: "foo", - ActiveState: "bar", - SubState: "baz", - MachineID: "", - UnitHash: "heh", - UnitName: "name", + LoadState: "foo", + ActiveState: "bar", + SubState: "baz", + MachineID: "", + UnitHash: "heh", + UnitName: "name", + ActiveEnterTimestamp: 1234567890, }, want: &unitStateModel{ - LoadState: "foo", - ActiveState: "bar", - SubState: "baz", - MachineState: nil, - UnitHash: "heh", + LoadState: "foo", + ActiveState: "bar", + SubState: "baz", + MachineState: nil, + UnitHash: "heh", + ActiveEnterTimestamp: 1234567890, }, }, { in: &unit.UnitState{ - LoadState: "foo", - ActiveState: "bar", - SubState: "baz", - MachineID: "woof", - UnitHash: "miaow", - UnitName: "name", + LoadState: "foo", + ActiveState: "bar", + SubState: "baz", + MachineID: "woof", + UnitHash: "miaow", + UnitName: "name", + ActiveEnterTimestamp: 54321, }, want: &unitStateModel{ - LoadState: "foo", - ActiveState: "bar", - SubState: "baz", - MachineState: &machine.MachineState{ID: "woof"}, - UnitHash: "miaow", + LoadState: "foo", + ActiveState: "bar", + SubState: "baz", + MachineState: &machine.MachineState{ID: "woof"}, + UnitHash: "miaow", + ActiveEnterTimestamp: 54321, }, }, } { @@ -258,25 +263,27 @@ func TestModelToUnitState(t *testing.T) { want: nil, }, { - in: &unitStateModel{"foo", "bar", "baz", nil, ""}, + in: &unitStateModel{"foo", "bar", "baz", nil, "", 1234567890}, want: &unit.UnitState{ - LoadState: "foo", - ActiveState: "bar", - SubState: "baz", - MachineID: "", - UnitHash: "", - UnitName: "name", + LoadState: "foo", + ActiveState: "bar", + SubState: "baz", + MachineID: "", + UnitHash: "", + UnitName: "name", + ActiveEnterTimestamp: 1234567890, }, }, { - in: &unitStateModel{"z", "x", "y", &machine.MachineState{ID: "abcd"}, ""}, + in: &unitStateModel{"z", "x", "y", &machine.MachineState{ID: "abcd"}, "", 987654321}, want: &unit.UnitState{ - LoadState: "z", - ActiveState: "x", - SubState: "y", - MachineID: "abcd", - UnitHash: "", - UnitName: "name", + LoadState: "z", + ActiveState: "x", + SubState: "y", + MachineID: "abcd", + UnitHash: "", + UnitName: "name", + ActiveEnterTimestamp: 987654321, }, }, } { diff --git a/schema/mapper.go b/schema/mapper.go index 463895e47..c2b6e2d55 100644 --- a/schema/mapper.go +++ b/schema/mapper.go @@ -15,6 +15,8 @@ package schema import ( + "strconv" + gsunit "github.com/coreos/go-systemd/unit" "github.com/coreos/fleet/job" @@ -129,12 +131,13 @@ func MapUnitStatesToSchemaUnitStates(entities []*unit.UnitState) []*UnitState { func MapUnitStateToSchemaUnitState(entity *unit.UnitState) *UnitState { us := UnitState{ - Name: entity.UnitName, - Hash: entity.UnitHash, - MachineID: entity.MachineID, - SystemdLoadState: entity.LoadState, - SystemdActiveState: entity.ActiveState, - SystemdSubState: entity.SubState, + Name: entity.UnitName, + Hash: entity.UnitHash, + MachineID: entity.MachineID, + SystemdLoadState: entity.LoadState, + SystemdActiveState: entity.ActiveState, + SystemdSubState: entity.SubState, + SystemdActiveEnterTimestamp: strconv.Itoa(int(entity.ActiveEnterTimestamp)), } return &us @@ -143,13 +146,15 @@ func MapUnitStateToSchemaUnitState(entity *unit.UnitState) *UnitState { func MapSchemaUnitStatesToUnitStates(entities []*UnitState) []*unit.UnitState { us := make([]*unit.UnitState, len(entities)) for i, e := range entities { + ts, _ := strconv.Atoi(e.SystemdActiveEnterTimestamp) us[i] = &unit.UnitState{ - UnitName: e.Name, - UnitHash: e.Hash, - MachineID: e.MachineID, - LoadState: e.SystemdLoadState, - ActiveState: e.SystemdActiveState, - SubState: e.SystemdSubState, + UnitName: e.Name, + UnitHash: e.Hash, + MachineID: e.MachineID, + LoadState: e.SystemdLoadState, + ActiveState: e.SystemdActiveState, + SubState: e.SystemdSubState, + ActiveEnterTimestamp: uint64(ts), } } diff --git a/schema/v1-gen.go b/schema/v1-gen.go index 9eb3fa8b1..1fa1a7114 100644 --- a/schema/v1-gen.go +++ b/schema/v1-gen.go @@ -294,6 +294,8 @@ type UnitState struct { Name string `json:"name,omitempty"` + SystemdActiveEnterTimestamp string `json:"systemdActiveEnterTimestamp,omitempty"` + SystemdActiveState string `json:"systemdActiveState,omitempty"` SystemdLoadState string `json:"systemdLoadState,omitempty"` diff --git a/schema/v1-json.go b/schema/v1-json.go index 7d3520c98..e3bdfed2e 100644 --- a/schema/v1-json.go +++ b/schema/v1-json.go @@ -162,6 +162,9 @@ const DiscoveryJSON = `{ }, "systemdSubState": { "type": "string" + }, + "systemdActiveEnterTimestamp": { + "type": "string" } } }, diff --git a/schema/v1.json b/schema/v1.json index 189f15cfc..b1a007d20 100644 --- a/schema/v1.json +++ b/schema/v1.json @@ -141,6 +141,9 @@ }, "systemdSubState": { "type": "string" + }, + "systemdActiveEnterTimestamp": { + "type": "string" } } }, diff --git a/systemd/manager.go b/systemd/manager.go index cab4ed55c..b09d68a20 100644 --- a/systemd/manager.go +++ b/systemd/manager.go @@ -174,9 +174,10 @@ func (m *systemdUnitManager) getUnitState(name string) (*unit.UnitState, error) return nil, err } us := unit.UnitState{ - LoadState: info["LoadState"].(string), - ActiveState: info["ActiveState"].(string), - SubState: info["SubState"].(string), + LoadState: info["LoadState"].(string), + ActiveState: info["ActiveState"].(string), + SubState: info["SubState"].(string), + ActiveEnterTimestamp: info["ActiveEnterTimestamp"].(uint64), } return &us, nil } @@ -238,6 +239,9 @@ func (m *systemdUnitManager) GetUnitStates(filter pkg.Set) (map[string]*unit.Uni if h, ok := m.hashes[dus.Name]; ok { us.UnitHash = h.String() } + + us.ActiveEnterTimestamp = m.getActiveEnterTimestamp(dus.Name) + states[dus.Name] = us } @@ -256,6 +260,9 @@ func (m *systemdUnitManager) GetUnitStates(filter pkg.Set) (map[string]*unit.Uni if h, ok := m.hashes[name]; ok { us.UnitHash = h.String() } + + us.ActiveEnterTimestamp = m.getActiveEnterTimestamp(name) + states[name] = us } } @@ -317,6 +324,14 @@ func (m *systemdUnitManager) getUnitFilePath(name string) string { return path.Join(m.unitsDir, name) } +func (m *systemdUnitManager) getActiveEnterTimestamp(name string) (aTimestamp uint64) { + prop, err := m.systemd.GetUnitProperty(name, "ActiveEnterTimestamp") + if err != nil { + return 0 + } + return prop.Value.Value().(uint64) +} + func lsUnitsDir(dir string) ([]string, error) { filterFunc := func(name string) bool { if !unit.RecognizedUnitType(name) { diff --git a/unit/fake.go b/unit/fake.go index 34bcd5b17..9a6fb0674 100644 --- a/unit/fake.go +++ b/unit/fake.go @@ -84,7 +84,7 @@ func (fum *FakeUnitManager) GetUnitStates(filter pkg.Set) (map[string]*UnitState states := make(map[string]*UnitState) for _, name := range filter.Values() { if _, ok := fum.u[name]; ok { - states[name] = &UnitState{"loaded", "active", "running", "", "", name} + states[name] = &UnitState{"loaded", "active", "running", "", "", name, 0} } } diff --git a/unit/fake_test.go b/unit/fake_test.go index a0590c66d..ae78a10c5 100644 --- a/unit/fake_test.go +++ b/unit/fake_test.go @@ -60,7 +60,7 @@ func TestFakeUnitManagerLoadUnload(t *testing.T) { t.Fatalf("Expected non-nil UnitState") } - eus := NewUnitState("loaded", "active", "running", "") + eus := NewUnitState("loaded", "active", "running", "", 0) if !reflect.DeepEqual(*us, *eus) { t.Fatalf("Expected UnitState %v, got %v", eus, *us) } diff --git a/unit/generator_test.go b/unit/generator_test.go index 7e4f135f0..d01c0f0ef 100644 --- a/unit/generator_test.go +++ b/unit/generator_test.go @@ -49,7 +49,7 @@ func TestUnitStateGeneratorSubscribeLifecycle(t *testing.T) { // subscribed to foo.service so we should get a heartbeat expect := []UnitStateHeartbeat{ - UnitStateHeartbeat{Name: "foo.service", State: &UnitState{"loaded", "active", "running", "", "", "foo.service"}}, + UnitStateHeartbeat{Name: "foo.service", State: &UnitState{"loaded", "active", "running", "", "", "foo.service", 0}}, } assertGenerateUnitStateHeartbeats(t, um, gen, expect) diff --git a/unit/unit.go b/unit/unit.go index fc2b233b8..efe7d87f6 100644 --- a/unit/unit.go +++ b/unit/unit.go @@ -195,20 +195,22 @@ func HashFromHexString(key string) (Hash, error) { // UnitState encodes the current state of a unit loaded into a fleet agent type UnitState struct { - LoadState string - ActiveState string - SubState string - MachineID string - UnitHash string - UnitName string + LoadState string + ActiveState string + SubState string + MachineID string + UnitHash string + UnitName string + ActiveEnterTimestamp uint64 } -func NewUnitState(loadState, activeState, subState, mID string) *UnitState { +func NewUnitState(loadState, activeState, subState, mID string, activeEnterTimestamp uint64) *UnitState { return &UnitState{ - LoadState: loadState, - ActiveState: activeState, - SubState: subState, - MachineID: mID, + LoadState: loadState, + ActiveState: activeState, + SubState: subState, + MachineID: mID, + ActiveEnterTimestamp: activeEnterTimestamp, } } diff --git a/unit/unit_test.go b/unit/unit_test.go index 8bbb19729..4514b6c18 100644 --- a/unit/unit_test.go +++ b/unit/unit_test.go @@ -110,15 +110,16 @@ func TestDefaultUnitType(t *testing.T) { func TestNewUnitState(t *testing.T) { want := &UnitState{ - LoadState: "ls", - ActiveState: "as", - SubState: "ss", - MachineID: "id", + LoadState: "ls", + ActiveState: "as", + SubState: "ss", + MachineID: "id", + ActiveEnterTimestamp: 1234567890, } - got := NewUnitState("ls", "as", "ss", "id") + got := NewUnitState("ls", "as", "ss", "id", 1234567890) if !reflect.DeepEqual(got, want) { - t.Fatalf("NewUnitState did not create a correct UnitState: got %s, want %s", got, want) + t.Fatalf("NewUnitState did not create a correct UnitState: got %v, want %v", got, want) } }