diff --git a/grpc/pb/patch_test.go b/grpc/pb/patch_test.go new file mode 100644 index 0000000..e5a78a0 --- /dev/null +++ b/grpc/pb/patch_test.go @@ -0,0 +1,46 @@ +package pb + +import ( + "encoding/json" + "testing" +) + +// An empty ApplyUpdatesCmd must stay an empty object on the wire, so an old +// agent and a new server, or a new agent and an old server, agree that it +// means "install everything, no reboot, no deadline". +func TestApplyUpdatesCmdEmptyIsEmptyObject(t *testing.T) { + b, err := json.Marshal(ApplyUpdatesCmd{}) + if err != nil { + t.Fatal(err) + } + if string(b) != "{}" { + t.Fatalf("got %s, want {}", b) + } +} + +func TestPatchResultRoundTrip(t *testing.T) { + in := AgentMessage{PatchResult: &PatchResult{ + CommandId: "c1", Status: PatchStatusOK, OutputTail: "done", + PendingAfter: 0, RebootRequired: true, Rebooting: true, + }} + b, err := json.Marshal(in) + if err != nil { + t.Fatal(err) + } + var out AgentMessage + if err := json.Unmarshal(b, &out); err != nil { + t.Fatal(err) + } + if out.PatchResult == nil || *out.PatchResult != *in.PatchResult { + t.Fatalf("round trip lost data: %+v", out.PatchResult) + } +} + +func TestInventoryBootTimeOnWire(t *testing.T) { + b, _ := json.Marshal(InventoryReport{BootTimeUnix: 1757800000}) + var m map[string]any + _ = json.Unmarshal(b, &m) + if m["boot_time_unix"] != float64(1757800000) { + t.Fatalf("boot_time_unix missing: %s", b) + } +} diff --git a/grpc/pb/vantage.pb.go b/grpc/pb/vantage.pb.go index b4d2a1f..6d148c5 100644 --- a/grpc/pb/vantage.pb.go +++ b/grpc/pb/vantage.pb.go @@ -123,6 +123,7 @@ type InventoryReport struct { Partitions []PartitionReport `json:"partitions,omitempty"` Kernel string `json:"kernel,omitempty"` RebootRequired bool `json:"reboot_required,omitempty"` + BootTimeUnix int64 `json:"boot_time_unix,omitempty"` // every report; proves a reboot happened } type InventoryReportResponse struct{} @@ -161,7 +162,40 @@ type ReportChecksRequest struct { } type ReportChecksResponse struct{} -type ApplyUpdatesCmd struct{} +// ApplyUpdatesCmd installs pending OS updates. The zero value means what the +// command always meant: every pending update, no reboot, no deadline. That is +// what keeps old servers and new agents, and new servers and old agents, +// compatible - but only in that direction for Scope: an agent that predates +// these fields installs everything even when asked for security only, which +// is why the control plane gates on agent version before sending a scope. +type ApplyUpdatesCmd struct { + Scope string `json:"scope,omitempty"` // "" or PatchScopeAll | PatchScopeSecurity + RebootIfRequired bool `json:"reboot_if_required,omitempty"` // reboot only if the OS reports one is owed + DeadlineUnix int64 `json:"deadline_unix,omitempty"` // 0 = none; the agent caps the upgrade at 2h +} + +const ( + PatchScopeAll = "all" + PatchScopeSecurity = "security" + + PatchStatusOK = "ok" + PatchStatusFailed = "failed" + PatchStatusUnsupported = "unsupported" + PatchStatusBusy = "busy" +) + +// PatchResult answers an ApplyUpdatesCmd. Rebooting is sent immediately before +// the agent restarts the host, so the control plane knows to wait for a +// post-boot inventory report rather than a second result. +type PatchResult struct { + CommandId string `json:"command_id"` + Status string `json:"status"` + Message string `json:"message,omitempty"` + OutputTail string `json:"output_tail,omitempty"` // at most 64KB, newest bytes + PendingAfter int32 `json:"pending_after"` // -1 when the post-apply check failed + RebootRequired bool `json:"reboot_required,omitempty"` + Rebooting bool `json:"rebooting,omitempty"` +} type OpenProxyCmd struct { ProxyId string `json:"proxy_id"` @@ -240,6 +274,7 @@ type AgentMessage struct { StepOutput *StepOutputChunk `json:"step_output,omitempty"` WorkloadLogsResult *WorkloadLogsResult `json:"workload_logs_result,omitempty"` + PatchResult *PatchResult `json:"patch_result,omitempty"` } type AgentReady struct{} diff --git a/proto/vantage/v1/vantage.proto b/proto/vantage/v1/vantage.proto index 4ae108b..b21f7b1 100644 --- a/proto/vantage/v1/vantage.proto +++ b/proto/vantage/v1/vantage.proto @@ -109,6 +109,7 @@ message AgentMessage { StepResult step_result = 5; StepOutputChunk step_output = 6; WorkloadLogsResult workload_logs_result = 7; + PatchResult patch_result = 8; } } @@ -168,9 +169,9 @@ message InventoryReport { uint64 swap_used = 7; repeated PartitionReport partitions = 8; string kernel = 9; - // Set on static snapshots only. The agent never reboots; it reports that one - // is owed and leaves the decision to a person or a workflow. + // Set on static snapshots only. The agent reboots a host only when an ApplyUpdatesCmd asks it to and the OS reports a reboot is owed. bool reboot_required = 10; + int64 boot_time_unix = 11; // every report; proves a reboot happened } message InventoryReportResponse { @@ -220,7 +221,19 @@ message ReportChecksResponse { } message ApplyUpdatesCmd { + string scope = 1; // "" or "all" | "security" + bool reboot_if_required = 2; // reboot only if the OS reports one is owed + int64 deadline_unix = 3; // 0 = none; the agent caps the upgrade at 2h +} +message PatchResult { + string command_id = 1; + string status = 2; // ok | failed | unsupported | busy + string message = 3; + string output_tail = 4; // at most 64KB, newest bytes kept + int32 pending_after = 5; // -1 when the post-apply check failed + bool reboot_required = 6; + bool rebooting = 7; // sent just before the agent reboots itself } message ServerCommand {