Compare commits
1
Commits
agent/v1.4.0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f552006318 |
@@ -9,7 +9,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
gitea.hostxtra.co.uk/vantage/vantage-shared v0.5.0
|
||||
gitea.hostxtra.co.uk/vantage/vantage-shared v0.6.0
|
||||
golang.org/x/net v0.58.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260908043556-f8649ddbbfe6 // indirect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
gitea.hostxtra.co.uk/vantage/vantage-shared v0.5.0 h1:xwSIEkQKTd4Qk+BYHvoGN+h84Isr2h5qqnitUWF1m2w=
|
||||
gitea.hostxtra.co.uk/vantage/vantage-shared v0.5.0/go.mod h1:Zo66XhqF8No3dveIowLCepvMxVg8KnhsNMz0k0Xpuck=
|
||||
gitea.hostxtra.co.uk/vantage/vantage-shared v0.6.0 h1:EtojZ1d3cN9foHpc/CAI3KzBewYGn4sKWdkWs2MV78Q=
|
||||
gitea.hostxtra.co.uk/vantage/vantage-shared v0.6.0/go.mod h1:Zo66XhqF8No3dveIowLCepvMxVg8KnhsNMz0k0Xpuck=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
|
||||
@@ -120,12 +120,12 @@ func reportPendingUpdates(cfg *config.Config) int {
|
||||
}
|
||||
list := make([]pb.PackageUpdate, len(pkgs))
|
||||
for i, p := range pkgs {
|
||||
list[i] = pb.PackageUpdate{Name: p.Name, CurrentVersion: p.CurrentVersion, NewVersion: p.NewVersion}
|
||||
list[i] = pb.PackageUpdate{Name: p.Name, CurrentVersion: p.CurrentVersion, NewVersion: p.NewVersion, Phased: p.Phased}
|
||||
}
|
||||
client, err := grpcclient.New(cfg.ServerURL, cfg.TLS)
|
||||
if err != nil {
|
||||
log.Printf("post-apply report dial: %v", err)
|
||||
return len(pkgs)
|
||||
return updates.CountInstallable(pkgs)
|
||||
}
|
||||
defer client.Close()
|
||||
if err := client.ReportUpdates(cfg.ServerID, cfg.AgentToken, list); err != nil {
|
||||
|
||||
@@ -406,6 +406,7 @@ func runUpdateCheck(ctx context.Context, cfg *config.Config) {
|
||||
Name: p.Name,
|
||||
CurrentVersion: p.CurrentVersion,
|
||||
NewVersion: p.NewVersion,
|
||||
Phased: p.Phased,
|
||||
}
|
||||
}
|
||||
client, err := grpcclient.New(cfg.ServerURL, cfg.TLS)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Ubuntu phases some -updates releases: a host is selected at random for each
|
||||
// update, and until it is, apt lists the package as upgradable while an
|
||||
// upgrade defers it. Reporting those as pending made a freshly patched host
|
||||
// look unpatched, so they are flagged and left out of pending counts. The
|
||||
// -security pocket is never phased.
|
||||
|
||||
// aptPhasedDeferred asks apt which upgrades it would defer, by simulating an
|
||||
// upgrade. The simulation takes no lock and changes nothing. LC_ALL=C keeps
|
||||
// the heading parsePhasedDeferred looks for in English. Any failure reports no
|
||||
// phased packages: the update list is then exactly what it was before this
|
||||
// existed, never shorter.
|
||||
func aptPhasedDeferred() map[string]bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "apt-get", "-s", "-o", "Debug::NoLocking=1", "upgrade")
|
||||
cmd.Env = append(os.Environ(), "LC_ALL=C", "DEBIAN_FRONTEND=noninteractive")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return parsePhasedDeferred(string(out))
|
||||
}
|
||||
|
||||
// parsePhasedDeferred reads the package names apt lists under "The following
|
||||
// upgrades have been deferred due to phasing:". The names follow on indented
|
||||
// lines; the block ends at the first line that is not indented.
|
||||
func parsePhasedDeferred(out string) map[string]bool {
|
||||
phased := map[string]bool{}
|
||||
in := false
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
if strings.Contains(line, "deferred due to phasing") {
|
||||
in = true
|
||||
continue
|
||||
}
|
||||
if !in {
|
||||
continue
|
||||
}
|
||||
if line == "" || (line[0] != ' ' && line[0] != '\t') {
|
||||
in = false
|
||||
continue
|
||||
}
|
||||
for _, name := range strings.Fields(line) {
|
||||
phased[name] = true
|
||||
}
|
||||
}
|
||||
return phased
|
||||
}
|
||||
|
||||
// markPhased flags the updates apt would defer.
|
||||
func markPhased(ups []PackageUpdate, phased map[string]bool) {
|
||||
for i := range ups {
|
||||
if phased[ups[i].Name] {
|
||||
ups[i].Phased = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CountInstallable is the number of updates an upgrade would actually install
|
||||
// now: phased updates are pending but not yet installable.
|
||||
func CountInstallable(ups []PackageUpdate) int {
|
||||
n := 0
|
||||
for _, u := range ups {
|
||||
if !u.Phased {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const simulatedUpgrade = `Reading package lists...
|
||||
Building dependency tree...
|
||||
Reading state information...
|
||||
Calculating upgrade...
|
||||
The following upgrades have been deferred due to phasing:
|
||||
libgssapi-krb5-2 libk5crypto3 libkrb5-3 libkrb5support0 libnetplan1
|
||||
netplan-generator netplan.io python3-netplan
|
||||
The following packages will be upgraded:
|
||||
curl libcurl4
|
||||
2 upgraded, 0 newly installed, 0 to remove and 8 not upgraded.
|
||||
Inst curl [8.5.0-2ubuntu10.5] (8.5.0-2ubuntu10.6 Ubuntu:24.04/noble-updates [amd64])
|
||||
`
|
||||
|
||||
func TestParsePhasedDeferred(t *testing.T) {
|
||||
got := parsePhasedDeferred(simulatedUpgrade)
|
||||
want := map[string]bool{
|
||||
"libgssapi-krb5-2": true, "libk5crypto3": true, "libkrb5-3": true, "libkrb5support0": true,
|
||||
"libnetplan1": true, "netplan-generator": true, "netplan.io": true, "python3-netplan": true,
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v\nwant %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePhasedDeferredNone(t *testing.T) {
|
||||
out := "Calculating upgrade...\nThe following packages will be upgraded:\n curl\n1 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.\n"
|
||||
if got := parsePhasedDeferred(out); len(got) != 0 {
|
||||
t.Fatalf("got %v, want none", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Packages kept back for other reasons are not phased and must not be flagged.
|
||||
func TestParsePhasedDeferredIgnoresKeptBack(t *testing.T) {
|
||||
out := "The following packages have been kept back:\n linux-generic\nThe following upgrades have been deferred due to phasing:\n netplan.io\n0 upgraded\n"
|
||||
got := parsePhasedDeferred(out)
|
||||
if !reflect.DeepEqual(got, map[string]bool{"netplan.io": true}) {
|
||||
t.Fatalf("got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkPhased(t *testing.T) {
|
||||
ups := []PackageUpdate{{Name: "curl"}, {Name: "netplan.io"}}
|
||||
markPhased(ups, map[string]bool{"netplan.io": true})
|
||||
if ups[0].Phased || !ups[1].Phased {
|
||||
t.Fatalf("got %+v", ups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountInstallable(t *testing.T) {
|
||||
ups := []PackageUpdate{{Name: "a"}, {Name: "b", Phased: true}, {Name: "c"}}
|
||||
if got := CountInstallable(ups); got != 2 {
|
||||
t.Fatalf("got %d, want 2", got)
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ type PackageUpdate struct {
|
||||
Name string
|
||||
CurrentVersion string
|
||||
NewVersion string
|
||||
Phased bool // Ubuntu phased update this host is not yet selected for
|
||||
}
|
||||
|
||||
// CheckAvailable lists pending OS updates.
|
||||
|
||||
@@ -30,7 +30,11 @@ func detectPM() string {
|
||||
func checkAvailable() ([]PackageUpdate, error) {
|
||||
switch detectPM() {
|
||||
case "apt":
|
||||
return checkApt()
|
||||
ups, err := checkApt()
|
||||
if err == nil && len(ups) > 0 {
|
||||
markPhased(ups, aptPhasedDeferred())
|
||||
}
|
||||
return ups, err
|
||||
case "dnf":
|
||||
return checkDnfYum("dnf")
|
||||
case "yum":
|
||||
|
||||
Reference in New Issue
Block a user