Classify and enrich guest services

This commit is contained in:
Maxim
2026-08-06 23:24:43 +03:00
parent 331f0ee39a
commit a2525b53f0
4 changed files with 121 additions and 7 deletions

112
agent.go
View File

@@ -58,10 +58,18 @@ type AgentInterface struct {
}
type AgentService struct {
Name string `json:"name"`
State string `json:"state"`
SubState string `json:"subState"`
Description string `json:"description"`
Name string `json:"name"`
State string `json:"state"`
SubState string `json:"subState"`
Description string `json:"description"`
Origin string `json:"origin"`
UnitPath string `json:"unitPath,omitempty"`
Package string `json:"package,omitempty"`
Restarts uint64 `json:"restarts"`
ExitCode int `json:"exitCode"`
StartedAt int64 `json:"startedAt,omitempty"`
MemoryBytes uint64 `json:"memoryBytes,omitempty"`
CPUSeconds float64 `json:"cpuSeconds,omitempty"`
}
type AgentIssue struct {
@@ -595,7 +603,7 @@ func runAgentCommand(timeout time.Duration, name string, args ...string) string
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
output, err := exec.CommandContext(ctx, name, args...).Output()
if err != nil {
if err != nil && len(output) == 0 {
return ""
}
return string(output)
@@ -622,7 +630,99 @@ func collectAgentServices() []AgentService {
break
}
}
return result
return enrichAgentServices(result)
}
func enrichAgentServices(services []AgentService) []AgentService {
if len(services) == 0 {
return services
}
args := []string{"show", "--no-pager", "--property=Id", "--property=FragmentPath", "--property=NRestarts", "--property=ExecMainStatus", "--property=ActiveEnterTimestampUSec", "--property=MemoryCurrent", "--property=CPUUsageNSec"}
for _, service := range services {
args = append(args, service.Name)
}
show := runAgentCommand(8*time.Second, "systemctl", args...)
properties := map[string]map[string]string{}
paths := map[string]string{}
for _, record := range strings.Split(show, "\n\n") {
values := map[string]string{}
for _, line := range strings.Split(record, "\n") {
key, value, ok := strings.Cut(line, "=")
if ok {
values[key] = value
}
}
if id := values["Id"]; id != "" {
properties[id] = values
paths[id] = values["FragmentPath"]
}
}
pathArgs := []string{"-S"}
for _, service := range services {
if path := paths[service.Name]; path != "" {
pathArgs = append(pathArgs, path)
}
}
owners := map[string]string{}
packages := []string{}
if len(pathArgs) > 1 {
output := runAgentCommand(8*time.Second, "dpkg-query", pathArgs...)
for _, line := range strings.Split(output, "\n") {
left, path, ok := strings.Cut(line, ": ")
if !ok {
continue
}
pkg := strings.TrimSpace(strings.Split(left, ",")[0])
path = strings.TrimSpace(path)
if pkg != "" && path != "" {
owners[path] = pkg
packages = append(packages, pkg)
}
}
}
priorities := map[string]string{}
if len(packages) > 0 {
packageArgs := []string{"-W", "-f=${binary:Package}\t${Priority}\n"}
packageArgs = append(packageArgs, packages...)
output := runAgentCommand(8*time.Second, "dpkg-query", packageArgs...)
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) >= 2 {
priorities[fields[0]] = fields[1]
}
}
}
for i := range services {
service := &services[i]
service.UnitPath = paths[service.Name]
service.Package = owners[service.UnitPath]
service.Origin = classifyAgentService(service.UnitPath, service.Package, priorities[service.Package])
values := properties[service.Name]
service.Restarts, _ = strconv.ParseUint(values["NRestarts"], 10, 64)
exit, _ := strconv.ParseInt(values["ExecMainStatus"], 10, 32)
service.ExitCode = int(exit)
started, _ := strconv.ParseInt(values["ActiveEnterTimestampUSec"], 10, 64)
service.StartedAt = started / 1_000_000
service.MemoryBytes, _ = strconv.ParseUint(values["MemoryCurrent"], 10, 64)
cpu, _ := strconv.ParseUint(values["CPUUsageNSec"], 10, 64)
service.CPUSeconds = float64(cpu) / 1_000_000_000
}
return services
}
func classifyAgentService(path, pkg, priority string) string {
switch {
case strings.HasPrefix(path, "/etc/systemd/") || strings.HasPrefix(path, "/usr/local/"):
return "local"
case priority == "required" || priority == "important" || priority == "standard":
return "system"
case pkg != "":
return "added"
case strings.HasPrefix(path, "/run/systemd/"):
return "runtime"
default:
return "system"
}
}
func collectAgentIssues() []AgentIssue {