From c20c7c231d806f6ab407b971964aa681403efa63 Mon Sep 17 00:00:00 2001 From: Dhamith Hewamullage Date: Wed, 30 Sep 2026 01:43:21 +0530 Subject: [PATCH 1/5] sign agent builds --- Makefile | 18 ++++++-- internal/update/update.go | 69 ++++++++++++++++++++++++++++ internal/update/update_test.go | 45 +++++++++++++++++++ tools/sign/main.go | 82 ++++++++++++++++++++++++++++++++++ 4 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 internal/update/update.go create mode 100644 internal/update/update_test.go create mode 100644 tools/sign/main.go diff --git a/Makefile b/Makefile index 6f7b563..127439d 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,23 @@ -.PHONY: proto web clean build-all build-collector build-agent build-alertprocessor build-client pack-all pack-collector pack-agent pack-alertprocessor pack-client +.PHONY: proto web signing-key clean build-all build-collector build-agent build-alertprocessor build-client pack-all pack-collector pack-agent pack-alertprocessor pack-client # the version -version and the dashboard show, like v3.0.0-16-g2519821 VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) -LDFLAGS := -ldflags "-X github.com/dhamith93/SyMon/internal/version.Version=$(VERSION)" +# agents only install updates from the dashboard when they are signed with +# this key, which stays where the builds are made and never on the servers. +# Its public key is built into agents, and is empty until make signing-key. +SIGNING_KEY ?= $(HOME)/.config/symon/agent-signing.key +UPDATE_KEY = $(shell cat $(SIGNING_KEY).pub 2>/dev/null) +# recursive, so a key made earlier in the same run is picked up +LDFLAGS = -ldflags "-X github.com/dhamith93/SyMon/internal/version.Version=$(VERSION) -X github.com/dhamith93/SyMon/internal/update.PublicKey=$(UPDATE_KEY)" proto: cd internal && protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ api/api.proto alertapi/alertapi.proto +signing-key: + go run ./tools/sign keygen -key $(SIGNING_KEY) + web: cd client/web && npm ci && npm run build @@ -51,7 +60,7 @@ pack-collector: build-collector cd release/ && tar -cvf collector_linux_x86_64.tar.gz collector_linux_x86_64 rm -rf release/collector_linux_x86_64 -pack-agent: build-agent +pack-agent: signing-key build-agent mkdir -p release/agent_linux_x86_64 cp agent/agent_linux_x86_64 release/agent_linux_x86_64 cp agent/.env-example release/agent_linux_x86_64 @@ -67,12 +76,13 @@ pack-alertprocessor: build-alertprocessor # the agent builds are what new hosts download from the dashboard. # GOARM=6 also runs on ARMv7, so one arm build covers every Raspberry Pi. -pack-client: build-client +pack-client: signing-key build-client mkdir -p release/client_linux_x86_64/downloads cp client/client_linux_x86_64 release/client_linux_x86_64 cd agent && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build $(LDFLAGS) -o ../release/client_linux_x86_64/downloads/agent-linux-amd64 cd agent && GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build $(LDFLAGS) -o ../release/client_linux_x86_64/downloads/agent-linux-arm64 cd agent && GOOS=linux GOARCH=arm GOARM=6 CGO_ENABLED=0 go build $(LDFLAGS) -o ../release/client_linux_x86_64/downloads/agent-linux-arm + go run ./tools/sign sign -key $(SIGNING_KEY) release/client_linux_x86_64/downloads/agent-linux-amd64 release/client_linux_x86_64/downloads/agent-linux-arm64 release/client_linux_x86_64/downloads/agent-linux-arm cp client/.env-example release/client_linux_x86_64 cp client/Dockerfile release/client_linux_x86_64 cd release/ && tar -cvf client_linux_x86_64.tar.gz client_linux_x86_64 diff --git a/internal/update/update.go b/internal/update/update.go new file mode 100644 index 0000000..b6c9379 --- /dev/null +++ b/internal/update/update.go @@ -0,0 +1,69 @@ +// Package update signs agent builds and checks them before an agent +// installs one. Builds are signed with an ed25519 key that stays where they +// are built, so a build an agent accepts cannot come from anyone else. +package update + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + "strings" +) + +// PublicKey is the base64 key agent builds are signed with, set at build +// time. An agent built without it does not install updates. +var PublicKey = "" + +// ErrNoKey is an agent built without PublicKey +var ErrNoKey = errors.New("this agent was built without an update key, update it with the install command") + +// Verify checks a build against its signature file, which holds the +// base64 signature +func Verify(build []byte, signature []byte) error { + if PublicKey == "" { + return ErrNoKey + } + key, err := base64.StdEncoding.DecodeString(PublicKey) + if err != nil || len(key) != ed25519.PublicKeySize { + return errors.New("this agent's update key is broken") + } + sig, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(signature))) + if err != nil || len(sig) != ed25519.SignatureSize { + return errors.New("the signature file is not a signature") + } + if !ed25519.Verify(ed25519.PublicKey(key), build, sig) { + return errors.New("the build is not signed with this agent's update key") + } + return nil +} + +// NewKey returns a private key as the text kept in the key file, and its +// public key +func NewKey() (private string, public string, err error) { + publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return "", "", err + } + return base64.StdEncoding.EncodeToString(privateKey.Seed()), base64.StdEncoding.EncodeToString(publicKey), nil +} + +// ParsePrivateKey reads the text of a key file +func ParsePrivateKey(text string) (ed25519.PrivateKey, error) { + seed, err := base64.StdEncoding.DecodeString(strings.TrimSpace(text)) + if err != nil || len(seed) != ed25519.SeedSize { + return nil, fmt.Errorf("not a SyMon signing key") + } + return ed25519.NewKeyFromSeed(seed), nil +} + +// Sign returns the text of a build's signature file +func Sign(key ed25519.PrivateKey, build []byte) string { + return base64.StdEncoding.EncodeToString(ed25519.Sign(key, build)) + "\n" +} + +// PublicKeyOf returns the base64 public key of a private key +func PublicKeyOf(key ed25519.PrivateKey) string { + return base64.StdEncoding.EncodeToString(key.Public().(ed25519.PublicKey)) +} diff --git a/internal/update/update_test.go b/internal/update/update_test.go new file mode 100644 index 0000000..ba49af4 --- /dev/null +++ b/internal/update/update_test.go @@ -0,0 +1,45 @@ +package update + +import ( + "strings" + "testing" +) + +func TestSignAndVerify(t *testing.T) { + private, public, err := NewKey() + if err != nil { + t.Fatal(err) + } + key, err := ParsePrivateKey(private) + if err != nil || PublicKeyOf(key) != public { + t.Fatalf("expected the key to read back, got %v", err) + } + build := []byte("an agent build") + signature := Sign(key, build) + + PublicKey = "" + if err := Verify(build, []byte(signature)); err != ErrNoKey { + t.Errorf("expected ErrNoKey without a key, got %v", err) + } + + PublicKey = public + t.Cleanup(func() { PublicKey = "" }) + if err := Verify(build, []byte(signature)); err != nil { + t.Errorf("expected the signature to check out, got %v", err) + } + if err := Verify([]byte("another build"), []byte(signature)); err == nil || !strings.Contains(err.Error(), "not signed") { + t.Errorf("expected a changed build to fail, got %v", err) + } + _, otherPublic, _ := NewKey() + PublicKey = otherPublic + if err := Verify(build, []byte(signature)); err == nil { + t.Error("expected a build signed with another key to fail") + } + PublicKey = public + if err := Verify(build, []byte("not a signature")); err == nil { + t.Error("expected a garbled signature to fail") + } + if _, err := ParsePrivateKey("short"); err == nil { + t.Error("expected a garbled key file to fail") + } +} diff --git a/tools/sign/main.go b/tools/sign/main.go new file mode 100644 index 0000000..85ea5ea --- /dev/null +++ b/tools/sign/main.go @@ -0,0 +1,82 @@ +// sign makes the key agent builds are signed with, and signs them. Agents +// install an update from the dashboard only when it is signed with this key. +// +// go run ./tools/sign keygen -key ~/.config/symon/agent-signing.key +// go run ./tools/sign sign -key ~/.config/symon/agent-signing.key agent-linux-amd64 ... +// +// keygen writes the public key next to the key, as .pub, which the +// Makefile builds into agents. It leaves an existing key alone. +package main + +import ( + "errors" + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/dhamith93/SyMon/internal/update" +) + +func main() { + if len(os.Args) < 2 { + usage() + } + flags := flag.NewFlagSet(os.Args[1], flag.ExitOnError) + keyPath := flags.String("key", "", "the signing key file") + flags.Parse(os.Args[2:]) + if *keyPath == "" { + usage() + } + + switch os.Args[1] { + case "keygen": + keygen(*keyPath) + case "sign": + sign(*keyPath, flags.Args()) + default: + usage() + } +} + +func keygen(path string) { + if _, err := os.Stat(path); err == nil { + return + } + private, public, err := update.NewKey() + check(err) + check(os.MkdirAll(filepath.Dir(path), 0700)) + check(os.WriteFile(path, []byte(private+"\n"), 0600)) + check(os.WriteFile(path+".pub", []byte(public+"\n"), 0644)) + fmt.Fprintf(os.Stderr, "Created the agent signing key %s. Back it up: agents only accept updates signed with it.\n", path) +} + +func sign(path string, builds []string) { + text, err := os.ReadFile(path) + if errors.Is(err, os.ErrNotExist) { + fail("there is no signing key at " + path + ", create one with: make signing-key") + } + check(err) + key, err := update.ParsePrivateKey(string(text)) + check(err) + for _, build := range builds { + data, err := os.ReadFile(build) + check(err) + check(os.WriteFile(build+".sig", []byte(update.Sign(key, data)), 0644)) + } +} + +func check(err error) { + if err != nil { + fail(err.Error()) + } +} + +func fail(message string) { + fmt.Fprintln(os.Stderr, message) + os.Exit(1) +} + +func usage() { + fail("usage: sign keygen -key FILE | sign sign -key FILE BUILD...") +} From b9223d8a973e3552279442acbfc855af69411be6 Mon Sep 17 00:00:00 2001 From: Dhamith Hewamullage Date: Wed, 30 Sep 2026 01:44:45 +0530 Subject: [PATCH 2/5] let the collector hand agents an update an admin asked for --- internal/api/api.go | 38 +- internal/api/api.pb.go | 777 ++++++++++++------ internal/api/api.proto | 41 +- internal/api/api_grpc.pb.go | 50 +- internal/store/agents.go | 63 ++ internal/store/agents_test.go | 79 ++ .../store/migrations/011_agent_updates.sql | 11 + internal/store/query.go | 22 +- 8 files changed, 839 insertions(+), 242 deletions(-) create mode 100644 internal/store/agents.go create mode 100644 internal/store/agents_test.go create mode 100644 internal/store/migrations/011_agent_updates.sql diff --git a/internal/api/api.go b/internal/api/api.go index f8ff257..3952caf 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -4,7 +4,9 @@ import ( "context" "encoding/json" "errors" + "fmt" "regexp" + "strings" "sync" "time" @@ -100,15 +102,39 @@ func (s *Server) InitAgent(ctx context.Context, in *ServerInfo) (*Message, error return &Message{Body: "agent added"}, nil } -func (s *Server) HandlePing(ctx context.Context, in *ServerInfo) (*Message, error) { +// HandlePing records that the host is alive and what its agent says about +// itself, and hands the agent an update an admin asked for +func (s *Server) HandlePing(ctx context.Context, in *ServerInfo) (*PingResponse, error) { host := in.ServerName if authenticated, ok := agentHost(ctx); ok { host = authenticated } - if err := s.Store.Heartbeat(ctx, host, time.Now()); err != nil { + update, err := s.Store.AgentCheckIn(ctx, host, time.Now(), in.AgentVersion, in.Arch, in.UpdateError) + if err != nil { return nil, agentStatus(host, err) } - return &Message{Body: "pong"}, nil + if in.UpdateError != "" { + logger.Log("error", "agent on "+host+" could not update: "+in.UpdateError) + } + response := &PingResponse{Body: "pong"} + if update != nil { + response.Update = &AgentUpdate{Version: update.Version, DownloadUrl: update.URL, RequestedAt: unix(update.RequestedAt)} + } + return response, nil +} + +// RequestAgentUpdate asks agents to install the build the dashboard hands +// out. Agents that cannot update themselves are left out. +func (s *Server) RequestAgentUpdate(ctx context.Context, in *AgentUpdateRequest) (*AgentUpdateResult, error) { + if in.Version == "" || !strings.HasPrefix(in.DownloadUrl, "http://") && !strings.HasPrefix(in.DownloadUrl, "https://") { + return nil, status.Error(codes.InvalidArgument, "an update needs a version and the dashboard's address") + } + requested, err := s.Store.RequestAgentUpdate(ctx, in.Hosts, in.Version, in.DownloadUrl) + if err != nil { + return nil, toStatus(err) + } + logger.Log("info", fmt.Sprintf("agent update to %s asked of %d hosts by %q", in.Version, requested, in.By)) + return &AgentUpdateResult{Requested: int32(requested)}, nil } func (s *Server) HandleMonitorData(ctx context.Context, in *MonitorData) (*Message, error) { @@ -165,6 +191,12 @@ func (s *Server) Fleet(ctx context.Context, in *Void) (*FleetSummary, error) { WorstSeverity: int32(summary.WorstSeverity), Containers: int32(summary.Containers), AgentVersion: summary.AgentVersion, + CanUpdate: summary.CanUpdate, + UpdateVersion: summary.UpdateVersion, + UpdateError: summary.UpdateError, + } + if !summary.UpdateRequestedAt.IsZero() { + host.UpdateRequestedAt = summary.UpdateRequestedAt.Unix() } if days, ok := diskFull[summary.Name]; ok { host.DiskFullDays = &days diff --git a/internal/api/api.pb.go b/internal/api/api.pb.go index bc5b1ee..01b0ca9 100644 --- a/internal/api/api.pb.go +++ b/internal/api/api.pb.go @@ -101,10 +101,15 @@ func (x *Message) GetBody() string { return "" } +// agentVersion and arch are sent by agents that can update themselves, +// updateError when their last try failed type ServerInfo struct { state protoimpl.MessageState `protogen:"open.v1"` ServerName string `protobuf:"bytes,1,opt,name=serverName,proto3" json:"serverName,omitempty"` Timezone string `protobuf:"bytes,2,opt,name=timezone,proto3" json:"timezone,omitempty"` + AgentVersion string `protobuf:"bytes,3,opt,name=agentVersion,proto3" json:"agentVersion,omitempty"` + Arch string `protobuf:"bytes,4,opt,name=arch,proto3" json:"arch,omitempty"` + UpdateError string `protobuf:"bytes,5,opt,name=updateError,proto3" json:"updateError,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -153,6 +158,254 @@ func (x *ServerInfo) GetTimezone() string { return "" } +func (x *ServerInfo) GetAgentVersion() string { + if x != nil { + return x.AgentVersion + } + return "" +} + +func (x *ServerInfo) GetArch() string { + if x != nil { + return x.Arch + } + return "" +} + +func (x *ServerInfo) GetUpdateError() string { + if x != nil { + return x.UpdateError + } + return "" +} + +// An update an admin asked for. The agent downloads the build from +// downloadUrl, the dashboard, and installs it only when it is signed. +type AgentUpdate struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + DownloadUrl string `protobuf:"bytes,2,opt,name=downloadUrl,proto3" json:"downloadUrl,omitempty"` + RequestedAt int64 `protobuf:"varint,3,opt,name=requestedAt,proto3" json:"requestedAt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentUpdate) Reset() { + *x = AgentUpdate{} + mi := &file_api_api_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentUpdate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentUpdate) ProtoMessage() {} + +func (x *AgentUpdate) ProtoReflect() protoreflect.Message { + mi := &file_api_api_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentUpdate.ProtoReflect.Descriptor instead. +func (*AgentUpdate) Descriptor() ([]byte, []int) { + return file_api_api_proto_rawDescGZIP(), []int{3} +} + +func (x *AgentUpdate) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *AgentUpdate) GetDownloadUrl() string { + if x != nil { + return x.DownloadUrl + } + return "" +} + +func (x *AgentUpdate) GetRequestedAt() int64 { + if x != nil { + return x.RequestedAt + } + return 0 +} + +// body is field 1 like in Message, so agents from before updates still read it +type PingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Body string `protobuf:"bytes,1,opt,name=body,proto3" json:"body,omitempty"` + Update *AgentUpdate `protobuf:"bytes,2,opt,name=update,proto3" json:"update,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PingResponse) Reset() { + *x = PingResponse{} + mi := &file_api_api_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PingResponse) ProtoMessage() {} + +func (x *PingResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_api_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PingResponse.ProtoReflect.Descriptor instead. +func (*PingResponse) Descriptor() ([]byte, []int) { + return file_api_api_proto_rawDescGZIP(), []int{4} +} + +func (x *PingResponse) GetBody() string { + if x != nil { + return x.Body + } + return "" +} + +func (x *PingResponse) GetUpdate() *AgentUpdate { + if x != nil { + return x.Update + } + return nil +} + +type AgentUpdateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Hosts []string `protobuf:"bytes,1,rep,name=hosts,proto3" json:"hosts,omitempty"` + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + DownloadUrl string `protobuf:"bytes,3,opt,name=downloadUrl,proto3" json:"downloadUrl,omitempty"` + By string `protobuf:"bytes,4,opt,name=by,proto3" json:"by,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentUpdateRequest) Reset() { + *x = AgentUpdateRequest{} + mi := &file_api_api_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentUpdateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentUpdateRequest) ProtoMessage() {} + +func (x *AgentUpdateRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_api_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentUpdateRequest.ProtoReflect.Descriptor instead. +func (*AgentUpdateRequest) Descriptor() ([]byte, []int) { + return file_api_api_proto_rawDescGZIP(), []int{5} +} + +func (x *AgentUpdateRequest) GetHosts() []string { + if x != nil { + return x.Hosts + } + return nil +} + +func (x *AgentUpdateRequest) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *AgentUpdateRequest) GetDownloadUrl() string { + if x != nil { + return x.DownloadUrl + } + return "" +} + +func (x *AgentUpdateRequest) GetBy() string { + if x != nil { + return x.By + } + return "" +} + +type AgentUpdateResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + Requested int32 `protobuf:"varint,1,opt,name=requested,proto3" json:"requested,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentUpdateResult) Reset() { + *x = AgentUpdateResult{} + mi := &file_api_api_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentUpdateResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentUpdateResult) ProtoMessage() {} + +func (x *AgentUpdateResult) ProtoReflect() protoreflect.Message { + mi := &file_api_api_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentUpdateResult.ProtoReflect.Descriptor instead. +func (*AgentUpdateResult) Descriptor() ([]byte, []int) { + return file_api_api_proto_rawDescGZIP(), []int{6} +} + +func (x *AgentUpdateResult) GetRequested() int32 { + if x != nil { + return x.Requested + } + return 0 +} + type MonitorData struct { state protoimpl.MessageState `protogen:"open.v1"` MonitorData string `protobuf:"bytes,1,opt,name=monitorData,proto3" json:"monitorData,omitempty"` @@ -162,7 +415,7 @@ type MonitorData struct { func (x *MonitorData) Reset() { *x = MonitorData{} - mi := &file_api_api_proto_msgTypes[3] + mi := &file_api_api_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -174,7 +427,7 @@ func (x *MonitorData) String() string { func (*MonitorData) ProtoMessage() {} func (x *MonitorData) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[3] + mi := &file_api_api_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -187,7 +440,7 @@ func (x *MonitorData) ProtoReflect() protoreflect.Message { // Deprecated: Use MonitorData.ProtoReflect.Descriptor instead. func (*MonitorData) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{3} + return file_api_api_proto_rawDescGZIP(), []int{7} } func (x *MonitorData) GetMonitorData() string { @@ -209,7 +462,7 @@ type EnrollRequest struct { func (x *EnrollRequest) Reset() { *x = EnrollRequest{} - mi := &file_api_api_proto_msgTypes[4] + mi := &file_api_api_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -221,7 +474,7 @@ func (x *EnrollRequest) String() string { func (*EnrollRequest) ProtoMessage() {} func (x *EnrollRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[4] + mi := &file_api_api_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -234,7 +487,7 @@ func (x *EnrollRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EnrollRequest.ProtoReflect.Descriptor instead. func (*EnrollRequest) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{4} + return file_api_api_proto_rawDescGZIP(), []int{8} } func (x *EnrollRequest) GetToken() string { @@ -268,7 +521,7 @@ type EnrollResponse struct { func (x *EnrollResponse) Reset() { *x = EnrollResponse{} - mi := &file_api_api_proto_msgTypes[5] + mi := &file_api_api_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -280,7 +533,7 @@ func (x *EnrollResponse) String() string { func (*EnrollResponse) ProtoMessage() {} func (x *EnrollResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[5] + mi := &file_api_api_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -293,7 +546,7 @@ func (x *EnrollResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EnrollResponse.ProtoReflect.Descriptor instead. func (*EnrollResponse) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{5} + return file_api_api_proto_rawDescGZIP(), []int{9} } func (x *EnrollResponse) GetHostName() string { @@ -332,14 +585,20 @@ type HostSummary struct { // days until the first disk fills up, unset when none is filling up DiskFullDays *float64 `protobuf:"fixed64,16,opt,name=diskFullDays,proto3,oneof" json:"diskFullDays,omitempty"` // empty from agents older than versions - AgentVersion string `protobuf:"bytes,17,opt,name=agentVersion,proto3" json:"agentVersion,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + AgentVersion string `protobuf:"bytes,17,opt,name=agentVersion,proto3" json:"agentVersion,omitempty"` + // the agent updates itself when asked + CanUpdate bool `protobuf:"varint,18,opt,name=canUpdate,proto3" json:"canUpdate,omitempty"` + // an update asked for and not done yet, empty for none + UpdateVersion string `protobuf:"bytes,19,opt,name=updateVersion,proto3" json:"updateVersion,omitempty"` + UpdateRequestedAt int64 `protobuf:"varint,20,opt,name=updateRequestedAt,proto3" json:"updateRequestedAt,omitempty"` + UpdateError string `protobuf:"bytes,21,opt,name=updateError,proto3" json:"updateError,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *HostSummary) Reset() { *x = HostSummary{} - mi := &file_api_api_proto_msgTypes[6] + mi := &file_api_api_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -351,7 +610,7 @@ func (x *HostSummary) String() string { func (*HostSummary) ProtoMessage() {} func (x *HostSummary) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[6] + mi := &file_api_api_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -364,7 +623,7 @@ func (x *HostSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use HostSummary.ProtoReflect.Descriptor instead. func (*HostSummary) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{6} + return file_api_api_proto_rawDescGZIP(), []int{10} } func (x *HostSummary) GetName() string { @@ -486,6 +745,34 @@ func (x *HostSummary) GetAgentVersion() string { return "" } +func (x *HostSummary) GetCanUpdate() bool { + if x != nil { + return x.CanUpdate + } + return false +} + +func (x *HostSummary) GetUpdateVersion() string { + if x != nil { + return x.UpdateVersion + } + return "" +} + +func (x *HostSummary) GetUpdateRequestedAt() int64 { + if x != nil { + return x.UpdateRequestedAt + } + return 0 +} + +func (x *HostSummary) GetUpdateError() string { + if x != nil { + return x.UpdateError + } + return "" +} + type FleetSummary struct { state protoimpl.MessageState `protogen:"open.v1"` Hosts []*HostSummary `protobuf:"bytes,1,rep,name=hosts,proto3" json:"hosts,omitempty"` @@ -495,7 +782,7 @@ type FleetSummary struct { func (x *FleetSummary) Reset() { *x = FleetSummary{} - mi := &file_api_api_proto_msgTypes[7] + mi := &file_api_api_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -507,7 +794,7 @@ func (x *FleetSummary) String() string { func (*FleetSummary) ProtoMessage() {} func (x *FleetSummary) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[7] + mi := &file_api_api_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -520,7 +807,7 @@ func (x *FleetSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use FleetSummary.ProtoReflect.Descriptor instead. func (*FleetSummary) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{7} + return file_api_api_proto_rawDescGZIP(), []int{11} } func (x *FleetSummary) GetHosts() []*HostSummary { @@ -539,7 +826,7 @@ type HostRequest struct { func (x *HostRequest) Reset() { *x = HostRequest{} - mi := &file_api_api_proto_msgTypes[8] + mi := &file_api_api_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -551,7 +838,7 @@ func (x *HostRequest) String() string { func (*HostRequest) ProtoMessage() {} func (x *HostRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[8] + mi := &file_api_api_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -564,7 +851,7 @@ func (x *HostRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use HostRequest.ProtoReflect.Descriptor instead. func (*HostRequest) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{8} + return file_api_api_proto_rawDescGZIP(), []int{12} } func (x *HostRequest) GetHost() string { @@ -588,7 +875,7 @@ type HostSnapshot struct { func (x *HostSnapshot) Reset() { *x = HostSnapshot{} - mi := &file_api_api_proto_msgTypes[9] + mi := &file_api_api_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -600,7 +887,7 @@ func (x *HostSnapshot) String() string { func (*HostSnapshot) ProtoMessage() {} func (x *HostSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[9] + mi := &file_api_api_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -613,7 +900,7 @@ func (x *HostSnapshot) ProtoReflect() protoreflect.Message { // Deprecated: Use HostSnapshot.ProtoReflect.Descriptor instead. func (*HostSnapshot) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{9} + return file_api_api_proto_rawDescGZIP(), []int{13} } func (x *HostSnapshot) GetHost() string { @@ -668,7 +955,7 @@ type SeriesRequest struct { func (x *SeriesRequest) Reset() { *x = SeriesRequest{} - mi := &file_api_api_proto_msgTypes[10] + mi := &file_api_api_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -680,7 +967,7 @@ func (x *SeriesRequest) String() string { func (*SeriesRequest) ProtoMessage() {} func (x *SeriesRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[10] + mi := &file_api_api_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -693,7 +980,7 @@ func (x *SeriesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SeriesRequest.ProtoReflect.Descriptor instead. func (*SeriesRequest) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{10} + return file_api_api_proto_rawDescGZIP(), []int{14} } func (x *SeriesRequest) GetHost() string { @@ -755,7 +1042,7 @@ type Point struct { func (x *Point) Reset() { *x = Point{} - mi := &file_api_api_proto_msgTypes[11] + mi := &file_api_api_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -767,7 +1054,7 @@ func (x *Point) String() string { func (*Point) ProtoMessage() {} func (x *Point) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[11] + mi := &file_api_api_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -780,7 +1067,7 @@ func (x *Point) ProtoReflect() protoreflect.Message { // Deprecated: Use Point.ProtoReflect.Descriptor instead. func (*Point) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{11} + return file_api_api_proto_rawDescGZIP(), []int{15} } func (x *Point) GetTime() int64 { @@ -807,7 +1094,7 @@ type Series struct { func (x *Series) Reset() { *x = Series{} - mi := &file_api_api_proto_msgTypes[12] + mi := &file_api_api_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -819,7 +1106,7 @@ func (x *Series) String() string { func (*Series) ProtoMessage() {} func (x *Series) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[12] + mi := &file_api_api_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -832,7 +1119,7 @@ func (x *Series) ProtoReflect() protoreflect.Message { // Deprecated: Use Series.ProtoReflect.Descriptor instead. func (*Series) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{12} + return file_api_api_proto_rawDescGZIP(), []int{16} } func (x *Series) GetLabel() string { @@ -862,7 +1149,7 @@ type SeriesResponse struct { func (x *SeriesResponse) Reset() { *x = SeriesResponse{} - mi := &file_api_api_proto_msgTypes[13] + mi := &file_api_api_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -874,7 +1161,7 @@ func (x *SeriesResponse) String() string { func (*SeriesResponse) ProtoMessage() {} func (x *SeriesResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[13] + mi := &file_api_api_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -887,7 +1174,7 @@ func (x *SeriesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SeriesResponse.ProtoReflect.Descriptor instead. func (*SeriesResponse) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{13} + return file_api_api_proto_rawDescGZIP(), []int{17} } func (x *SeriesResponse) GetMetric() string { @@ -929,7 +1216,7 @@ type ProcessesRequest struct { func (x *ProcessesRequest) Reset() { *x = ProcessesRequest{} - mi := &file_api_api_proto_msgTypes[14] + mi := &file_api_api_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -941,7 +1228,7 @@ func (x *ProcessesRequest) String() string { func (*ProcessesRequest) ProtoMessage() {} func (x *ProcessesRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[14] + mi := &file_api_api_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -954,7 +1241,7 @@ func (x *ProcessesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ProcessesRequest.ProtoReflect.Descriptor instead. func (*ProcessesRequest) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{14} + return file_api_api_proto_rawDescGZIP(), []int{18} } func (x *ProcessesRequest) GetHost() string { @@ -982,7 +1269,7 @@ type ProcessesResponse struct { func (x *ProcessesResponse) Reset() { *x = ProcessesResponse{} - mi := &file_api_api_proto_msgTypes[15] + mi := &file_api_api_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -994,7 +1281,7 @@ func (x *ProcessesResponse) String() string { func (*ProcessesResponse) ProtoMessage() {} func (x *ProcessesResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[15] + mi := &file_api_api_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1007,7 +1294,7 @@ func (x *ProcessesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ProcessesResponse.ProtoReflect.Descriptor instead. func (*ProcessesResponse) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{15} + return file_api_api_proto_rawDescGZIP(), []int{19} } func (x *ProcessesResponse) GetTime() int64 { @@ -1033,7 +1320,7 @@ type NameList struct { func (x *NameList) Reset() { *x = NameList{} - mi := &file_api_api_proto_msgTypes[16] + mi := &file_api_api_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1045,7 +1332,7 @@ func (x *NameList) String() string { func (*NameList) ProtoMessage() {} func (x *NameList) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[16] + mi := &file_api_api_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1058,7 +1345,7 @@ func (x *NameList) ProtoReflect() protoreflect.Message { // Deprecated: Use NameList.ProtoReflect.Descriptor instead. func (*NameList) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{16} + return file_api_api_proto_rawDescGZIP(), []int{20} } func (x *NameList) GetNames() []string { @@ -1081,7 +1368,7 @@ type AlertsRequest struct { func (x *AlertsRequest) Reset() { *x = AlertsRequest{} - mi := &file_api_api_proto_msgTypes[17] + mi := &file_api_api_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1093,7 +1380,7 @@ func (x *AlertsRequest) String() string { func (*AlertsRequest) ProtoMessage() {} func (x *AlertsRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[17] + mi := &file_api_api_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1106,7 +1393,7 @@ func (x *AlertsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AlertsRequest.ProtoReflect.Descriptor instead. func (*AlertsRequest) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{17} + return file_api_api_proto_rawDescGZIP(), []int{21} } func (x *AlertsRequest) GetHost() string { @@ -1157,7 +1444,7 @@ type AlertRecord struct { func (x *AlertRecord) Reset() { *x = AlertRecord{} - mi := &file_api_api_proto_msgTypes[18] + mi := &file_api_api_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1169,7 +1456,7 @@ func (x *AlertRecord) String() string { func (*AlertRecord) ProtoMessage() {} func (x *AlertRecord) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[18] + mi := &file_api_api_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1182,7 +1469,7 @@ func (x *AlertRecord) ProtoReflect() protoreflect.Message { // Deprecated: Use AlertRecord.ProtoReflect.Descriptor instead. func (*AlertRecord) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{18} + return file_api_api_proto_rawDescGZIP(), []int{22} } func (x *AlertRecord) GetId() int64 { @@ -1264,7 +1551,7 @@ type AlertList struct { func (x *AlertList) Reset() { *x = AlertList{} - mi := &file_api_api_proto_msgTypes[19] + mi := &file_api_api_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1276,7 +1563,7 @@ func (x *AlertList) String() string { func (*AlertList) ProtoMessage() {} func (x *AlertList) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[19] + mi := &file_api_api_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1289,7 +1576,7 @@ func (x *AlertList) ProtoReflect() protoreflect.Message { // Deprecated: Use AlertList.ProtoReflect.Descriptor instead. func (*AlertList) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{19} + return file_api_api_proto_rawDescGZIP(), []int{23} } func (x *AlertList) GetAlerts() []*AlertRecord { @@ -1320,7 +1607,7 @@ type DiskForecast struct { func (x *DiskForecast) Reset() { *x = DiskForecast{} - mi := &file_api_api_proto_msgTypes[20] + mi := &file_api_api_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1332,7 +1619,7 @@ func (x *DiskForecast) String() string { func (*DiskForecast) ProtoMessage() {} func (x *DiskForecast) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[20] + mi := &file_api_api_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1345,7 +1632,7 @@ func (x *DiskForecast) ProtoReflect() protoreflect.Message { // Deprecated: Use DiskForecast.ProtoReflect.Descriptor instead. func (*DiskForecast) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{20} + return file_api_api_proto_rawDescGZIP(), []int{24} } func (x *DiskForecast) GetDevice() string { @@ -1413,7 +1700,7 @@ type DiskForecastList struct { func (x *DiskForecastList) Reset() { *x = DiskForecastList{} - mi := &file_api_api_proto_msgTypes[21] + mi := &file_api_api_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1425,7 +1712,7 @@ func (x *DiskForecastList) String() string { func (*DiskForecastList) ProtoMessage() {} func (x *DiskForecastList) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[21] + mi := &file_api_api_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1438,7 +1725,7 @@ func (x *DiskForecastList) ProtoReflect() protoreflect.Message { // Deprecated: Use DiskForecastList.ProtoReflect.Descriptor instead. func (*DiskForecastList) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{21} + return file_api_api_proto_rawDescGZIP(), []int{25} } func (x *DiskForecastList) GetDisks() []*DiskForecast { @@ -1459,7 +1746,7 @@ type ProcessUsageRequest struct { func (x *ProcessUsageRequest) Reset() { *x = ProcessUsageRequest{} - mi := &file_api_api_proto_msgTypes[22] + mi := &file_api_api_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1471,7 +1758,7 @@ func (x *ProcessUsageRequest) String() string { func (*ProcessUsageRequest) ProtoMessage() {} func (x *ProcessUsageRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[22] + mi := &file_api_api_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1484,7 +1771,7 @@ func (x *ProcessUsageRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ProcessUsageRequest.ProtoReflect.Descriptor instead. func (*ProcessUsageRequest) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{22} + return file_api_api_proto_rawDescGZIP(), []int{26} } func (x *ProcessUsageRequest) GetHost() string { @@ -1525,7 +1812,7 @@ type ProcessUsage struct { func (x *ProcessUsage) Reset() { *x = ProcessUsage{} - mi := &file_api_api_proto_msgTypes[23] + mi := &file_api_api_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1537,7 +1824,7 @@ func (x *ProcessUsage) String() string { func (*ProcessUsage) ProtoMessage() {} func (x *ProcessUsage) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[23] + mi := &file_api_api_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1550,7 +1837,7 @@ func (x *ProcessUsage) ProtoReflect() protoreflect.Message { // Deprecated: Use ProcessUsage.ProtoReflect.Descriptor instead. func (*ProcessUsage) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{23} + return file_api_api_proto_rawDescGZIP(), []int{27} } func (x *ProcessUsage) GetName() string { @@ -1607,7 +1894,7 @@ type ProcessUsageList struct { func (x *ProcessUsageList) Reset() { *x = ProcessUsageList{} - mi := &file_api_api_proto_msgTypes[24] + mi := &file_api_api_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1619,7 +1906,7 @@ func (x *ProcessUsageList) String() string { func (*ProcessUsageList) ProtoMessage() {} func (x *ProcessUsageList) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[24] + mi := &file_api_api_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1632,7 +1919,7 @@ func (x *ProcessUsageList) ProtoReflect() protoreflect.Message { // Deprecated: Use ProcessUsageList.ProtoReflect.Descriptor instead. func (*ProcessUsageList) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{24} + return file_api_api_proto_rawDescGZIP(), []int{28} } func (x *ProcessUsageList) GetSnapshots() int32 { @@ -1670,7 +1957,7 @@ type CustomValue struct { func (x *CustomValue) Reset() { *x = CustomValue{} - mi := &file_api_api_proto_msgTypes[25] + mi := &file_api_api_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1682,7 +1969,7 @@ func (x *CustomValue) String() string { func (*CustomValue) ProtoMessage() {} func (x *CustomValue) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[25] + mi := &file_api_api_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1695,7 +1982,7 @@ func (x *CustomValue) ProtoReflect() protoreflect.Message { // Deprecated: Use CustomValue.ProtoReflect.Descriptor instead. func (*CustomValue) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{25} + return file_api_api_proto_rawDescGZIP(), []int{29} } func (x *CustomValue) GetHost() string { @@ -1744,7 +2031,7 @@ type SnapshotList struct { func (x *SnapshotList) Reset() { *x = SnapshotList{} - mi := &file_api_api_proto_msgTypes[26] + mi := &file_api_api_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1756,7 +2043,7 @@ func (x *SnapshotList) String() string { func (*SnapshotList) ProtoMessage() {} func (x *SnapshotList) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[26] + mi := &file_api_api_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1769,7 +2056,7 @@ func (x *SnapshotList) ProtoReflect() protoreflect.Message { // Deprecated: Use SnapshotList.ProtoReflect.Descriptor instead. func (*SnapshotList) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{26} + return file_api_api_proto_rawDescGZIP(), []int{30} } func (x *SnapshotList) GetHosts() []*HostSnapshot { @@ -1796,7 +2083,7 @@ type EndpointsRequest struct { func (x *EndpointsRequest) Reset() { *x = EndpointsRequest{} - mi := &file_api_api_proto_msgTypes[27] + mi := &file_api_api_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1808,7 +2095,7 @@ func (x *EndpointsRequest) String() string { func (*EndpointsRequest) ProtoMessage() {} func (x *EndpointsRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[27] + mi := &file_api_api_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1821,7 +2108,7 @@ func (x *EndpointsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EndpointsRequest.ProtoReflect.Descriptor instead. func (*EndpointsRequest) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{27} + return file_api_api_proto_rawDescGZIP(), []int{31} } func (x *EndpointsRequest) GetFrom() int64 { @@ -1863,7 +2150,7 @@ type EndpointStatus struct { func (x *EndpointStatus) Reset() { *x = EndpointStatus{} - mi := &file_api_api_proto_msgTypes[28] + mi := &file_api_api_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1875,7 +2162,7 @@ func (x *EndpointStatus) String() string { func (*EndpointStatus) ProtoMessage() {} func (x *EndpointStatus) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[28] + mi := &file_api_api_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1888,7 +2175,7 @@ func (x *EndpointStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use EndpointStatus.ProtoReflect.Descriptor instead. func (*EndpointStatus) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{28} + return file_api_api_proto_rawDescGZIP(), []int{32} } func (x *EndpointStatus) GetName() string { @@ -1984,7 +2271,7 @@ type EndpointList struct { func (x *EndpointList) Reset() { *x = EndpointList{} - mi := &file_api_api_proto_msgTypes[29] + mi := &file_api_api_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1996,7 +2283,7 @@ func (x *EndpointList) String() string { func (*EndpointList) ProtoMessage() {} func (x *EndpointList) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[29] + mi := &file_api_api_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2009,7 +2296,7 @@ func (x *EndpointList) ProtoReflect() protoreflect.Message { // Deprecated: Use EndpointList.ProtoReflect.Descriptor instead. func (*EndpointList) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{29} + return file_api_api_proto_rawDescGZIP(), []int{33} } func (x *EndpointList) GetEndpoints() []*EndpointStatus { @@ -2033,7 +2320,7 @@ type EndpointSeriesRequest struct { func (x *EndpointSeriesRequest) Reset() { *x = EndpointSeriesRequest{} - mi := &file_api_api_proto_msgTypes[30] + mi := &file_api_api_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2045,7 +2332,7 @@ func (x *EndpointSeriesRequest) String() string { func (*EndpointSeriesRequest) ProtoMessage() {} func (x *EndpointSeriesRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[30] + mi := &file_api_api_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2058,7 +2345,7 @@ func (x *EndpointSeriesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EndpointSeriesRequest.ProtoReflect.Descriptor instead. func (*EndpointSeriesRequest) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{30} + return file_api_api_proto_rawDescGZIP(), []int{34} } func (x *EndpointSeriesRequest) GetName() string { @@ -2106,7 +2393,7 @@ type Credentials struct { func (x *Credentials) Reset() { *x = Credentials{} - mi := &file_api_api_proto_msgTypes[31] + mi := &file_api_api_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2118,7 +2405,7 @@ func (x *Credentials) String() string { func (*Credentials) ProtoMessage() {} func (x *Credentials) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[31] + mi := &file_api_api_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2131,7 +2418,7 @@ func (x *Credentials) ProtoReflect() protoreflect.Message { // Deprecated: Use Credentials.ProtoReflect.Descriptor instead. func (*Credentials) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{31} + return file_api_api_proto_rawDescGZIP(), []int{35} } func (x *Credentials) GetUser() string { @@ -2161,7 +2448,7 @@ type SessionInfo struct { func (x *SessionInfo) Reset() { *x = SessionInfo{} - mi := &file_api_api_proto_msgTypes[32] + mi := &file_api_api_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2173,7 +2460,7 @@ func (x *SessionInfo) String() string { func (*SessionInfo) ProtoMessage() {} func (x *SessionInfo) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[32] + mi := &file_api_api_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2186,7 +2473,7 @@ func (x *SessionInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionInfo.ProtoReflect.Descriptor instead. func (*SessionInfo) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{32} + return file_api_api_proto_rawDescGZIP(), []int{36} } func (x *SessionInfo) GetToken() string { @@ -2228,7 +2515,7 @@ type ChangePasswordRequest struct { func (x *ChangePasswordRequest) Reset() { *x = ChangePasswordRequest{} - mi := &file_api_api_proto_msgTypes[33] + mi := &file_api_api_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2240,7 +2527,7 @@ func (x *ChangePasswordRequest) String() string { func (*ChangePasswordRequest) ProtoMessage() {} func (x *ChangePasswordRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[33] + mi := &file_api_api_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2253,7 +2540,7 @@ func (x *ChangePasswordRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangePasswordRequest.ProtoReflect.Descriptor instead. func (*ChangePasswordRequest) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{33} + return file_api_api_proto_rawDescGZIP(), []int{37} } func (x *ChangePasswordRequest) GetToken() string { @@ -2286,7 +2573,7 @@ type SessionRequest struct { func (x *SessionRequest) Reset() { *x = SessionRequest{} - mi := &file_api_api_proto_msgTypes[34] + mi := &file_api_api_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2298,7 +2585,7 @@ func (x *SessionRequest) String() string { func (*SessionRequest) ProtoMessage() {} func (x *SessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[34] + mi := &file_api_api_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2311,7 +2598,7 @@ func (x *SessionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionRequest.ProtoReflect.Descriptor instead. func (*SessionRequest) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{34} + return file_api_api_proto_rawDescGZIP(), []int{38} } func (x *SessionRequest) GetToken() string { @@ -2330,7 +2617,7 @@ type UserStatus struct { func (x *UserStatus) Reset() { *x = UserStatus{} - mi := &file_api_api_proto_msgTypes[35] + mi := &file_api_api_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2342,7 +2629,7 @@ func (x *UserStatus) String() string { func (*UserStatus) ProtoMessage() {} func (x *UserStatus) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[35] + mi := &file_api_api_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2355,7 +2642,7 @@ func (x *UserStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use UserStatus.ProtoReflect.Descriptor instead. func (*UserStatus) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{35} + return file_api_api_proto_rawDescGZIP(), []int{39} } func (x *UserStatus) GetHasUsers() bool { @@ -2380,7 +2667,7 @@ type AlertRuleInfo struct { func (x *AlertRuleInfo) Reset() { *x = AlertRuleInfo{} - mi := &file_api_api_proto_msgTypes[36] + mi := &file_api_api_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2392,7 +2679,7 @@ func (x *AlertRuleInfo) String() string { func (*AlertRuleInfo) ProtoMessage() {} func (x *AlertRuleInfo) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[36] + mi := &file_api_api_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2405,7 +2692,7 @@ func (x *AlertRuleInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use AlertRuleInfo.ProtoReflect.Descriptor instead. func (*AlertRuleInfo) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{36} + return file_api_api_proto_rawDescGZIP(), []int{40} } func (x *AlertRuleInfo) GetId() int64 { @@ -2452,7 +2739,7 @@ type AlertRuleList struct { func (x *AlertRuleList) Reset() { *x = AlertRuleList{} - mi := &file_api_api_proto_msgTypes[37] + mi := &file_api_api_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2464,7 +2751,7 @@ func (x *AlertRuleList) String() string { func (*AlertRuleList) ProtoMessage() {} func (x *AlertRuleList) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[37] + mi := &file_api_api_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2477,7 +2764,7 @@ func (x *AlertRuleList) ProtoReflect() protoreflect.Message { // Deprecated: Use AlertRuleList.ProtoReflect.Descriptor instead. func (*AlertRuleList) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{37} + return file_api_api_proto_rawDescGZIP(), []int{41} } func (x *AlertRuleList) GetRules() []*AlertRuleInfo { @@ -2500,7 +2787,7 @@ type SaveRuleRequest struct { func (x *SaveRuleRequest) Reset() { *x = SaveRuleRequest{} - mi := &file_api_api_proto_msgTypes[38] + mi := &file_api_api_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2512,7 +2799,7 @@ func (x *SaveRuleRequest) String() string { func (*SaveRuleRequest) ProtoMessage() {} func (x *SaveRuleRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[38] + mi := &file_api_api_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2525,7 +2812,7 @@ func (x *SaveRuleRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SaveRuleRequest.ProtoReflect.Descriptor instead. func (*SaveRuleRequest) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{38} + return file_api_api_proto_rawDescGZIP(), []int{42} } func (x *SaveRuleRequest) GetId() int64 { @@ -2566,7 +2853,7 @@ type RuleRequest struct { func (x *RuleRequest) Reset() { *x = RuleRequest{} - mi := &file_api_api_proto_msgTypes[39] + mi := &file_api_api_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2578,7 +2865,7 @@ func (x *RuleRequest) String() string { func (*RuleRequest) ProtoMessage() {} func (x *RuleRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_api_proto_msgTypes[39] + mi := &file_api_api_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2591,7 +2878,7 @@ func (x *RuleRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RuleRequest.ProtoReflect.Descriptor instead. func (*RuleRequest) Descriptor() ([]byte, []int) { - return file_api_api_proto_rawDescGZIP(), []int{39} + return file_api_api_proto_rawDescGZIP(), []int{43} } func (x *RuleRequest) GetId() int64 { @@ -2615,13 +2902,30 @@ const file_api_api_proto_rawDesc = "" + "\rapi/api.proto\x12\x03api\"\x06\n" + "\x04Void\"\x1d\n" + "\aMessage\x12\x12\n" + - "\x04body\x18\x01 \x01(\tR\x04body\"H\n" + + "\x04body\x18\x01 \x01(\tR\x04body\"\xa2\x01\n" + "\n" + "ServerInfo\x12\x1e\n" + "\n" + "serverName\x18\x01 \x01(\tR\n" + "serverName\x12\x1a\n" + - "\btimezone\x18\x02 \x01(\tR\btimezone\"/\n" + + "\btimezone\x18\x02 \x01(\tR\btimezone\x12\"\n" + + "\fagentVersion\x18\x03 \x01(\tR\fagentVersion\x12\x12\n" + + "\x04arch\x18\x04 \x01(\tR\x04arch\x12 \n" + + "\vupdateError\x18\x05 \x01(\tR\vupdateError\"k\n" + + "\vAgentUpdate\x12\x18\n" + + "\aversion\x18\x01 \x01(\tR\aversion\x12 \n" + + "\vdownloadUrl\x18\x02 \x01(\tR\vdownloadUrl\x12 \n" + + "\vrequestedAt\x18\x03 \x01(\x03R\vrequestedAt\"L\n" + + "\fPingResponse\x12\x12\n" + + "\x04body\x18\x01 \x01(\tR\x04body\x12(\n" + + "\x06update\x18\x02 \x01(\v2\x10.api.AgentUpdateR\x06update\"v\n" + + "\x12AgentUpdateRequest\x12\x14\n" + + "\x05hosts\x18\x01 \x03(\tR\x05hosts\x12\x18\n" + + "\aversion\x18\x02 \x01(\tR\aversion\x12 \n" + + "\vdownloadUrl\x18\x03 \x01(\tR\vdownloadUrl\x12\x0e\n" + + "\x02by\x18\x04 \x01(\tR\x02by\"1\n" + + "\x11AgentUpdateResult\x12\x1c\n" + + "\trequested\x18\x01 \x01(\x05R\trequested\"/\n" + "\vMonitorData\x12 \n" + "\vmonitorData\x18\x01 \x01(\tR\vmonitorData\"]\n" + "\rEnrollRequest\x12\x14\n" + @@ -2630,7 +2934,7 @@ const file_api_api_proto_rawDesc = "" + "\btimezone\x18\x03 \x01(\tR\btimezone\"H\n" + "\x0eEnrollResponse\x12\x1a\n" + "\bhostName\x18\x01 \x01(\tR\bhostName\x12\x1a\n" + - "\bagentKey\x18\x02 \x01(\tR\bagentKey\"\x87\x04\n" + + "\bagentKey\x18\x02 \x01(\tR\bagentKey\"\x9b\x05\n" + "\vHostSummary\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x0e\n" + "\x02up\x18\x02 \x01(\bR\x02up\x12\x1a\n" + @@ -2653,7 +2957,11 @@ const file_api_api_proto_rawDesc = "" + "containers\x18\x0f \x01(\x05R\n" + "containers\x12'\n" + "\fdiskFullDays\x18\x10 \x01(\x01H\x00R\fdiskFullDays\x88\x01\x01\x12\"\n" + - "\fagentVersion\x18\x11 \x01(\tR\fagentVersionB\x0f\n" + + "\fagentVersion\x18\x11 \x01(\tR\fagentVersion\x12\x1c\n" + + "\tcanUpdate\x18\x12 \x01(\bR\tcanUpdate\x12$\n" + + "\rupdateVersion\x18\x13 \x01(\tR\rupdateVersion\x12,\n" + + "\x11updateRequestedAt\x18\x14 \x01(\x03R\x11updateRequestedAt\x12 \n" + + "\vupdateError\x18\x15 \x01(\tR\vupdateErrorB\x0f\n" + "\r_diskFullDays\"6\n" + "\fFleetSummary\x12&\n" + "\x05hosts\x18\x01 \x03(\v2\x10.api.HostSummaryR\x05hosts\"!\n" + @@ -2813,12 +3121,11 @@ const file_api_api_proto_rawDesc = "" + "\x02by\x18\x04 \x01(\tR\x02by\"-\n" + "\vRuleRequest\x12\x0e\n" + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x0e\n" + - "\x02by\x18\x02 \x01(\tR\x02by2\xef\n" + - "\n" + + "\x02by\x18\x02 \x01(\tR\x02by2\xbd\v\n" + "\x12MonitorDataService\x123\n" + - "\x06Enroll\x12\x12.api.EnrollRequest\x1a\x13.api.EnrollResponse\"\x00\x12-\n" + + "\x06Enroll\x12\x12.api.EnrollRequest\x1a\x13.api.EnrollResponse\"\x00\x122\n" + "\n" + - "HandlePing\x12\x0f.api.ServerInfo\x1a\f.api.Message\"\x00\x12,\n" + + "HandlePing\x12\x0f.api.ServerInfo\x1a\x11.api.PingResponse\"\x00\x12,\n" + "\tInitAgent\x12\x0f.api.ServerInfo\x1a\f.api.Message\"\x00\x125\n" + "\x11HandleMonitorData\x12\x10.api.MonitorData\x1a\f.api.Message\"\x00\x12;\n" + "\x17HandleCustomMonitorData\x12\x10.api.MonitorData\x1a\f.api.Message\"\x00\x12'\n" + @@ -2844,7 +3151,8 @@ const file_api_api_proto_rawDesc = "" + "AlertRules\x12\t.api.Void\x1a\x12.api.AlertRuleList\"\x00\x126\n" + "\bSaveRule\x12\x14.api.SaveRuleRequest\x1a\x12.api.AlertRuleInfo\"\x00\x12.\n" + "\n" + - "DeleteRule\x12\x10.api.RuleRequest\x1a\f.api.Message\"\x00B)Z'github.com/dhamith93/SyMon/internal/apib\x06proto3" + "DeleteRule\x12\x10.api.RuleRequest\x1a\f.api.Message\"\x00\x12G\n" + + "\x12RequestAgentUpdate\x12\x17.api.AgentUpdateRequest\x1a\x16.api.AgentUpdateResult\"\x00B)Z'github.com/dhamith93/SyMon/internal/apib\x06proto3" var ( file_api_api_proto_rawDescOnce sync.Once @@ -2858,117 +3166,124 @@ func file_api_api_proto_rawDescGZIP() []byte { return file_api_api_proto_rawDescData } -var file_api_api_proto_msgTypes = make([]protoimpl.MessageInfo, 40) +var file_api_api_proto_msgTypes = make([]protoimpl.MessageInfo, 44) var file_api_api_proto_goTypes = []any{ (*Void)(nil), // 0: api.Void (*Message)(nil), // 1: api.Message (*ServerInfo)(nil), // 2: api.ServerInfo - (*MonitorData)(nil), // 3: api.MonitorData - (*EnrollRequest)(nil), // 4: api.EnrollRequest - (*EnrollResponse)(nil), // 5: api.EnrollResponse - (*HostSummary)(nil), // 6: api.HostSummary - (*FleetSummary)(nil), // 7: api.FleetSummary - (*HostRequest)(nil), // 8: api.HostRequest - (*HostSnapshot)(nil), // 9: api.HostSnapshot - (*SeriesRequest)(nil), // 10: api.SeriesRequest - (*Point)(nil), // 11: api.Point - (*Series)(nil), // 12: api.Series - (*SeriesResponse)(nil), // 13: api.SeriesResponse - (*ProcessesRequest)(nil), // 14: api.ProcessesRequest - (*ProcessesResponse)(nil), // 15: api.ProcessesResponse - (*NameList)(nil), // 16: api.NameList - (*AlertsRequest)(nil), // 17: api.AlertsRequest - (*AlertRecord)(nil), // 18: api.AlertRecord - (*AlertList)(nil), // 19: api.AlertList - (*DiskForecast)(nil), // 20: api.DiskForecast - (*DiskForecastList)(nil), // 21: api.DiskForecastList - (*ProcessUsageRequest)(nil), // 22: api.ProcessUsageRequest - (*ProcessUsage)(nil), // 23: api.ProcessUsage - (*ProcessUsageList)(nil), // 24: api.ProcessUsageList - (*CustomValue)(nil), // 25: api.CustomValue - (*SnapshotList)(nil), // 26: api.SnapshotList - (*EndpointsRequest)(nil), // 27: api.EndpointsRequest - (*EndpointStatus)(nil), // 28: api.EndpointStatus - (*EndpointList)(nil), // 29: api.EndpointList - (*EndpointSeriesRequest)(nil), // 30: api.EndpointSeriesRequest - (*Credentials)(nil), // 31: api.Credentials - (*SessionInfo)(nil), // 32: api.SessionInfo - (*ChangePasswordRequest)(nil), // 33: api.ChangePasswordRequest - (*SessionRequest)(nil), // 34: api.SessionRequest - (*UserStatus)(nil), // 35: api.UserStatus - (*AlertRuleInfo)(nil), // 36: api.AlertRuleInfo - (*AlertRuleList)(nil), // 37: api.AlertRuleList - (*SaveRuleRequest)(nil), // 38: api.SaveRuleRequest - (*RuleRequest)(nil), // 39: api.RuleRequest + (*AgentUpdate)(nil), // 3: api.AgentUpdate + (*PingResponse)(nil), // 4: api.PingResponse + (*AgentUpdateRequest)(nil), // 5: api.AgentUpdateRequest + (*AgentUpdateResult)(nil), // 6: api.AgentUpdateResult + (*MonitorData)(nil), // 7: api.MonitorData + (*EnrollRequest)(nil), // 8: api.EnrollRequest + (*EnrollResponse)(nil), // 9: api.EnrollResponse + (*HostSummary)(nil), // 10: api.HostSummary + (*FleetSummary)(nil), // 11: api.FleetSummary + (*HostRequest)(nil), // 12: api.HostRequest + (*HostSnapshot)(nil), // 13: api.HostSnapshot + (*SeriesRequest)(nil), // 14: api.SeriesRequest + (*Point)(nil), // 15: api.Point + (*Series)(nil), // 16: api.Series + (*SeriesResponse)(nil), // 17: api.SeriesResponse + (*ProcessesRequest)(nil), // 18: api.ProcessesRequest + (*ProcessesResponse)(nil), // 19: api.ProcessesResponse + (*NameList)(nil), // 20: api.NameList + (*AlertsRequest)(nil), // 21: api.AlertsRequest + (*AlertRecord)(nil), // 22: api.AlertRecord + (*AlertList)(nil), // 23: api.AlertList + (*DiskForecast)(nil), // 24: api.DiskForecast + (*DiskForecastList)(nil), // 25: api.DiskForecastList + (*ProcessUsageRequest)(nil), // 26: api.ProcessUsageRequest + (*ProcessUsage)(nil), // 27: api.ProcessUsage + (*ProcessUsageList)(nil), // 28: api.ProcessUsageList + (*CustomValue)(nil), // 29: api.CustomValue + (*SnapshotList)(nil), // 30: api.SnapshotList + (*EndpointsRequest)(nil), // 31: api.EndpointsRequest + (*EndpointStatus)(nil), // 32: api.EndpointStatus + (*EndpointList)(nil), // 33: api.EndpointList + (*EndpointSeriesRequest)(nil), // 34: api.EndpointSeriesRequest + (*Credentials)(nil), // 35: api.Credentials + (*SessionInfo)(nil), // 36: api.SessionInfo + (*ChangePasswordRequest)(nil), // 37: api.ChangePasswordRequest + (*SessionRequest)(nil), // 38: api.SessionRequest + (*UserStatus)(nil), // 39: api.UserStatus + (*AlertRuleInfo)(nil), // 40: api.AlertRuleInfo + (*AlertRuleList)(nil), // 41: api.AlertRuleList + (*SaveRuleRequest)(nil), // 42: api.SaveRuleRequest + (*RuleRequest)(nil), // 43: api.RuleRequest } var file_api_api_proto_depIdxs = []int32{ - 6, // 0: api.FleetSummary.hosts:type_name -> api.HostSummary - 11, // 1: api.Series.points:type_name -> api.Point - 12, // 2: api.SeriesResponse.series:type_name -> api.Series - 18, // 3: api.AlertList.alerts:type_name -> api.AlertRecord - 20, // 4: api.DiskForecastList.disks:type_name -> api.DiskForecast - 23, // 5: api.ProcessUsageList.processes:type_name -> api.ProcessUsage - 9, // 6: api.SnapshotList.hosts:type_name -> api.HostSnapshot - 25, // 7: api.SnapshotList.customMetrics:type_name -> api.CustomValue - 28, // 8: api.EndpointList.endpoints:type_name -> api.EndpointStatus - 36, // 9: api.AlertRuleList.rules:type_name -> api.AlertRuleInfo - 4, // 10: api.MonitorDataService.Enroll:input_type -> api.EnrollRequest - 2, // 11: api.MonitorDataService.HandlePing:input_type -> api.ServerInfo - 2, // 12: api.MonitorDataService.InitAgent:input_type -> api.ServerInfo - 3, // 13: api.MonitorDataService.HandleMonitorData:input_type -> api.MonitorData - 3, // 14: api.MonitorDataService.HandleCustomMonitorData:input_type -> api.MonitorData - 0, // 15: api.MonitorDataService.Fleet:input_type -> api.Void - 8, // 16: api.MonitorDataService.Snapshot:input_type -> api.HostRequest - 10, // 17: api.MonitorDataService.QuerySeries:input_type -> api.SeriesRequest - 14, // 18: api.MonitorDataService.Processes:input_type -> api.ProcessesRequest - 8, // 19: api.MonitorDataService.CustomMetricNames:input_type -> api.HostRequest - 17, // 20: api.MonitorDataService.Alerts:input_type -> api.AlertsRequest - 8, // 21: api.MonitorDataService.DiskForecasts:input_type -> api.HostRequest - 22, // 22: api.MonitorDataService.ProcessUsage:input_type -> api.ProcessUsageRequest - 0, // 23: api.MonitorDataService.Snapshots:input_type -> api.Void - 27, // 24: api.MonitorDataService.Endpoints:input_type -> api.EndpointsRequest - 30, // 25: api.MonitorDataService.EndpointSeries:input_type -> api.EndpointSeriesRequest - 0, // 26: api.MonitorDataService.Version:input_type -> api.Void - 31, // 27: api.MonitorDataService.Login:input_type -> api.Credentials - 34, // 28: api.MonitorDataService.CheckSession:input_type -> api.SessionRequest - 34, // 29: api.MonitorDataService.Logout:input_type -> api.SessionRequest - 33, // 30: api.MonitorDataService.ChangePassword:input_type -> api.ChangePasswordRequest - 31, // 31: api.MonitorDataService.CheckPassword:input_type -> api.Credentials - 0, // 32: api.MonitorDataService.HasUsers:input_type -> api.Void - 0, // 33: api.MonitorDataService.AlertRules:input_type -> api.Void - 38, // 34: api.MonitorDataService.SaveRule:input_type -> api.SaveRuleRequest - 39, // 35: api.MonitorDataService.DeleteRule:input_type -> api.RuleRequest - 5, // 36: api.MonitorDataService.Enroll:output_type -> api.EnrollResponse - 1, // 37: api.MonitorDataService.HandlePing:output_type -> api.Message - 1, // 38: api.MonitorDataService.InitAgent:output_type -> api.Message - 1, // 39: api.MonitorDataService.HandleMonitorData:output_type -> api.Message - 1, // 40: api.MonitorDataService.HandleCustomMonitorData:output_type -> api.Message - 7, // 41: api.MonitorDataService.Fleet:output_type -> api.FleetSummary - 9, // 42: api.MonitorDataService.Snapshot:output_type -> api.HostSnapshot - 13, // 43: api.MonitorDataService.QuerySeries:output_type -> api.SeriesResponse - 15, // 44: api.MonitorDataService.Processes:output_type -> api.ProcessesResponse - 16, // 45: api.MonitorDataService.CustomMetricNames:output_type -> api.NameList - 19, // 46: api.MonitorDataService.Alerts:output_type -> api.AlertList - 21, // 47: api.MonitorDataService.DiskForecasts:output_type -> api.DiskForecastList - 24, // 48: api.MonitorDataService.ProcessUsage:output_type -> api.ProcessUsageList - 26, // 49: api.MonitorDataService.Snapshots:output_type -> api.SnapshotList - 29, // 50: api.MonitorDataService.Endpoints:output_type -> api.EndpointList - 13, // 51: api.MonitorDataService.EndpointSeries:output_type -> api.SeriesResponse - 1, // 52: api.MonitorDataService.Version:output_type -> api.Message - 32, // 53: api.MonitorDataService.Login:output_type -> api.SessionInfo - 32, // 54: api.MonitorDataService.CheckSession:output_type -> api.SessionInfo - 1, // 55: api.MonitorDataService.Logout:output_type -> api.Message - 1, // 56: api.MonitorDataService.ChangePassword:output_type -> api.Message - 1, // 57: api.MonitorDataService.CheckPassword:output_type -> api.Message - 35, // 58: api.MonitorDataService.HasUsers:output_type -> api.UserStatus - 37, // 59: api.MonitorDataService.AlertRules:output_type -> api.AlertRuleList - 36, // 60: api.MonitorDataService.SaveRule:output_type -> api.AlertRuleInfo - 1, // 61: api.MonitorDataService.DeleteRule:output_type -> api.Message - 36, // [36:62] is the sub-list for method output_type - 10, // [10:36] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 3, // 0: api.PingResponse.update:type_name -> api.AgentUpdate + 10, // 1: api.FleetSummary.hosts:type_name -> api.HostSummary + 15, // 2: api.Series.points:type_name -> api.Point + 16, // 3: api.SeriesResponse.series:type_name -> api.Series + 22, // 4: api.AlertList.alerts:type_name -> api.AlertRecord + 24, // 5: api.DiskForecastList.disks:type_name -> api.DiskForecast + 27, // 6: api.ProcessUsageList.processes:type_name -> api.ProcessUsage + 13, // 7: api.SnapshotList.hosts:type_name -> api.HostSnapshot + 29, // 8: api.SnapshotList.customMetrics:type_name -> api.CustomValue + 32, // 9: api.EndpointList.endpoints:type_name -> api.EndpointStatus + 40, // 10: api.AlertRuleList.rules:type_name -> api.AlertRuleInfo + 8, // 11: api.MonitorDataService.Enroll:input_type -> api.EnrollRequest + 2, // 12: api.MonitorDataService.HandlePing:input_type -> api.ServerInfo + 2, // 13: api.MonitorDataService.InitAgent:input_type -> api.ServerInfo + 7, // 14: api.MonitorDataService.HandleMonitorData:input_type -> api.MonitorData + 7, // 15: api.MonitorDataService.HandleCustomMonitorData:input_type -> api.MonitorData + 0, // 16: api.MonitorDataService.Fleet:input_type -> api.Void + 12, // 17: api.MonitorDataService.Snapshot:input_type -> api.HostRequest + 14, // 18: api.MonitorDataService.QuerySeries:input_type -> api.SeriesRequest + 18, // 19: api.MonitorDataService.Processes:input_type -> api.ProcessesRequest + 12, // 20: api.MonitorDataService.CustomMetricNames:input_type -> api.HostRequest + 21, // 21: api.MonitorDataService.Alerts:input_type -> api.AlertsRequest + 12, // 22: api.MonitorDataService.DiskForecasts:input_type -> api.HostRequest + 26, // 23: api.MonitorDataService.ProcessUsage:input_type -> api.ProcessUsageRequest + 0, // 24: api.MonitorDataService.Snapshots:input_type -> api.Void + 31, // 25: api.MonitorDataService.Endpoints:input_type -> api.EndpointsRequest + 34, // 26: api.MonitorDataService.EndpointSeries:input_type -> api.EndpointSeriesRequest + 0, // 27: api.MonitorDataService.Version:input_type -> api.Void + 35, // 28: api.MonitorDataService.Login:input_type -> api.Credentials + 38, // 29: api.MonitorDataService.CheckSession:input_type -> api.SessionRequest + 38, // 30: api.MonitorDataService.Logout:input_type -> api.SessionRequest + 37, // 31: api.MonitorDataService.ChangePassword:input_type -> api.ChangePasswordRequest + 35, // 32: api.MonitorDataService.CheckPassword:input_type -> api.Credentials + 0, // 33: api.MonitorDataService.HasUsers:input_type -> api.Void + 0, // 34: api.MonitorDataService.AlertRules:input_type -> api.Void + 42, // 35: api.MonitorDataService.SaveRule:input_type -> api.SaveRuleRequest + 43, // 36: api.MonitorDataService.DeleteRule:input_type -> api.RuleRequest + 5, // 37: api.MonitorDataService.RequestAgentUpdate:input_type -> api.AgentUpdateRequest + 9, // 38: api.MonitorDataService.Enroll:output_type -> api.EnrollResponse + 4, // 39: api.MonitorDataService.HandlePing:output_type -> api.PingResponse + 1, // 40: api.MonitorDataService.InitAgent:output_type -> api.Message + 1, // 41: api.MonitorDataService.HandleMonitorData:output_type -> api.Message + 1, // 42: api.MonitorDataService.HandleCustomMonitorData:output_type -> api.Message + 11, // 43: api.MonitorDataService.Fleet:output_type -> api.FleetSummary + 13, // 44: api.MonitorDataService.Snapshot:output_type -> api.HostSnapshot + 17, // 45: api.MonitorDataService.QuerySeries:output_type -> api.SeriesResponse + 19, // 46: api.MonitorDataService.Processes:output_type -> api.ProcessesResponse + 20, // 47: api.MonitorDataService.CustomMetricNames:output_type -> api.NameList + 23, // 48: api.MonitorDataService.Alerts:output_type -> api.AlertList + 25, // 49: api.MonitorDataService.DiskForecasts:output_type -> api.DiskForecastList + 28, // 50: api.MonitorDataService.ProcessUsage:output_type -> api.ProcessUsageList + 30, // 51: api.MonitorDataService.Snapshots:output_type -> api.SnapshotList + 33, // 52: api.MonitorDataService.Endpoints:output_type -> api.EndpointList + 17, // 53: api.MonitorDataService.EndpointSeries:output_type -> api.SeriesResponse + 1, // 54: api.MonitorDataService.Version:output_type -> api.Message + 36, // 55: api.MonitorDataService.Login:output_type -> api.SessionInfo + 36, // 56: api.MonitorDataService.CheckSession:output_type -> api.SessionInfo + 1, // 57: api.MonitorDataService.Logout:output_type -> api.Message + 1, // 58: api.MonitorDataService.ChangePassword:output_type -> api.Message + 1, // 59: api.MonitorDataService.CheckPassword:output_type -> api.Message + 39, // 60: api.MonitorDataService.HasUsers:output_type -> api.UserStatus + 41, // 61: api.MonitorDataService.AlertRules:output_type -> api.AlertRuleList + 40, // 62: api.MonitorDataService.SaveRule:output_type -> api.AlertRuleInfo + 1, // 63: api.MonitorDataService.DeleteRule:output_type -> api.Message + 6, // 64: api.MonitorDataService.RequestAgentUpdate:output_type -> api.AgentUpdateResult + 38, // [38:65] is the sub-list for method output_type + 11, // [11:38] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_api_api_proto_init() } @@ -2976,15 +3291,15 @@ func file_api_api_proto_init() { if File_api_api_proto != nil { return } - file_api_api_proto_msgTypes[6].OneofWrappers = []any{} - file_api_api_proto_msgTypes[20].OneofWrappers = []any{} + file_api_api_proto_msgTypes[10].OneofWrappers = []any{} + file_api_api_proto_msgTypes[24].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_api_proto_rawDesc), len(file_api_api_proto_rawDesc)), NumEnums: 0, - NumMessages: 40, + NumMessages: 44, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/api/api.proto b/internal/api/api.proto index 2bb208e..f724dce 100644 --- a/internal/api/api.proto +++ b/internal/api/api.proto @@ -11,9 +11,39 @@ message Message { string body = 1; } +// agentVersion and arch are sent by agents that can update themselves, +// updateError when their last try failed message ServerInfo { string serverName = 1; string timezone = 2; + string agentVersion = 3; + string arch = 4; + string updateError = 5; +} + +// An update an admin asked for. The agent downloads the build from +// downloadUrl, the dashboard, and installs it only when it is signed. +message AgentUpdate { + string version = 1; + string downloadUrl = 2; + int64 requestedAt = 3; +} + +// body is field 1 like in Message, so agents from before updates still read it +message PingResponse { + string body = 1; + AgentUpdate update = 2; +} + +message AgentUpdateRequest { + repeated string hosts = 1; + string version = 2; + string downloadUrl = 3; + string by = 4; +} + +message AgentUpdateResult { + int32 requested = 1; } message MonitorData { @@ -56,6 +86,12 @@ message HostSummary { optional double diskFullDays = 16; // empty from agents older than versions string agentVersion = 17; + // the agent updates itself when asked + bool canUpdate = 18; + // an update asked for and not done yet, empty for none + string updateVersion = 19; + int64 updateRequestedAt = 20; + string updateError = 21; } message FleetSummary { @@ -305,7 +341,7 @@ message RuleRequest { service MonitorDataService { // agent rpc Enroll(EnrollRequest) returns (EnrollResponse) {} - rpc HandlePing(ServerInfo) returns (Message) {} + rpc HandlePing(ServerInfo) returns (PingResponse) {} rpc InitAgent(ServerInfo) returns (Message) {} rpc HandleMonitorData(MonitorData) returns (Message) {} rpc HandleCustomMonitorData(MonitorData) returns (Message) {} @@ -341,4 +377,7 @@ service MonitorDataService { rpc AlertRules(Void) returns (AlertRuleList) {} rpc SaveRule(SaveRuleRequest) returns (AlertRuleInfo) {} rpc DeleteRule(RuleRequest) returns (Message) {} + + // ask agents to update themselves, from the dashboard + rpc RequestAgentUpdate(AgentUpdateRequest) returns (AgentUpdateResult) {} } diff --git a/internal/api/api_grpc.pb.go b/internal/api/api_grpc.pb.go index 1c22aa7..0a1abf9 100644 --- a/internal/api/api_grpc.pb.go +++ b/internal/api/api_grpc.pb.go @@ -45,6 +45,7 @@ const ( MonitorDataService_AlertRules_FullMethodName = "/api.MonitorDataService/AlertRules" MonitorDataService_SaveRule_FullMethodName = "/api.MonitorDataService/SaveRule" MonitorDataService_DeleteRule_FullMethodName = "/api.MonitorDataService/DeleteRule" + MonitorDataService_RequestAgentUpdate_FullMethodName = "/api.MonitorDataService/RequestAgentUpdate" ) // MonitorDataServiceClient is the client API for MonitorDataService service. @@ -53,7 +54,7 @@ const ( type MonitorDataServiceClient interface { // agent Enroll(ctx context.Context, in *EnrollRequest, opts ...grpc.CallOption) (*EnrollResponse, error) - HandlePing(ctx context.Context, in *ServerInfo, opts ...grpc.CallOption) (*Message, error) + HandlePing(ctx context.Context, in *ServerInfo, opts ...grpc.CallOption) (*PingResponse, error) InitAgent(ctx context.Context, in *ServerInfo, opts ...grpc.CallOption) (*Message, error) HandleMonitorData(ctx context.Context, in *MonitorData, opts ...grpc.CallOption) (*Message, error) HandleCustomMonitorData(ctx context.Context, in *MonitorData, opts ...grpc.CallOption) (*Message, error) @@ -86,6 +87,8 @@ type MonitorDataServiceClient interface { AlertRules(ctx context.Context, in *Void, opts ...grpc.CallOption) (*AlertRuleList, error) SaveRule(ctx context.Context, in *SaveRuleRequest, opts ...grpc.CallOption) (*AlertRuleInfo, error) DeleteRule(ctx context.Context, in *RuleRequest, opts ...grpc.CallOption) (*Message, error) + // ask agents to update themselves, from the dashboard + RequestAgentUpdate(ctx context.Context, in *AgentUpdateRequest, opts ...grpc.CallOption) (*AgentUpdateResult, error) } type monitorDataServiceClient struct { @@ -106,9 +109,9 @@ func (c *monitorDataServiceClient) Enroll(ctx context.Context, in *EnrollRequest return out, nil } -func (c *monitorDataServiceClient) HandlePing(ctx context.Context, in *ServerInfo, opts ...grpc.CallOption) (*Message, error) { +func (c *monitorDataServiceClient) HandlePing(ctx context.Context, in *ServerInfo, opts ...grpc.CallOption) (*PingResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(Message) + out := new(PingResponse) err := c.cc.Invoke(ctx, MonitorDataService_HandlePing_FullMethodName, in, out, cOpts...) if err != nil { return nil, err @@ -356,13 +359,23 @@ func (c *monitorDataServiceClient) DeleteRule(ctx context.Context, in *RuleReque return out, nil } +func (c *monitorDataServiceClient) RequestAgentUpdate(ctx context.Context, in *AgentUpdateRequest, opts ...grpc.CallOption) (*AgentUpdateResult, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AgentUpdateResult) + err := c.cc.Invoke(ctx, MonitorDataService_RequestAgentUpdate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // MonitorDataServiceServer is the server API for MonitorDataService service. // All implementations must embed UnimplementedMonitorDataServiceServer // for forward compatibility. type MonitorDataServiceServer interface { // agent Enroll(context.Context, *EnrollRequest) (*EnrollResponse, error) - HandlePing(context.Context, *ServerInfo) (*Message, error) + HandlePing(context.Context, *ServerInfo) (*PingResponse, error) InitAgent(context.Context, *ServerInfo) (*Message, error) HandleMonitorData(context.Context, *MonitorData) (*Message, error) HandleCustomMonitorData(context.Context, *MonitorData) (*Message, error) @@ -395,6 +408,8 @@ type MonitorDataServiceServer interface { AlertRules(context.Context, *Void) (*AlertRuleList, error) SaveRule(context.Context, *SaveRuleRequest) (*AlertRuleInfo, error) DeleteRule(context.Context, *RuleRequest) (*Message, error) + // ask agents to update themselves, from the dashboard + RequestAgentUpdate(context.Context, *AgentUpdateRequest) (*AgentUpdateResult, error) mustEmbedUnimplementedMonitorDataServiceServer() } @@ -408,7 +423,7 @@ type UnimplementedMonitorDataServiceServer struct{} func (UnimplementedMonitorDataServiceServer) Enroll(context.Context, *EnrollRequest) (*EnrollResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Enroll not implemented") } -func (UnimplementedMonitorDataServiceServer) HandlePing(context.Context, *ServerInfo) (*Message, error) { +func (UnimplementedMonitorDataServiceServer) HandlePing(context.Context, *ServerInfo) (*PingResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method HandlePing not implemented") } func (UnimplementedMonitorDataServiceServer) InitAgent(context.Context, *ServerInfo) (*Message, error) { @@ -483,6 +498,9 @@ func (UnimplementedMonitorDataServiceServer) SaveRule(context.Context, *SaveRule func (UnimplementedMonitorDataServiceServer) DeleteRule(context.Context, *RuleRequest) (*Message, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteRule not implemented") } +func (UnimplementedMonitorDataServiceServer) RequestAgentUpdate(context.Context, *AgentUpdateRequest) (*AgentUpdateResult, error) { + return nil, status.Errorf(codes.Unimplemented, "method RequestAgentUpdate not implemented") +} func (UnimplementedMonitorDataServiceServer) mustEmbedUnimplementedMonitorDataServiceServer() {} func (UnimplementedMonitorDataServiceServer) testEmbeddedByValue() {} @@ -972,6 +990,24 @@ func _MonitorDataService_DeleteRule_Handler(srv interface{}, ctx context.Context return interceptor(ctx, in, info, handler) } +func _MonitorDataService_RequestAgentUpdate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AgentUpdateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MonitorDataServiceServer).RequestAgentUpdate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MonitorDataService_RequestAgentUpdate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MonitorDataServiceServer).RequestAgentUpdate(ctx, req.(*AgentUpdateRequest)) + } + return interceptor(ctx, in, info, handler) +} + // MonitorDataService_ServiceDesc is the grpc.ServiceDesc for MonitorDataService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1083,6 +1119,10 @@ var MonitorDataService_ServiceDesc = grpc.ServiceDesc{ MethodName: "DeleteRule", Handler: _MonitorDataService_DeleteRule_Handler, }, + { + MethodName: "RequestAgentUpdate", + Handler: _MonitorDataService_RequestAgentUpdate_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "api/api.proto", diff --git a/internal/store/agents.go b/internal/store/agents.go new file mode 100644 index 0000000..c2d1d14 --- /dev/null +++ b/internal/store/agents.go @@ -0,0 +1,63 @@ +package store + +import ( + "context" + "errors" + "time" + + "github.com/jackc/pgx/v5" +) + +// AgentUpdate is an update an admin asked a host's agent to install +type AgentUpdate struct { + Version string + // URL is the dashboard's address, which the agent downloads the build from + URL string + RequestedAt time.Time +} + +// AgentCheckIn records an agent's ping: when the host was last heard from +// and what the agent says about itself. It returns the update waiting for +// the agent, or nil. An update clears once the agent runs the version asked +// for. Agents from before updates send no version and no arch. +func (s *Store) AgentCheckIn(ctx context.Context, host string, at time.Time, agentVersion string, arch string, updateError string) (*AgentUpdate, error) { + var version, url *string + var requestedAt *time.Time + // the SET expressions all see the row as it was + err := s.pool.QueryRow(ctx, ` + UPDATE hosts SET + last_seen = greatest(last_seen, $2), + agent_version = CASE WHEN $3 = '' THEN agent_version ELSE $3 END, + agent_arch = CASE WHEN $4 = '' THEN agent_arch ELSE $4 END, + update_error = CASE WHEN update_version = $3 THEN '' ELSE $5 END, + update_url = CASE WHEN update_version = $3 THEN NULL ELSE update_url END, + update_requested_at = CASE WHEN update_version = $3 THEN NULL ELSE update_requested_at END, + update_version = CASE WHEN update_version = $3 THEN NULL ELSE update_version END + WHERE name = $1 + RETURNING update_version, update_url, update_requested_at`, + host, at, agentVersion, arch, updateError).Scan(&version, &url, &requestedAt) + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrNotFound + } + if err != nil || version == nil { + return nil, err + } + update := &AgentUpdate{Version: *version, URL: *url} + if requestedAt != nil { + update.RequestedAt = *requestedAt + } + return update, nil +} + +// RequestAgentUpdate asks the agents of these hosts to install a version +// from the dashboard at url. Agents that cannot update themselves, or run +// that version already, are left out. It returns how many were asked. +func (s *Store) RequestAgentUpdate(ctx context.Context, hosts []string, version string, url string) (int64, error) { + tag, err := s.pool.Exec(ctx, ` + UPDATE hosts SET update_version = $2, update_url = $3, update_requested_at = now(), update_error = '' + WHERE name = ANY($1) AND agent_arch <> '' AND agent_version <> $2`, hosts, version, url) + if err != nil { + return 0, err + } + return tag.RowsAffected(), nil +} diff --git a/internal/store/agents_test.go b/internal/store/agents_test.go new file mode 100644 index 0000000..e2791d3 --- /dev/null +++ b/internal/store/agents_test.go @@ -0,0 +1,79 @@ +package store + +import ( + "context" + "errors" + "testing" + "time" +) + +func TestAgentUpdates(t *testing.T) { + st := testStore(t) + ctx := context.Background() + for _, host := range []string{"new1", "old1"} { + if err := st.AddHost(ctx, host, "UTC"); err != nil { + t.Fatal(err) + } + } + now := time.Now() + + // new1 can update itself, old1 is from before updates and says nothing + if update, err := st.AgentCheckIn(ctx, "new1", now, "v3.1.0", "arm64", ""); err != nil || update != nil { + t.Fatalf("expected no update yet, got %+v %v", update, err) + } + if _, err := st.AgentCheckIn(ctx, "old1", now, "", "", ""); err != nil { + t.Fatal(err) + } + if _, err := st.AgentCheckIn(ctx, "ghost", now, "", "", ""); !errors.Is(err, ErrNotFound) { + t.Errorf("expected ErrNotFound for an unknown host, got %v", err) + } + + requested, err := st.RequestAgentUpdate(ctx, []string{"new1", "old1"}, "v3.2.0", "https://symon.example.com") + if err != nil || requested != 1 { + t.Fatalf("expected only new1 to be asked, got %d %v", requested, err) + } + // an agent on that version already is not asked again + if requested, err := st.RequestAgentUpdate(ctx, []string{"new1"}, "v3.1.0", "https://symon.example.com"); err != nil || requested != 0 { + t.Errorf("expected no request for the version it runs, got %d %v", requested, err) + } + + update, err := st.AgentCheckIn(ctx, "new1", now, "v3.1.0", "arm64", "") + if err != nil || update == nil || update.Version != "v3.2.0" || update.URL != "https://symon.example.com" || update.RequestedAt.IsZero() { + t.Fatalf("expected the update on the next ping, got %+v %v", update, err) + } + + // a failed try is kept for the dashboard, and the update stays + if update, err := st.AgentCheckIn(ctx, "new1", now, "v3.1.0", "arm64", "the build is not signed with this agent's update key"); err != nil || update == nil { + t.Fatalf("expected the update to stay after a failure, got %+v %v", update, err) + } + fleet, err := st.FleetSummary(ctx) + if err != nil { + t.Fatal(err) + } + var new1, old1 HostSummary + for _, host := range fleet { + switch host.Name { + case "new1": + new1 = host + case "old1": + old1 = host + } + } + if !new1.CanUpdate || new1.AgentVersion != "v3.1.0" || new1.UpdateVersion != "v3.2.0" || new1.UpdateError == "" || new1.UpdateRequestedAt.IsZero() { + t.Errorf("unexpected new1 %+v", new1) + } + if old1.CanUpdate || old1.UpdateVersion != "" { + t.Errorf("unexpected old1 %+v", old1) + } + + // running the new version clears it + if update, err := st.AgentCheckIn(ctx, "new1", now, "v3.2.0", "arm64", ""); err != nil || update != nil { + t.Errorf("expected the update to clear, got %+v %v", update, err) + } + fleet, _ = st.FleetSummary(ctx) + for _, host := range fleet { + if host.Name == "new1" && (host.UpdateVersion != "" || host.UpdateError != "" || host.AgentVersion != "v3.2.0") { + t.Errorf("expected new1 up to date, got %+v", host) + } + } +} diff --git a/internal/store/migrations/011_agent_updates.sql b/internal/store/migrations/011_agent_updates.sql new file mode 100644 index 0000000..56d0efa --- /dev/null +++ b/internal/store/migrations/011_agent_updates.sql @@ -0,0 +1,11 @@ +-- What each agent last said about itself on a ping, and an update an admin +-- asked for. Only agents that can update themselves report their arch. The +-- agent picks an update up on its next ping, and it clears once the agent +-- runs the version asked for. +ALTER TABLE hosts + ADD COLUMN agent_version text NOT NULL DEFAULT '', + ADD COLUMN agent_arch text NOT NULL DEFAULT '', + ADD COLUMN update_version text, + ADD COLUMN update_url text, + ADD COLUMN update_requested_at timestamptz, + ADD COLUMN update_error text NOT NULL DEFAULT ''; diff --git a/internal/store/query.go b/internal/store/query.go index 6c85cb9..5e25b63 100644 --- a/internal/store/query.go +++ b/internal/store/query.go @@ -34,11 +34,19 @@ type HostSummary struct { Containers int // AgentVersion is empty from agents older than versions AgentVersion string + // CanUpdate is true for agents that update themselves when asked + CanUpdate bool + // UpdateVersion is the update asked for and not done yet, empty for none + UpdateVersion string + UpdateRequestedAt time.Time + // UpdateError is why the agent's last try at the update failed + UpdateError string } func (s *Store) FleetSummary(ctx context.Context) ([]HostSummary, error) { rows, err := s.pool.Query(ctx, ` - SELECT h.name, h.last_seen, l.time, l.snapshot, count(a.id), coalesce(max(a.severity), 0) + SELECT h.name, h.last_seen, l.time, l.snapshot, count(a.id), coalesce(max(a.severity), 0), + h.agent_version, h.agent_arch <> '', coalesce(h.update_version, ''), h.update_requested_at, h.update_error FROM hosts h LEFT JOIN host_latest l ON l.host_id = h.id LEFT JOIN alerts a ON a.host_id = h.id AND a.resolved_at IS NULL @@ -54,9 +62,15 @@ func (s *Store) FleetSummary(ctx context.Context) ([]HostSummary, error) { var summary HostSummary var lastSeen, snapshotTime *time.Time var snapshot []byte - if err := rows.Scan(&summary.Name, &lastSeen, &snapshotTime, &snapshot, &summary.ActiveAlerts, &summary.WorstSeverity); err != nil { + var pingVersion string + var requestedAt *time.Time + if err := rows.Scan(&summary.Name, &lastSeen, &snapshotTime, &snapshot, &summary.ActiveAlerts, &summary.WorstSeverity, + &pingVersion, &summary.CanUpdate, &summary.UpdateVersion, &requestedAt, &summary.UpdateError); err != nil { return nil, err } + if requestedAt != nil { + summary.UpdateRequestedAt = *requestedAt + } if lastSeen != nil { summary.LastSeen = *lastSeen } @@ -68,6 +82,10 @@ func (s *Store) FleetSummary(ctx context.Context) ([]HostSummary, error) { } fillSummary(&summary, &data) } + // a ping is newer than the snapshot right after an update + if pingVersion != "" { + summary.AgentVersion = pingVersion + } summaries = append(summaries, summary) } return summaries, rows.Err() From c39f83e9194f86ef0134bef6fb6ac8f8d7ed4190 Mon Sep 17 00:00:00 2001 From: Dhamith Hewamullage Date: Wed, 30 Sep 2026 01:46:05 +0530 Subject: [PATCH 3/5] let agents install a signed update they are asked for --- agent/main.go | 20 +++++- agent/update.go | 164 +++++++++++++++++++++++++++++++++++++++++++ agent/update_test.go | 129 ++++++++++++++++++++++++++++++++++ 3 files changed, 310 insertions(+), 3 deletions(-) create mode 100644 agent/update.go create mode 100644 agent/update_test.go diff --git a/agent/main.go b/agent/main.go index 18166ac..800fecf 100644 --- a/agent/main.go +++ b/agent/main.go @@ -10,6 +10,7 @@ import ( "log" "os" "path/filepath" + "runtime" "strconv" "strings" "sync" @@ -86,6 +87,10 @@ func main() { } logger.Log("info", "agent "+version.String()+" started for "+config.ServerId) + // right away, so the dashboard sees this agent's version, and an update + // that restarted it is confirmed + updates := newUpdater() + sendPing(client, &config, updates) interval := time.Duration(config.MonitorIntervalSeconds) * time.Second collector := monitor.NewCollector(&config) ticker := time.NewTicker(interval) @@ -118,7 +123,7 @@ func main() { for { select { case <-tickerForPing.C: - sendPing(client, &config) + sendPing(client, &config, updates) case <-quitForPing: ticker.Stop() return @@ -193,13 +198,22 @@ func initAgent(client api.MonitorDataServiceClient, config *config.Agent) { fmt.Printf("%s \n", response.Body) } -func sendPing(client api.MonitorDataServiceClient, config *config.Agent) { +// sendPing tells the collector the host is alive, and which agent it runs. +// The reply may carry an update an admin asked for. +func sendPing(client api.MonitorDataServiceClient, config *config.Agent, updates *updater) { ctx, cancel := transport.Context() defer cancel() - _, err := client.HandlePing(ctx, &api.ServerInfo{ServerName: config.ServerId}) + response, err := client.HandlePing(ctx, &api.ServerInfo{ + ServerName: config.ServerId, + AgentVersion: version.String(), + Arch: runtime.GOARCH, + UpdateError: updates.err(), + }) if err != nil { logger.Log("error", "error sending ping: "+err.Error()) + return } + updates.handle(response.Update) } func sendMonitorData(client api.MonitorDataServiceClient, monitorData string, config *config.Agent) { diff --git a/agent/update.go b/agent/update.go new file mode 100644 index 0000000..f1251a5 --- /dev/null +++ b/agent/update.go @@ -0,0 +1,164 @@ +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "sync" + "syscall" + "time" + + "github.com/dhamith93/SyMon/internal/api" + "github.com/dhamith93/SyMon/internal/logger" + "github.com/dhamith93/SyMon/internal/update" + "github.com/dhamith93/SyMon/internal/version" +) + +// An admin can ask an agent to update itself on the dashboard. The +// collector passes the request on in its reply to a ping. The agent +// downloads the build the dashboard hands out, and installs it only when it +// is signed with the key built into this agent, runs on this machine, and +// is the version asked for. + +const ( + updateTimeout = 5 * time.Minute + // no agent build comes close to this + maxBuildSize = 200 << 20 +) + +// updater tries each request once, so a failed update is not retried every +// minute, and keeps the error for the next ping to report +type updater struct { + mu sync.Mutex + tried int64 + lastError string + // install is selfUpdate, replaced in tests. It returns only on failure. + install func(*api.AgentUpdate) error +} + +func newUpdater() *updater { + return &updater{install: selfUpdate} +} + +func (u *updater) handle(request *api.AgentUpdate) { + if request == nil || request.Version == version.String() { + return + } + u.mu.Lock() + if request.RequestedAt == u.tried { + u.mu.Unlock() + return + } + u.tried = request.RequestedAt + u.mu.Unlock() + + logger.Log("info", "updating to "+request.Version+" as asked on the dashboard") + err := u.install(request) + logger.Log("error", "cannot update to "+request.Version+": "+err.Error()) + u.mu.Lock() + u.lastError = err.Error() + u.mu.Unlock() +} + +func (u *updater) err() string { + u.mu.Lock() + defer u.mu.Unlock() + return u.lastError +} + +// selfUpdate replaces this agent with the build asked for and restarts into +// it. It returns only when that failed. +func selfUpdate(request *api.AgentUpdate) error { + exe, err := os.Executable() + if err != nil { + return err + } + if exe, err = filepath.EvalSymlinks(exe); err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), updateTimeout) + defer cancel() + build, signature, err := fetchBuild(ctx, request.DownloadUrl) + if err != nil { + return err + } + if err := installBuild(exe, build, signature, request.Version); err != nil { + return err + } + logger.Log("info", "updated to "+request.Version+", restarting") + return syscall.Exec(exe, os.Args, os.Environ()) +} + +// fetchBuild downloads this machine's build and its signature from the +// dashboard +func fetchBuild(ctx context.Context, dashboard string) ([]byte, []byte, error) { + url := strings.TrimRight(dashboard, "/") + "/downloads/agent-linux-" + runtime.GOARCH + build, err := download(ctx, url) + if err != nil { + return nil, nil, err + } + signature, err := download(ctx, url+".sig") + if err != nil { + return nil, nil, err + } + return build, signature, nil +} + +func download(ctx context.Context, url string) ([]byte, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + response, err := http.DefaultClient.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s answered %s", url, response.Status) + } + data, err := io.ReadAll(io.LimitReader(response.Body, maxBuildSize+1)) + if err != nil { + return nil, err + } + if len(data) > maxBuildSize { + return nil, fmt.Errorf("%s is too big for an agent build", url) + } + return data, nil +} + +// installBuild checks a build and puts it in place of exe +func installBuild(exe string, build []byte, signature []byte, want string) error { + if err := update.Verify(build, signature); err != nil { + return err + } + next := exe + ".new" + if err := os.WriteFile(next, build, 0755); err != nil { + return err + } + if err := os.Chmod(next, 0755); err != nil { + os.Remove(next) + return err + } + // the build has to run here and be the version asked for + out, err := exec.Command(next, "-version").Output() + if err != nil { + os.Remove(next) + return fmt.Errorf("the new build does not run here: %w", err) + } + if got := strings.TrimSpace(string(out)); got != "SyMon agent "+want { + os.Remove(next) + return fmt.Errorf("the new build says %q, not %s", got, want) + } + if err := os.Rename(next, exe); err != nil { + os.Remove(next) + return err + } + return nil +} diff --git a/agent/update_test.go b/agent/update_test.go new file mode 100644 index 0000000..c33bfdb --- /dev/null +++ b/agent/update_test.go @@ -0,0 +1,129 @@ +package main + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/dhamith93/SyMon/internal/api" + "github.com/dhamith93/SyMon/internal/update" +) + +// fakeBuild is a script that answers -version like an agent would +func fakeBuild(version string) []byte { + return []byte("#!/bin/sh\necho 'SyMon agent " + version + "'\n") +} + +// signWithTestKey makes this test's agent trust a new key, and signs with it +func signWithTestKey(t *testing.T) func([]byte) []byte { + t.Helper() + private, public, err := update.NewKey() + if err != nil { + t.Fatal(err) + } + update.PublicKey = public + t.Cleanup(func() { update.PublicKey = "" }) + key, _ := update.ParsePrivateKey(private) + return func(build []byte) []byte { return []byte(update.Sign(key, build)) } +} + +func TestInstallBuild(t *testing.T) { + sign := signWithTestKey(t) + exe := filepath.Join(t.TempDir(), "agent") + if err := os.WriteFile(exe, fakeBuild("v1.0.0"), 0755); err != nil { + t.Fatal(err) + } + unchanged := func() { + t.Helper() + if data, _ := os.ReadFile(exe); string(data) != string(fakeBuild("v1.0.0")) { + t.Fatal("expected the agent to be left alone") + } + if _, err := os.Stat(exe + ".new"); !errors.Is(err, os.ErrNotExist) { + t.Error("expected no leftover .new file") + } + } + + good := fakeBuild("v2.0.0") + if err := installBuild(exe, good, sign([]byte("something else")), "v2.0.0"); err == nil || !strings.Contains(err.Error(), "not signed") { + t.Errorf("expected a signature for another build to fail, got %v", err) + } + unchanged() + if err := installBuild(exe, good, sign(good), "v3.0.0"); err == nil || !strings.Contains(err.Error(), `says "SyMon agent v2.0.0"`) { + t.Errorf("expected a build of another version to fail, got %v", err) + } + unchanged() + broken := []byte("not a program") + if err := installBuild(exe, broken, sign(broken), "v2.0.0"); err == nil || !strings.Contains(err.Error(), "does not run here") { + t.Errorf("expected a build that cannot run to fail, got %v", err) + } + unchanged() + + if err := installBuild(exe, good, sign(good), "v2.0.0"); err != nil { + t.Fatal(err) + } + if data, _ := os.ReadFile(exe); string(data) != string(good) { + t.Error("expected the new build in place") + } +} + +func TestInstallBuildNeedsAKey(t *testing.T) { + update.PublicKey = "" + exe := filepath.Join(t.TempDir(), "agent") + if err := installBuild(exe, fakeBuild("v2.0.0"), []byte("x"), "v2.0.0"); !errors.Is(err, update.ErrNoKey) { + t.Errorf("expected ErrNoKey, got %v", err) + } +} + +func TestFetchBuild(t *testing.T) { + name := "/downloads/agent-linux-" + runtime.GOARCH + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case name: + w.Write([]byte("build")) + case name + ".sig": + w.Write([]byte("signature")) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + build, signature, err := fetchBuild(context.Background(), server.URL+"/") + if err != nil || string(build) != "build" || string(signature) != "signature" { + t.Errorf("expected the build and its signature, got %q %q %v", build, signature, err) + } + if _, _, err := fetchBuild(context.Background(), server.URL+"/elsewhere"); err == nil || !strings.Contains(err.Error(), "404") { + t.Errorf("expected a missing build to fail, got %v", err) + } +} + +func TestUpdaterTriesEachRequestOnce(t *testing.T) { + tries := 0 + updates := &updater{install: func(*api.AgentUpdate) error { + tries++ + return errors.New("the build is not signed with this agent's update key") + }} + + updates.handle(nil) + updates.handle(&api.AgentUpdate{Version: "dev", RequestedAt: 1}) + if tries != 0 { + t.Fatalf("expected no try without a request or for this version, got %d", tries) + } + request := &api.AgentUpdate{Version: "v9.0.0", DownloadUrl: "https://symon.example.com", RequestedAt: 1700000000} + updates.handle(request) + updates.handle(request) + if tries != 1 || updates.err() == "" { + t.Errorf("expected one try and its error kept, got %d %q", tries, updates.err()) + } + // asking again on the dashboard is a new request + updates.handle(&api.AgentUpdate{Version: "v9.0.0", RequestedAt: 1700000100}) + if tries != 2 { + t.Errorf("expected a new request to be tried, got %d", tries) + } +} From 4d36737a892daf0ad5622eac6efb7e8a13c76e52 Mon Sep 17 00:00:00 2001 From: Dhamith Hewamullage Date: Wed, 30 Sep 2026 01:48:11 +0530 Subject: [PATCH 4/5] update agents from the dashboard --- client/internal/server/agents.go | 60 ++++++++++++++++++++++++ client/internal/server/agents_test.go | 66 ++++++++++++++++++++++++++ client/internal/server/install.go | 9 +++- client/internal/server/server.go | 45 +++++++++++------- client/internal/server/server_test.go | 17 +++++-- client/web/src/lib/api.ts | 8 ++++ client/web/src/lib/versions.test.ts | 19 +++++++- client/web/src/lib/versions.ts | 18 +++++++ client/web/src/pages/Fleet.svelte | 48 +++++++++++++++++-- client/web/src/pages/Host.svelte | 67 +++++++++++++++++++++++---- 10 files changed, 320 insertions(+), 37 deletions(-) create mode 100644 client/internal/server/agents.go create mode 100644 client/internal/server/agents_test.go diff --git a/client/internal/server/agents.go b/client/internal/server/agents.go new file mode 100644 index 0000000..e6e036f --- /dev/null +++ b/client/internal/server/agents.go @@ -0,0 +1,60 @@ +package server + +import ( + "encoding/json" + "errors" + "net/http" + "os" + "path/filepath" + + "github.com/dhamith93/SyMon/internal/api" + "github.com/dhamith93/SyMon/internal/version" +) + +// agentArchs are the builds the downloads folder has, one per CPU +var agentArchs = []string{"amd64", "arm64", "arm"} + +// postAgentUpdate asks agents to update themselves to the build the +// dashboard hands out, which has the dashboard's own version. Agents fetch +// it from the dashboard the way the browser reached it, like the install +// script does, and install it only when it is signed. +func (s *server) postAgentUpdate(w http.ResponseWriter, r *http.Request) { + if !jsonBody(r) { + writeError(w, http.StatusUnsupportedMediaType, "send the hosts as JSON") + return + } + var body struct { + Hosts []string `json:"hosts"` + } + if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&body); err != nil || len(body.Hosts) == 0 { + writeError(w, http.StatusBadRequest, "send the hosts to update") + return + } + if !safeHost.MatchString(r.Host) { + writeError(w, http.StatusBadRequest, "unexpected Host header") + return + } + for _, arch := range agentArchs { + if _, err := os.Stat(filepath.Join(s.downloadsDir, "agent-linux-"+arch+".sig")); errors.Is(err, os.ErrNotExist) { + writeError(w, http.StatusConflict, "the agent downloads are not signed, so agents would refuse them. Build them with make pack-client") + return + } + } + + scheme := "http" + if isHTTPS(r) { + scheme = "https" + } + session, _ := s.sessionOf(r) + result, err := s.collector.RequestAgentUpdate(r.Context(), &api.AgentUpdateRequest{ + Hosts: body.Hosts, + Version: version.String(), + DownloadUrl: scheme + "://" + r.Host, + By: session.user, + }) + if err != nil { + writeGRPCError(w, "agent update", err) + return + } + writeJSON(w, map[string]any{"requested": result.Requested, "version": version.String()}) +} diff --git a/client/internal/server/agents_test.go b/client/internal/server/agents_test.go new file mode 100644 index 0000000..3c5e4ff --- /dev/null +++ b/client/internal/server/agents_test.go @@ -0,0 +1,66 @@ +package server + +import ( + "net/http" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/dhamith93/SyMon/internal/api" + "github.com/dhamith93/SyMon/internal/version" +) + +func TestAgentUpdate(t *testing.T) { + s, fake := newTestServer(t, nil) + version.Version = "v3.2.0" + t.Cleanup(func() { version.Version = "" }) + s.downloadsDir = t.TempDir() + body := `{"hosts":["web1","db1"]}` + + // agents would refuse builds that are not signed + if rec := call(s, "POST", "/api/v1/agents/update", body, testSession, asJSON); rec.Code != http.StatusConflict || !strings.Contains(rec.Body.String(), "not signed") { + t.Errorf("expected 409 without signatures, got %d %s", rec.Code, rec.Body) + } + for _, arch := range agentArchs { + if err := os.WriteFile(filepath.Join(s.downloadsDir, "agent-linux-"+arch+".sig"), []byte("signature\n"), 0644); err != nil { + t.Fatal(err) + } + } + + rec := call(s, "POST", "/api/v1/agents/update", body, testSession, func(r *http.Request) { + asJSON(r) + r.Host = "symon.example.com" + r.Header.Set("X-Forwarded-Proto", "https") + }) + if rec.Code != 200 || strings.TrimSpace(rec.Body.String()) != `{"requested":2,"version":"v3.2.0"}` { + t.Fatalf("unexpected response %d %s", rec.Code, rec.Body) + } + request := fake.lastAgentUpdate.Load().(*api.AgentUpdateRequest) + if request.Version != "v3.2.0" || request.DownloadUrl != "https://symon.example.com" || request.By != "tester" || len(request.Hosts) != 2 { + t.Errorf("unexpected request %+v", request) + } + + tests := []struct { + body string + cookie string + prepare func(*http.Request) + code int + }{ + {body, viewerSession, asJSON, http.StatusForbidden}, + {`{"hosts":[]}`, testSession, asJSON, http.StatusBadRequest}, + {body, testSession, nil, http.StatusUnsupportedMediaType}, + {body, "", asJSON, http.StatusUnauthorized}, + } + for _, tt := range tests { + if rec := call(s, "POST", "/api/v1/agents/update", tt.body, tt.cookie, tt.prepare); rec.Code != tt.code { + t.Errorf("%s as %q: got %d %s, want %d", tt.body, tt.cookie, rec.Code, rec.Body, tt.code) + } + } + + // the signatures are downloads too, without a login + rec = call(s, "GET", "/downloads/agent-linux-arm64.sig", "", "", nil) + if rec.Code != 200 || rec.Body.String() != "signature\n" || !strings.HasPrefix(rec.Header().Get("Content-Type"), "text/plain") { + t.Errorf("unexpected signature download %d %q %q", rec.Code, rec.Body, rec.Header().Get("Content-Type")) + } +} diff --git a/client/internal/server/install.go b/client/internal/server/install.go index d3f149d..88f845c 100644 --- a/client/internal/server/install.go +++ b/client/internal/server/install.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "regexp" + "strings" "text/template" ) @@ -18,8 +19,9 @@ var installTemplate = template.Must(template.New("install.sh").Parse(installScri // both values end up inside a shell script, so they are checked first var ( - safeHost = regexp.MustCompile(`^[A-Za-z0-9.\-]+(:[0-9]+)?$|^\[[0-9A-Fa-f:.]+\](:[0-9]+)?$`) - agentFile = regexp.MustCompile(`^agent-linux-(amd64|arm64|arm)$`) + safeHost = regexp.MustCompile(`^[A-Za-z0-9.\-]+(:[0-9]+)?$|^\[[0-9A-Fa-f:.]+\](:[0-9]+)?$`) + // a build, or the signature agents check it with before updating + agentFile = regexp.MustCompile(`^agent-linux-(amd64|arm64|arm)(\.sig)?$`) ) // getInstallScript serves the script that installs and enrolls an agent, @@ -80,5 +82,8 @@ func (s *server) getDownload(w http.ResponseWriter, r *http.Request) { return } w.Header().Set("Content-Type", "application/octet-stream") + if strings.HasSuffix(name, ".sig") { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + } http.ServeFile(w, r, path) } diff --git a/client/internal/server/server.go b/client/internal/server/server.go index f83b319..38f12b0 100644 --- a/client/internal/server/server.go +++ b/client/internal/server/server.go @@ -87,6 +87,7 @@ func (s *server) routes() http.Handler { data.HandleFunc("POST /api/v1/rules", s.requireAdmin(s.postRule)) data.HandleFunc("PUT /api/v1/rules/{id}", s.requireAdmin(s.putRule)) data.HandleFunc("DELETE /api/v1/rules/{id}", s.requireAdmin(s.deleteRule)) + data.HandleFunc("POST /api/v1/agents/update", s.requireAdmin(s.postAgentUpdate)) data.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusNotFound, "no such endpoint") }) @@ -136,6 +137,12 @@ type hostSummary struct { DiskFullDays *float64 `json:"diskFullDays"` // empty from agents older than versions AgentVersion string `json:"agentVersion"` + // the agent updates itself when asked + CanUpdate bool `json:"canUpdate"` + // an update asked for and not done yet, empty for none + UpdateVersion string `json:"updateVersion"` + UpdateRequestedAt int64 `json:"updateRequestedAt"` + UpdateError string `json:"updateError"` } func (s *server) getFleet(w http.ResponseWriter, r *http.Request) { @@ -147,23 +154,27 @@ func (s *server) getFleet(w http.ResponseWriter, r *http.Request) { hosts := make([]hostSummary, 0, len(fleet.Hosts)) for _, h := range fleet.Hosts { hosts = append(hosts, hostSummary{ - Name: h.Name, - Up: h.Up, - LastSeen: h.LastSeen, - Time: h.Time, - OS: h.Os, - UptimeSeconds: h.UptimeSeconds, - CPUPct: h.CpuPct, - MemUsedPct: h.MemUsedPct, - SwapUsedPct: h.SwapUsedPct, - DiskUsedPct: h.DiskUsedPct, - RxBps: h.RxBps, - TxBps: h.TxBps, - ActiveAlerts: h.ActiveAlerts, - WorstSeverity: h.WorstSeverity, - Containers: h.Containers, - DiskFullDays: h.DiskFullDays, - AgentVersion: h.AgentVersion, + Name: h.Name, + Up: h.Up, + LastSeen: h.LastSeen, + Time: h.Time, + OS: h.Os, + UptimeSeconds: h.UptimeSeconds, + CPUPct: h.CpuPct, + MemUsedPct: h.MemUsedPct, + SwapUsedPct: h.SwapUsedPct, + DiskUsedPct: h.DiskUsedPct, + RxBps: h.RxBps, + TxBps: h.TxBps, + ActiveAlerts: h.ActiveAlerts, + WorstSeverity: h.WorstSeverity, + Containers: h.Containers, + DiskFullDays: h.DiskFullDays, + AgentVersion: h.AgentVersion, + CanUpdate: h.CanUpdate, + UpdateVersion: h.UpdateVersion, + UpdateRequestedAt: h.UpdateRequestedAt, + UpdateError: h.UpdateError, }) } writeJSON(w, map[string]any{"hosts": hosts}) diff --git a/client/internal/server/server_test.go b/client/internal/server/server_test.go index 6bdd051..f482de2 100644 --- a/client/internal/server/server_test.go +++ b/client/internal/server/server_test.go @@ -30,11 +30,12 @@ type fakeCollector struct { // login: testSession is valid, alice's password is "correct horse // battery", and the user "locked" has failed too often - noUsers atomic.Bool - sessionChecks atomic.Int32 - passwordChecks atomic.Int32 - loggedOut atomic.Value - lastRuleBy atomic.Value + noUsers atomic.Bool + sessionChecks atomic.Int32 + passwordChecks atomic.Int32 + loggedOut atomic.Value + lastRuleBy atomic.Value + lastAgentUpdate atomic.Value } const ( @@ -191,6 +192,12 @@ func (f *fakeCollector) DeleteRule(ctx context.Context, in *api.RuleRequest) (*a return &api.Message{Body: "ok"}, nil } +// RequestAgentUpdate asks every host it is given +func (f *fakeCollector) RequestAgentUpdate(ctx context.Context, in *api.AgentUpdateRequest) (*api.AgentUpdateResult, error) { + f.lastAgentUpdate.Store(in) + return &api.AgentUpdateResult{Requested: int32(len(in.Hosts))}, nil +} + func floatPtr(v float64) *float64 { return &v } diff --git a/client/web/src/lib/api.ts b/client/web/src/lib/api.ts index c41cb84..077bab9 100644 --- a/client/web/src/lib/api.ts +++ b/client/web/src/lib/api.ts @@ -22,6 +22,13 @@ export interface HostSummary { diskFullDays: number | null; // empty from agents older than versions agentVersion: string; + // the agent updates itself when asked + canUpdate: boolean; + // an update asked for and not done yet, empty for none + updateVersion: string; + updateRequestedAt: number; + // why the agent's last try at the update failed + updateError: string; } // An endpoint's newest check up to the end of a range, and how it did over it @@ -303,6 +310,7 @@ export const api = { logout: () => post('/api/v1/logout', {}), changePassword: (current: string, next: string) => post('/api/v1/password', { current, new: next }), rules: () => get<{ rules: AlertRule[] }>('/api/v1/rules'), + updateAgents: (hosts: string[]) => post<{ requested: number; version: string }>('/api/v1/agents/update', { hosts }), createRule: (enabled: boolean, rule: RuleConfig) => post<{ id: number }>('/api/v1/rules', { enabled, rule }), updateRule: (id: number, enabled: boolean, rule: RuleConfig) => send<{ id: number }>('PUT', `/api/v1/rules/${id}`, { enabled, rule }), deleteRule: (id: number) => send<{ id: number }>('DELETE', `/api/v1/rules/${id}`), diff --git a/client/web/src/lib/versions.test.ts b/client/web/src/lib/versions.test.ts index 0429013..89f6763 100644 --- a/client/web/src/lib/versions.test.ts +++ b/client/web/src/lib/versions.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { agentOutdated } from './versions'; +import type { HostSummary } from './api'; +import { agentOutdated, agentState } from './versions'; describe('agentOutdated', () => { it('compares with the build the dashboard hands out', () => { @@ -13,3 +14,19 @@ describe('agentOutdated', () => { expect(agentOutdated('', '')).toBe(false); }); }); + +describe('agentState', () => { + const host = (fields: Partial) => + ({ agentVersion: 'v3.1.0', canUpdate: true, updateVersion: '', updateError: '', ...fields }) as HostSummary; + + it('follows the update from asked to done', () => { + expect(agentState(host({ agentVersion: 'v3.2.0' }), 'v3.2.0')).toBe('current'); + expect(agentState(host({}), 'v3.2.0')).toBe('available'); + expect(agentState(host({ updateVersion: 'v3.2.0' }), 'v3.2.0')).toBe('requested'); + expect(agentState(host({ updateVersion: 'v3.2.0', updateError: 'not signed' }), 'v3.2.0')).toBe('failed'); + }); + + it('sends agents from before updates to the install command', () => { + expect(agentState(host({ agentVersion: '', canUpdate: false }), 'v3.2.0')).toBe('manual'); + }); +}); diff --git a/client/web/src/lib/versions.ts b/client/web/src/lib/versions.ts index 9f08b41..28e0cdc 100644 --- a/client/web/src/lib/versions.ts +++ b/client/web/src/lib/versions.ts @@ -1,6 +1,24 @@ +import type { HostSummary } from './api'; + // An agent is outdated when it is not the build the dashboard hands out, // which is what the install script upgrades it to. Agents from before // versions send none. export function agentOutdated(agentVersion: string | undefined, dashboardVersion: string): boolean { return dashboardVersion !== '' && agentVersion !== dashboardVersion; } + +// Where a host's agent stands: +// current runs the build the dashboard hands out +// available can be updated from the dashboard +// requested an update was asked for and the agent has not picked it up +// failed the agent tried the update and it did not work +// manual too old to update itself, needs the install command once +export type AgentState = 'current' | 'available' | 'requested' | 'failed' | 'manual'; + +export function agentState(host: HostSummary, dashboardVersion: string): AgentState { + if (!agentOutdated(host.agentVersion, dashboardVersion)) return 'current'; + if (!host.canUpdate) return 'manual'; + if (host.updateVersion && host.updateError) return 'failed'; + if (host.updateVersion) return 'requested'; + return 'available'; +} diff --git a/client/web/src/pages/Fleet.svelte b/client/web/src/pages/Fleet.svelte index dc2f174..d6ceda4 100644 --- a/client/web/src/pages/Fleet.svelte +++ b/client/web/src/pages/Fleet.svelte @@ -4,7 +4,8 @@ import { appConfig } from '../lib/config.svelte'; import { formatAgo, formatDays, formatDuration, formatRate } from '../lib/format'; import { poll } from '../lib/poll'; - import { agentOutdated } from '../lib/versions'; + import { isAdmin } from '../lib/auth.svelte'; + import { agentOutdated, agentState } from '../lib/versions'; import { hostPath } from '../lib/router.svelte'; import Meter from '../components/Meter.svelte'; import Sparkline from '../components/Sparkline.svelte'; @@ -64,6 +65,23 @@ const up = $derived(hosts.filter((h) => h.up).length); const outdatedAgents = $derived(hosts.filter(outdated).length); + // agents that update themselves when asked, including ones whose last try failed + const updatable = $derived(hosts.filter((h) => outdated(h) && ['available', 'failed'].includes(agentState(h, appConfig.version)))); + let updateMessage = $state(''); + let updating = $state(false); + + async function updateAgents() { + updating = true; + try { + const result = await api.updateAgents(updatable.map((h) => h.name)); + updateMessage = `Asked ${result.requested} agent${result.requested === 1 ? '' : 's'} to update to ${result.version}. They pick it up within a minute.`; + await loadFleet(); + } catch (e) { + updateMessage = `Could not ask the agents to update: ${(e as Error).message}`; + } finally { + updating = false; + } + } const openAlerts = $derived(hosts.reduce((sum, h) => sum + h.activeAlerts, 0)); const visible = $derived.by(() => { @@ -103,11 +121,18 @@ {#if outdatedAgents > 0} {#snippet extra()} - Not on {appConfig.version} + {#if isAdmin() && updatable.length > 0} + + {:else} + Not on {appConfig.version} + {/if} {/snippet} {/if} + {#if updateMessage}

{updateMessage}

{/if}
@@ -177,7 +202,14 @@ {/if} {#if outdated(host)} - + {@const state = agentState(host, appConfig.version)} + {#if state === 'requested'} + + {:else if state === 'failed'} + + {:else} + + {/if} {/if}
{/if} @@ -267,6 +299,16 @@ font-size: 12px; } + .update { + height: 26px; + font-size: 12px; + } + + .update-message { + margin: -6px 0 16px; + font-size: 13px; + } + .alerts { display: flex; flex-wrap: wrap; diff --git a/client/web/src/pages/Host.svelte b/client/web/src/pages/Host.svelte index 5eb7c8d..4af0e56 100644 --- a/client/web/src/pages/Host.svelte +++ b/client/web/src/pages/Host.svelte @@ -1,12 +1,13 @@