126 lines
3.8 KiB
Go
126 lines
3.8 KiB
Go
package agentsync
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"runtime"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/config"
|
|
grpcclient "gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc"
|
|
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc/pb"
|
|
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/packages"
|
|
)
|
|
|
|
// collectPackagesFlag is written by the 30s key poll and read by the hourly
|
|
// package loop — two different goroutines, hence the atomic.
|
|
//
|
|
// It defaults to false, so an agent that has not yet completed a poll, or is
|
|
// talking to a server too old to send the field, collects nothing. Off is the
|
|
// safe default: collecting without a licence costs the customer storage they
|
|
// are not paying for.
|
|
var collectPackagesFlag atomic.Bool
|
|
|
|
// firstPoll closes once a SyncKeys response has set the flag above.
|
|
//
|
|
// Without it the boot-time package report loses a race it can only lose: the
|
|
// hourly loop starts before the first poll, reads a flag that is still false by
|
|
// construction, and skips — so a freshly installed agent reports no packages for
|
|
// an hour and the server shows nothing to scan.
|
|
// How long the boot package report waits for that first poll. Two poll
|
|
// intervals plus slack: long enough to cover one failed attempt, short enough
|
|
// that a dead control plane does not hold the OS-update report hostage.
|
|
const firstPollWait = 90 * time.Second
|
|
|
|
var (
|
|
firstPoll = make(chan struct{})
|
|
firstPollOnce sync.Once
|
|
)
|
|
|
|
func markFirstPoll() { firstPollOnce.Do(func() { close(firstPoll) }) }
|
|
|
|
// waitFirstPoll blocks until the flag is known, or gives up. The wait is
|
|
// bounded because this loop also reports OS updates, which do not depend on the
|
|
// flag at all — a control plane that cannot be polled must not silence those too.
|
|
func waitFirstPoll(ctx context.Context, limit time.Duration) {
|
|
t := time.NewTimer(limit)
|
|
defer t.Stop()
|
|
select {
|
|
case <-firstPoll:
|
|
case <-t.C:
|
|
log.Printf("package collection: no SyncKeys response within %s, collecting nothing this round", limit)
|
|
case <-ctx.Done():
|
|
}
|
|
}
|
|
|
|
func collectPackagesEnabled() bool { return collectPackagesFlag.Load() }
|
|
|
|
// reportPackages offers a hash of the installed package set and sends the full
|
|
// list only if the server does not already hold it.
|
|
//
|
|
// It runs on the same hourly cadence as the update check because a package set
|
|
// changes on roughly the same schedule, and reusing that loop means one timer
|
|
// rather than two.
|
|
func reportPackages(client *grpcclient.Client, cfg *config.Config) {
|
|
if runtime.GOOS != "linux" {
|
|
return
|
|
}
|
|
if !collectPackagesEnabled() {
|
|
return
|
|
}
|
|
|
|
osrel, pkgs, err := packages.Collect()
|
|
if err != nil {
|
|
log.Printf("package collection error: %v", err)
|
|
return
|
|
}
|
|
|
|
pbOS := pb.OSRelease{
|
|
Family: osrel.Family,
|
|
VersionId: osrel.VersionID,
|
|
Arch: osrel.Arch,
|
|
}
|
|
hash := packages.Hash(pkgs)
|
|
|
|
// The offer: hash only, no body. On an unchanged host this is the whole
|
|
// exchange, which is the point of the handshake.
|
|
needFull, err := client.ReportPackages(&pb.ReportPackagesRequest{
|
|
ServerId: cfg.ServerID,
|
|
AgentToken: cfg.AgentToken,
|
|
Hash: hash,
|
|
Os: pbOS,
|
|
})
|
|
if err != nil {
|
|
log.Printf("ReportPackages offer error: %v", err)
|
|
return
|
|
}
|
|
if !needFull {
|
|
return
|
|
}
|
|
|
|
pbPkgs := make([]pb.InstalledPackage, len(pkgs))
|
|
for i, p := range pkgs {
|
|
pbPkgs[i] = pb.InstalledPackage{
|
|
Name: p.Name,
|
|
Version: p.Version,
|
|
Epoch: int32(p.Epoch),
|
|
Arch: p.Arch,
|
|
SourceName: p.SourceName,
|
|
}
|
|
}
|
|
|
|
if _, err := client.ReportPackages(&pb.ReportPackagesRequest{
|
|
ServerId: cfg.ServerID,
|
|
AgentToken: cfg.AgentToken,
|
|
Hash: hash,
|
|
Os: pbOS,
|
|
Packages: pbPkgs,
|
|
}); err != nil {
|
|
log.Printf("ReportPackages full error: %v", err)
|
|
return
|
|
}
|
|
log.Printf("reported %d installed packages", len(pkgs))
|
|
}
|