diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 00000000..785530f2 --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +# Test certificates - safe to ignore. +platform-manageability-agent/test/_dummy-key.pem:private-key:1 +platform-manageability-agent/test/_dummy-cert.pem diff --git a/ena-manifest.yaml b/ena-manifest.yaml index 5913f794..2a133c46 100644 --- a/ena-manifest.yaml +++ b/ena-manifest.yaml @@ -19,7 +19,7 @@ packages: version: 1.7.2 ociArtifact: edge-orch/en/deb/node-agent - name: platform-manageability-agent - version: 0.1.1 + version: 0.1.2 ociArtifact: edge-orch/en/deb/platform-manageability-agent - name: platform-observability-agent version: 1.8.0 diff --git a/platform-manageability-agent/VERSION b/platform-manageability-agent/VERSION index 17e51c38..d917d3e2 100644 --- a/platform-manageability-agent/VERSION +++ b/platform-manageability-agent/VERSION @@ -1 +1 @@ -0.1.1 +0.1.2 diff --git a/platform-manageability-agent/cmd/platform-manageability-agent/platform-manageability-agent.go b/platform-manageability-agent/cmd/platform-manageability-agent/platform-manageability-agent.go index 370e1818..23a0a2d6 100644 --- a/platform-manageability-agent/cmd/platform-manageability-agent/platform-manageability-agent.go +++ b/platform-manageability-agent/cmd/platform-manageability-agent/platform-manageability-agent.go @@ -17,15 +17,33 @@ import ( "time" "github.com/cenkalti/backoff/v4" + "github.com/sirupsen/logrus" + "github.com/open-edge-platform/edge-node-agents/common/pkg/metrics" "github.com/open-edge-platform/edge-node-agents/common/pkg/status" + auth "github.com/open-edge-platform/edge-node-agents/common/pkg/utils" "github.com/open-edge-platform/edge-node-agents/platform-manageability-agent/info" + "github.com/open-edge-platform/edge-node-agents/platform-manageability-agent/internal/comms" "github.com/open-edge-platform/edge-node-agents/platform-manageability-agent/internal/config" "github.com/open-edge-platform/edge-node-agents/platform-manageability-agent/internal/logger" - "github.com/sirupsen/logrus" + "github.com/open-edge-platform/edge-node-agents/platform-manageability-agent/internal/utils" + pb "github.com/open-edge-platform/infra-external/dm-manager/pkg/api/dm-manager" ) -const AGENT_NAME = "platform-manageability-agent" +const ( + AGENT_NAME = "platform-manageability-agent" + MAX_RETRIES = 3 + + // AMTStatus constants representing the state of AMT. + AMTStatusDisabled int32 = 0 + AMTStatusEnabled int32 = 1 +) + +var isAMTEnabled int32 + +func isAMTCurrentlyEnabled() bool { + return atomic.LoadInt32(&isAMTEnabled) == AMTStatusEnabled +} func main() { if len(os.Args) == 2 && os.Args[1] == "version" { @@ -99,8 +117,98 @@ func main() { log.Info("Platform Manageability Agent started successfully") - // Main agent loop using context-aware ticker + tlsConfig, err := auth.GetAuthConfig(auth.GetAuthContext(ctx, confs.AccessTokenPath), nil) + if err != nil { + log.Fatalf("TLS configuration creation failed! Error: %v", err) + } + + log.Infof("Connecting to Device Management Manager at %s", confs.Manageability.ServiceURL) + dmMgrClient := comms.ConnectToDMManager(auth.GetAuthContext(ctx, confs.AccessTokenPath), confs.Manageability.ServiceURL, tlsConfig) + + hostID, err := utils.GetSystemUUID() + if err != nil { + log.Fatalf("Failed to retrieve system UUID with an error: %v", err) + } + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + + op := func() error { + log.Infof("Reporting AMT status for host %s", hostID) + status, err := dmMgrClient.ReportAMTStatus(ctx, hostID) + if err != nil { + log.Errorf("Failed to report AMT status for host %s: %v", hostID, err) + return fmt.Errorf("failed to report AMT status: %w", err) + } + switch status { + case pb.AMTStatus_DISABLED: + atomic.StoreInt32(&isAMTEnabled, AMTStatusDisabled) + case pb.AMTStatus_ENABLED: + atomic.StoreInt32(&isAMTEnabled, AMTStatusEnabled) + default: + log.Warnf("Unknown AMT status: %v, treating as disabled", status) + atomic.StoreInt32(&isAMTEnabled, AMTStatusDisabled) + } + return nil + } + err := backoff.Retry(op, backoff.WithContext(backoff.NewExponentialBackOff(), ctx)) + if err != nil { + if ctx.Err() != nil { + log.Info("AMT status reporting canceled due to context cancellation") + } else { + log.Errorf("Failed to report AMT status for host %s after retries: %v", hostID, err) + } + return + } + log.Infof("Successfully reported AMT status for host %s", hostID) + }() + + var ( + activationCheckInterval = confs.Manageability.HeartbeatInterval + lastActivationCheckTimestamp int64 + ) + wg.Add(1) + go func() { + defer wg.Done() + activationTicker := time.NewTicker(activationCheckInterval) + defer activationTicker.Stop() + + op := func() error { + if !isAMTCurrentlyEnabled() { + log.Info("Skipping activation check because AMT is not enabled") + return nil + } + + log.Infof("AMT is enabled, checking activation details for host %s", hostID) + // FIXME: https://github.com/open-edge-platform/edge-node-agents/pull/170#discussion_r2236433075 + // The suggestion is to combine the activation check and retrieval of activation details into a single call + // to reduce the number of RPC calls. + err = dmMgrClient.RetrieveActivationDetails(ctx, hostID, confs) + if err != nil { + if errors.Is(err, comms.ErrActivationSkipped) { + log.Logger.Debugf("AMT activation skipped for host %s - reason: %v", hostID, err) + return nil + } + return fmt.Errorf("failed to retrieve activation details: %w", err) + } + log.Infof("Successfully retrieved activation details for host %s", hostID) + return nil + } + for { + select { + case <-ctx.Done(): + return + case <-activationTicker.C: + activationTicker.Stop() + updateWithRetry(ctx, log, op, &lastActivationCheckTimestamp) + } + activationTicker.Reset(activationCheckInterval) + } + }() + + // Main agent loop using context-aware ticker var lastUpdateTimestamp int64 ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() @@ -157,6 +265,7 @@ func main() { } }() + wg.Wait() log.Infof("Platform Manageability Agent finished") } @@ -185,3 +294,12 @@ func initStatusClientAndTicker(ctx context.Context, cancel context.CancelFunc, l return statusClient, interval } + +func updateWithRetry(ctx context.Context, log *logrus.Entry, op func() error, lastUpdateTimestamp *int64) { + err := backoff.Retry(op, backoff.WithMaxRetries(backoff.WithContext(backoff.NewExponentialBackOff(), ctx), MAX_RETRIES)) + if err != nil { + log.Errorf("Retry error: %v", err) + } else { + atomic.StoreInt64(lastUpdateTimestamp, time.Now().Unix()) + } +} diff --git a/platform-manageability-agent/configs/sudoers.d/pm-agent b/platform-manageability-agent/configs/sudoers.d/pm-agent index f2300c43..bed172c6 100644 --- a/platform-manageability-agent/configs/sudoers.d/pm-agent +++ b/platform-manageability-agent/configs/sudoers.d/pm-agent @@ -1 +1 @@ -platform-manageability-agent ALL=(root) NOPASSWD:/usr/bin/sh,/usr/bin/rpc \ No newline at end of file +platform-manageability-agent ALL=(root) NOPASSWD:/usr/bin/sh,/usr/bin/rpc,/usr/sbin/dmidecode \ No newline at end of file diff --git a/platform-manageability-agent/go.mod b/platform-manageability-agent/go.mod index 7e00c88e..90d9f332 100644 --- a/platform-manageability-agent/go.mod +++ b/platform-manageability-agent/go.mod @@ -1,12 +1,16 @@ module github.com/open-edge-platform/edge-node-agents/platform-manageability-agent -go 1.24.1 +go 1.24.4 require ( github.com/cenkalti/backoff/v4 v4.3.0 + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 github.com/open-edge-platform/edge-node-agents/common v1.7.1 + github.com/open-edge-platform/infra-external/dm-manager v0.2.2 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 + google.golang.org/grpc v1.73.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -14,35 +18,34 @@ require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250603165357-b52ab10f4468.1 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect github.com/cenkalti/backoff/v5 v5.0.2 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/ebitengine/purego v0.8.4 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/ebitengine/purego v0.8.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/shirou/gopsutil/v4 v4.25.5 // indirect + github.com/shirou/gopsutil/v4 v4.25.4 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/host v0.62.0 // indirect + go.opentelemetry.io/contrib/instrumentation/host v0.61.0 // indirect go.opentelemetry.io/otel v1.37.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/sdk v1.37.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect - go.opentelemetry.io/proto/otlp v1.7.0 // indirect + go.opentelemetry.io/proto/otlp v1.6.0 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/platform-manageability-agent/go.sum b/platform-manageability-agent/go.sum index 54239524..8f78d087 100644 --- a/platform-manageability-agent/go.sum +++ b/platform-manageability-agent/go.sum @@ -7,10 +7,11 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= -github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc= +github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -27,6 +28,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -37,16 +40,19 @@ github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr32 github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/open-edge-platform/edge-node-agents/common v1.7.1 h1:6BrEqf5w9rTk3a4XBuWyAuFnGr0/NW5+8mxjZyA4lfs= github.com/open-edge-platform/edge-node-agents/common v1.7.1/go.mod h1:MbjzFRp5qIY7MxhN2vfC0CE4d/xGwPgfzcBS6ABsHX4= +github.com/open-edge-platform/infra-external/dm-manager v0.2.2 h1:hy7/jTqzgrVPcyeo+QH+pVX44iffN+HoxCT2Z0lxFzw= +github.com/open-edge-platform/infra-external/dm-manager v0.2.2/go.mod h1:9PoYl/Z+994IeWTD1D4WGdFc/Feje9afT3iw3FBbqiQ= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc= -github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/shirou/gopsutil/v4 v4.25.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw= +github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -63,12 +69,14 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/host v0.62.0 h1:hAVkLihKCrIkiX/cUvY0qn6yi0uMdr1/zWpb7lEjdYY= -go.opentelemetry.io/contrib/instrumentation/host v0.62.0/go.mod h1:GiuKDIEAJPhz+D9gApgUxthEVmwC29T73Eg158qBT2g= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ= +go.opentelemetry.io/contrib/instrumentation/host v0.61.0 h1:apz8f6hish67DFuDuBr0erPSmTVO3aN7CPNICiF57o8= +go.opentelemetry.io/contrib/instrumentation/host v0.61.0/go.mod h1:VarXUWiLWgYcG91MOYm0UxZs+ScJeQ181C6PpQ6w1vg= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 h1:zG8GlgXCJQd5BU98C0hZnBbElszTmUgCNCfYneaDL0A= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0/go.mod h1:hOfBCz8kv/wuq73Mx2H2QnWokh/kHZxkh6SNF2bdKtw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 h1:zwdo1gS2eH26Rg+CoqVQpEK1h8gvt5qyU5Kk5Bixvow= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= @@ -77,8 +85,10 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= -go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= -go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= +go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= diff --git a/platform-manageability-agent/internal/comms/comms.go b/platform-manageability-agent/internal/comms/comms.go new file mode 100644 index 00000000..6a4633c5 --- /dev/null +++ b/platform-manageability-agent/internal/comms/comms.go @@ -0,0 +1,236 @@ +// SPDX-FileCopyrightText: (C) 2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +package comms + +import ( + "bufio" + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "strings" + "time" + + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/timeout" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/status" + + "github.com/open-edge-platform/edge-node-agents/platform-manageability-agent/internal/config" + log "github.com/open-edge-platform/edge-node-agents/platform-manageability-agent/internal/logger" + "github.com/open-edge-platform/edge-node-agents/platform-manageability-agent/internal/utils" + pb "github.com/open-edge-platform/infra-external/dm-manager/pkg/api/dm-manager" +) + +const ( + retryInterval = 10 * time.Second + tickerInterval = 500 * time.Millisecond + connTimeout = 5 * time.Second +) + +var ErrActivationSkipped = errors.New("activation skipped") + +type Client struct { + DMMgrServiceAddr string + Dialer grpc.DialOption + Transport grpc.DialOption + GrpcConn *grpc.ClientConn + DMMgrClient pb.DeviceManagementClient + RetryInterval time.Duration + Executor utils.CommandExecutor +} + +func WithNetworkDialer(serviceAddr string) func(*Client) { + return func(s *Client) { + s.Dialer = grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { + return net.Dial("tcp", serviceAddr) + }) + } +} + +func NewClient(serviceURL string, tlsConfig *tls.Config, options ...func(*Client)) *Client { + cli := &Client{} + cli.DMMgrServiceAddr = serviceURL + cli.RetryInterval = retryInterval + cli.Transport = grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)) + cli.Executor = &utils.RealCommandExecutor{} + + WithNetworkDialer(cli.DMMgrServiceAddr)(cli) + + // options can be used to override default values, e.g. from unit tests + for _, o := range options { + o(cli) + } + return cli +} + +func (cli *Client) Connect(ctx context.Context) (err error) { + cli.GrpcConn, err = grpc.DialContext(ctx, cli.DMMgrServiceAddr, cli.Transport, cli.Dialer, //nolint:staticcheck + grpc.WithUnaryInterceptor(timeout.UnaryClientInterceptor(connTimeout)), + grpc.WithStatsHandler(otelgrpc.NewClientHandler())) + if err != nil { + return fmt.Errorf("connection to %v failed: %w", cli.DMMgrServiceAddr, err) + } + cli.DMMgrClient = pb.NewDeviceManagementClient(cli.GrpcConn) + return nil +} + +func ConnectToDMManager(ctx context.Context, serviceAddr string, tlsConfig *tls.Config) *Client { + dmMgr := NewClient(serviceAddr, tlsConfig) + + cyclicalTicker := time.NewTicker(tickerInterval) + defer cyclicalTicker.Stop() + + for { + select { + case <-ctx.Done(): + log.Logger.Info("Connecting to DM Manager has been canceled") + return nil + case <-cyclicalTicker.C: + err := dmMgr.Connect(ctx) + if err != nil { + log.Logger.Warnf("Can't connect to DM Manager, retrying: %v", err) + time.Sleep(dmMgr.RetryInterval) + continue + } + log.Logger.Info("Successfully connected to DM Manager") + return dmMgr + } + } +} + +// parseAMTInfo parses the output of the `rpc amtinfo` command and populates the AMTStatusRequest. +func parseAMTInfo(uuid string, output []byte) *pb.AMTStatusRequest { + var ( + status = pb.AMTStatus_DISABLED + version string + ) + + scanner := bufio.NewScanner(strings.NewReader(string(output))) + for scanner.Scan() { + line := scanner.Text() + + if strings.HasPrefix(line, "Version") { + parts := strings.Split(line, ":") + if len(parts) > 1 { + version = strings.TrimSpace(parts[1]) + status = pb.AMTStatus_ENABLED + } + } + } + + req := &pb.AMTStatusRequest{ + HostId: uuid, + Status: status, + Version: version, + } + return req +} + +// ReportAMTStatus executes the `rpc amtinfo` command, parses the output, and sends the AMT status to the server. +func (cli *Client) ReportAMTStatus(ctx context.Context, hostID string) (pb.AMTStatus, error) { + defaultStatus := pb.AMTStatus_DISABLED + + output, err := cli.Executor.ExecuteAMTInfo() + if err != nil { + req := &pb.AMTStatusRequest{ + HostId: hostID, + Status: defaultStatus, + Version: "", + } + _, reportErr := cli.DMMgrClient.ReportAMTStatus(ctx, req) + if reportErr != nil { + return defaultStatus, fmt.Errorf("failed to report AMTStatus to DM Manager: %w", reportErr) + } + return defaultStatus, fmt.Errorf("failed to execute `rpc amtinfo` command: %w", err) + } + + req := parseAMTInfo(hostID, output) + _, err = cli.DMMgrClient.ReportAMTStatus(ctx, req) + if err != nil { + if st, ok := status.FromError(err); ok { + switch st.Code() { + case codes.FailedPrecondition: + log.Logger.Debugf("Received %v, %v", st.Message(), err.Error()) + return req.Status, nil + } + } + return defaultStatus, fmt.Errorf("failed to report AMT status: %w", err) + } + + log.Logger.Infof("Reported AMT status: HostID=%s, Status=%v, Version=%s", + req.HostId, req.Status, req.Version) + return req.Status, nil +} + +// RetrieveActivationDetails retrieves activation details and executes the activation command if required. +func (cli *Client) RetrieveActivationDetails(ctx context.Context, hostID string, conf *config.Config) error { + req := &pb.ActivationRequest{ + HostId: hostID, + } + resp, err := cli.DMMgrClient.RetrieveActivationDetails(ctx, req) + if err != nil { + if st, ok := status.FromError(err); ok { + switch st.Code() { + case codes.FailedPrecondition: + return fmt.Errorf("%w: host %s precondition failed - %v", ErrActivationSkipped, hostID, st.Message()) + } + } + return fmt.Errorf("failed to retrieve activation details for host %s: %w", hostID, err) + } + + log.Logger.Debugf("Retrieved activation details: HostID=%s, Operation=%v, ProfileName=%s", + resp.HostId, resp.Operation, resp.ProfileName) + + if resp.Operation == pb.OperationType_ACTIVATE { + rpsAddress := fmt.Sprintf("wss://%s/activate", conf.RPSAddress) + // TODO: + // This is a placeholder, replace with actual logic to fetch the password. + // Need to check how to fetch the password from dm-manager, hardcoded for now. + password := "P@ssw0rd" + output, err := cli.Executor.ExecuteAMTActivate(rpsAddress, resp.ProfileName, password) + if err != nil { + return fmt.Errorf("failed to execute activation command for host %s: %w, Output: %s", + hostID, err, string(output)) + } + log.Logger.Debugf("Activation command output for host %s: %s", hostID, string(output)) + + var req *pb.ActivationResultRequest + if isProvisioned(string(output)) { + req = &pb.ActivationResultRequest{ + HostId: hostID, + ActivationStatus: pb.ActivationStatus_PROVISIONED, + } + log.Logger.Infof("Provisioning successful for host: %s", hostID) + } else { + req = &pb.ActivationResultRequest{ + HostId: hostID, + ActivationStatus: pb.ActivationStatus_FAILED, + } + log.Logger.Infof("Provisioning failed for host: %s", hostID) + } + _, err = cli.DMMgrClient.ReportActivationResults(ctx, req) + if err != nil { + return fmt.Errorf("failed to report Activation results for host: %s, error: %w", hostID, err) + } + log.Logger.Debugf("Reported activation results: HostID=%s, ActivationStatus=%v", + hostID, req.ActivationStatus) + } + return nil +} + +// isProvisioned checks if the output contains the line indicating provisioning success. +func isProvisioned(output string) bool { + scanner := bufio.NewScanner(strings.NewReader(output)) + for scanner.Scan() { + line := scanner.Text() + if strings.Contains(line, `msg="CIRA: Configured"`) { + return true + } + } + return false +} diff --git a/platform-manageability-agent/internal/comms/comms_test.go b/platform-manageability-agent/internal/comms/comms_test.go new file mode 100644 index 00000000..87127a92 --- /dev/null +++ b/platform-manageability-agent/internal/comms/comms_test.go @@ -0,0 +1,255 @@ +// SPDX-FileCopyrightText: (C) 2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +package comms_test + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "testing" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/test/bufconn" + + "github.com/open-edge-platform/edge-node-agents/platform-manageability-agent/internal/comms" + "github.com/open-edge-platform/edge-node-agents/platform-manageability-agent/internal/config" + log "github.com/open-edge-platform/edge-node-agents/platform-manageability-agent/internal/logger" + "github.com/open-edge-platform/edge-node-agents/platform-manageability-agent/internal/utils" + pb "github.com/open-edge-platform/infra-external/dm-manager/pkg/api/dm-manager" +) + +type mockDeviceManagementServer struct { + pb.UnimplementedDeviceManagementServer + operationType pb.OperationType + onReportActivationResults func(context.Context, *pb.ActivationResultRequest) (*pb.ActivationResultResponse, error) +} + +type mockCommandExecutor struct { + amtInfoOutput []byte + amtInfoError error + activationOutput []byte + activationError error +} + +func (m *mockCommandExecutor) ExecuteAMTInfo() ([]byte, error) { + return m.amtInfoOutput, m.amtInfoError +} + +func (m *mockCommandExecutor) ExecuteAMTActivate(rpsAddress, profileName, password string) ([]byte, error) { + return m.activationOutput, m.activationError +} + +func (m *mockDeviceManagementServer) ReportAMTStatus(ctx context.Context, req *pb.AMTStatusRequest) (*pb.AMTStatusResponse, error) { + log.Logger.Infof("Received ReportAMTStatus request: HostID=%s, Status=%v, Version=%s", req.HostId, req.Status, req.Version) + return &pb.AMTStatusResponse{}, nil +} + +func (m *mockDeviceManagementServer) RetrieveActivationDetails(ctx context.Context, req *pb.ActivationRequest) (*pb.ActivationDetailsResponse, error) { + log.Logger.Infof("Received RetrieveActivationDetails request: %v", req) + return &pb.ActivationDetailsResponse{ + HostId: req.HostId, + Operation: m.operationType, + ProfileName: "mock-profile", + }, nil +} + +func (m *mockDeviceManagementServer) ReportActivationResults(ctx context.Context, req *pb.ActivationResultRequest) (*pb.ActivationResultResponse, error) { + if m.onReportActivationResults != nil { + return m.onReportActivationResults(ctx, req) + } + log.Logger.Infof("Received ReportActivationResults request: %v", req) + return &pb.ActivationResultResponse{}, nil +} + +func runMockServer(server pb.DeviceManagementServer) (*bufconn.Listener, *grpc.Server) { + lis := bufconn.Listen(1024 * 1024) + creds, err := credentials.NewServerTLSFromFile("../../test/_dummy-cert.pem", "../../test/_dummy-key.pem") + if err != nil { + log.Logger.Fatalf("Failed to load TLS credentials: %v", err) + } + s := grpc.NewServer(grpc.Creds(creds)) + pb.RegisterDeviceManagementServer(s, server) + go func() { + if err := s.Serve(lis); err != nil { + log.Logger.Infof("Mock server stopped: %v", err) + } + }() + return lis, s +} + +func WithBufconnDialer(lis *bufconn.Listener) func(*comms.Client) { + return func(cli *comms.Client) { + cli.Dialer = grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { + return lis.Dial() + }) + } +} + +func WithMockExecutor(executor utils.CommandExecutor) func(*comms.Client) { + return func(c *comms.Client) { + c.Executor = executor + } +} + +func TestRetrieveActivationDetails_DeactivateOperation(t *testing.T) { + lis, server := runMockServer(&mockDeviceManagementServer{ + operationType: pb.OperationType_DEACTIVATE, + }) + defer func() { + server.GracefulStop() + lis.Close() + }() + + tlsConfig := &tls.Config{InsecureSkipVerify: true} + client := comms.NewClient("mock-service", tlsConfig, WithBufconnDialer(lis)) + + err := client.Connect(context.Background()) + assert.NoError(t, err, "Client should connect successfully") + + err = client.RetrieveActivationDetails(context.Background(), "host-id", &config.Config{ + RPSAddress: "mock-service", + }) + assert.NoError(t, err, "RetrieveActivationDetails for deactivate should not process") +} + +func TestRetrieveActivationDetails_Success(t *testing.T) { + var capturedRequest *pb.ActivationResultRequest + + mockServer := &mockDeviceManagementServer{ + operationType: pb.OperationType_ACTIVATE, + onReportActivationResults: func(ctx context.Context, req *pb.ActivationResultRequest) (*pb.ActivationResultResponse, error) { + capturedRequest = req // Capture the request to verify later. + log.Logger.Infof("Received ReportActivationResults request: %v", req) + return &pb.ActivationResultResponse{}, nil + }, + } + + lis, server := runMockServer(mockServer) + defer func() { + server.GracefulStop() + lis.Close() + }() + + // Create mock executor with successful activation output. + mockExecutor := &mockCommandExecutor{ + activationOutput: []byte(`msg="CIRA: Configured"`), + activationError: nil, + } + + tlsConfig := &tls.Config{InsecureSkipVerify: true} + client := comms.NewClient("mock-service", tlsConfig, + WithBufconnDialer(lis), + WithMockExecutor(mockExecutor)) + + err := client.Connect(context.Background()) + assert.NoError(t, err, "Client should connect successfully") + + err = client.RetrieveActivationDetails(context.Background(), "host-id", &config.Config{ + RPSAddress: "mock-service", + }) + assert.NoError(t, err, "RetrieveActivationDetails should succeed") + + // Verify that the activation result was reported with PROVISIONED status. + assert.NotNil(t, capturedRequest, "Activation result should have been reported") + assert.Equal(t, "host-id", capturedRequest.HostId, "Host ID should match") + assert.Equal(t, pb.ActivationStatus_PROVISIONED, capturedRequest.ActivationStatus, + "Activation status should be PROVISIONED when CIRA is configured") +} + +func TestRetrieveActivationDetails_Failed(t *testing.T) { + var capturedRequest *pb.ActivationResultRequest + + mockServer := &mockDeviceManagementServer{ + operationType: pb.OperationType_ACTIVATE, + onReportActivationResults: func(ctx context.Context, req *pb.ActivationResultRequest) (*pb.ActivationResultResponse, error) { + capturedRequest = req // Capture the request to verify later. + log.Logger.Infof("Received ReportActivationResults request: %v", req) + return &pb.ActivationResultResponse{}, nil + }, + } + + lis, server := runMockServer(mockServer) + defer func() { + server.GracefulStop() + lis.Close() + }() + + // Create mock executor with failed activation output (no CIRA: Configured). + mockExecutor := &mockCommandExecutor{ + activationOutput: []byte(`msg="Activation failed"`), + activationError: nil, + } + + tlsConfig := &tls.Config{InsecureSkipVerify: true} + client := comms.NewClient("mock-service", tlsConfig, + WithBufconnDialer(lis), + WithMockExecutor(mockExecutor)) + + err := client.Connect(context.Background()) + assert.NoError(t, err, "Client should connect successfully") + + err = client.RetrieveActivationDetails(context.Background(), "host-id", &config.Config{ + RPSAddress: "mock-service", + }) + assert.NoError(t, err, "RetrieveActivationDetails should succeed") + + // Verify that the activation result was reported with FAILED status. + assert.NotNil(t, capturedRequest, "Activation result should have been reported") + assert.Equal(t, "host-id", capturedRequest.HostId, "Host ID should match") + assert.Equal(t, pb.ActivationStatus_FAILED, capturedRequest.ActivationStatus, + "Activation status should be FAILED when CIRA is not configured") +} + +func TestReportAMTStatus_Success(t *testing.T) { + lis, server := runMockServer(&mockDeviceManagementServer{}) + defer func() { + server.GracefulStop() + lis.Close() + }() + + mockExecutor := &mockCommandExecutor{ + amtInfoOutput: []byte("Version: 16.1.25.1424\nBuild Number: 3425\nRecovery Version: 16.1.25.1424"), + amtInfoError: nil, + } + + tlsConfig := &tls.Config{InsecureSkipVerify: true} + client := comms.NewClient("mock-service", tlsConfig, + WithBufconnDialer(lis), + WithMockExecutor(mockExecutor), + ) + + err := client.Connect(context.Background()) + assert.NoError(t, err, "Client should connect successfully") + + _, err = client.ReportAMTStatus(context.Background(), "host-id") + assert.NoError(t, err, "ReportAMTStatus should succeed") +} + +func TestReportAMTStatus_CommandFailure(t *testing.T) { + lis, server := runMockServer(&mockDeviceManagementServer{}) + defer func() { + server.GracefulStop() + lis.Close() + }() + + mockExecutor := &mockCommandExecutor{ + amtInfoOutput: nil, + amtInfoError: fmt.Errorf("command failed"), + } + + tlsConfig := &tls.Config{InsecureSkipVerify: true} + client := comms.NewClient("mock-service", tlsConfig, + WithBufconnDialer(lis), + WithMockExecutor(mockExecutor)) + + err := client.Connect(context.Background()) + assert.NoError(t, err, "Client should connect successfully") + + status, err := client.ReportAMTStatus(context.Background(), "host-id") + assert.Error(t, err, "ReportAMTStatus should fail when command fails") + assert.Equal(t, pb.AMTStatus_DISABLED, status, "AMT should be disabled on failure") +} diff --git a/platform-manageability-agent/internal/config/config.go b/platform-manageability-agent/internal/config/config.go index 7324fb39..0a56e744 100644 --- a/platform-manageability-agent/internal/config/config.go +++ b/platform-manageability-agent/internal/config/config.go @@ -31,6 +31,7 @@ type Config struct { StatusEndpoint string `yaml:"statusEndpoint"` MetricsEndpoint string `yaml:"metricsEndpoint"` MetricsInterval time.Duration `yaml:"metricsInterval"` + RPSAddress string `yaml:"rpsAddress"` AccessTokenPath string `yaml:"accessTokenPath"` } diff --git a/platform-manageability-agent/internal/utils/utils.go b/platform-manageability-agent/internal/utils/utils.go new file mode 100644 index 00000000..7ebdba4c --- /dev/null +++ b/platform-manageability-agent/internal/utils/utils.go @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: (C) 2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +package utils + +import ( + "fmt" + "os/exec" + "strings" + "time" + + log "github.com/open-edge-platform/edge-node-agents/platform-manageability-agent/internal/logger" +) + +// CommandExecutor defines an interface for executing commands. +// This allows for mocking in tests. +type CommandExecutor interface { + ExecuteAMTInfo() ([]byte, error) + ExecuteAMTActivate(rpsAddress, profileName, password string) ([]byte, error) +} + +type RealCommandExecutor struct{} + +// ExecuteAMTInfo executes the AMT info command with retries. +func (r *RealCommandExecutor) ExecuteAMTInfo() ([]byte, error) { + maxRetries := 3 + retryInterval := 5 * time.Second + + var err error + for i := 1; i <= maxRetries; i++ { + cmd := exec.Command("sudo", "/etc/intel_edge_node/rpc", "amtinfo") + output, err := cmd.Output() + if err == nil { + return output, nil + } + log.Logger.Warnf("Failed to execute AMT info command (attempt %d/%d): %v", i, maxRetries, err) + if i < maxRetries { + time.Sleep(retryInterval) + } + } + return nil, fmt.Errorf("amtInfo command failed after %d retries: %v", maxRetries, err) +} + +// ExecuteAMTActivate executes the AMT activate command. +func (r *RealCommandExecutor) ExecuteAMTActivate(rpsAddress, profileName, password string) ([]byte, error) { + cmd := exec.Command("sudo", "/etc/intel_edge_node/rpc", "activate", "-u", rpsAddress, "-profile", profileName, "-password", password, "-n") + return cmd.CombinedOutput() +} + +func GetSystemUUID() (string, error) { + cmd := exec.Command("sudo", "dmidecode", "-s", "system-uuid") + uuid, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to retrieve UUID: %v", err) + } + return strings.TrimSpace(string(uuid)), nil +} diff --git a/platform-manageability-agent/test/_dummy-cert.pem b/platform-manageability-agent/test/_dummy-cert.pem new file mode 100644 index 00000000..0489de41 --- /dev/null +++ b/platform-manageability-agent/test/_dummy-cert.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFJTCCAw2gAwIBAgIUaagGTC3GU6IXhPV7jnFBMZPH0SgwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDcyMjA5MjQxN1oXDTI2MDcy +MjA5MjQxN1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEApEABHMxeXb0GjLPioSr1r7Ox4UZTFxKizN/r5o/6DdWm +PZf7vfroXSywIIy7GMft6ePs+J7tHRfuXpaHtEWVOI8luOvB7QXHbIi/mnFzHc2A +48TKeA8omHazfOiJsLMF0D5O8STzpBOOmLYXwIXDY03iuv6J1u8M4nay+2biIP+q +dnFwxqeJNh4xc5hKj+wMLilQecJ5H2PBKM+SDxuvewHB38NIG+/pt2pwSkhw/+yb +SbiWq7bccOkqMs+yvrBRYd4Xt7XAjfig4jqK5LdZmqyQcfx5pQNe2D/MXx/uP8sw +t/plDhcdy4gECGERxL34/VgGD3UcHDww73rVrWgJg+XDsNbf8sDy7UYTPWmKEu3y +n4/zS90uaiGs+LJsmNtk/Jgmc1eO73SaLWL/EQjWQgW73ML5NBWvwFc9wZ1PdL4a +BcNMrnWlhboZyjv0Mqf/iGBPBfEshhPmEJlkRPMPKOOhj3sVeVlRibRdcOg2LbpB +idX9gy4iW07lcc0FYL1s5Tz3wPp4TY/7jqLkXtG30R41UfG3w8KAEcuzo1Gz7jKx +PiEg31fH3ipf8FO/3q/hkbDWLoj7Cz5PV2A8pSCZbfcJNXDmTcFJb9q9kvm6gNyj +z2PM4KlcQwidrcL7Kkb4QxKORvabLOcPaEeCt7xetrwqw18pwyCxFiquEBtaN7kC +AwEAAaNvMG0wHQYDVR0OBBYEFI/UrUVGjHkF6yxdXcZQLy64ITySMB8GA1UdIwQY +MBaAFI/UrUVGjHkF6yxdXcZQLy64ITySMA8GA1UdEwEB/wQFMAMBAf8wGgYDVR0R +BBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4ICAQAB6K9RcPrM +2hSzC//OLQJf0D/yUFT/jnZ7k6Z4yJrDMo/+dUA8lgB4ZZeIcFKGw5kxJ5WKNeMD +W00jOuCVj0pHQPblMXPdelNqHjf3bjtDzpbbzlAcBjlzAj8S/pHXW8q2zvTa9xim +zvYn/M9CH/zfL3DuouYhOMMoAOviBZIlpT3b2lg55zYyb6cCCXFy9BHlLjC1IIbV +RS1KWCJaknbjUQmj6X2KPhbCzRgz9KrKlAnTtd0wQCtBBwXFAA5G/p8W6c4tAjSM +mmTxtLl7qDmLHZLGzAcubbkpwKXSKnSLprtyy5JnVtzTmkHvkMjgc3sFSX57O8sF +1/OJXtMvSb+0TWkQJFWA5DIjhsKK0W7mw0+kTR7DJBtr3x2qs5IWN1C4A8QSVfLx +XbZKE+bWL2vIQfuxvao8kVcgxekcV/PPzMNYNq0bu23GsfIafGsKF8L53jhskJhF +mPHK8SChkeWmiCVfSDfyEY/5twJHLsZWDNZarQPg6h3YiLubKDFhvY3d3rh30jr+ +LWlDBEl4y6g5ROsXzO4mQi1t8uqcUOfR7HEjeGGcTGSby5jkL1axBlsE5othzdiS +xCrp2qk4YR57URNGPdvHR1i4ydJln3PbkwoqpQzVcmkpf9YhSbyMKOa4ibj8N1AM +0UAPTptaL3d5Ned8uOM+QvqOZYbU/CIHTg== +-----END CERTIFICATE----- diff --git a/platform-manageability-agent/test/_dummy-key.pem b/platform-manageability-agent/test/_dummy-key.pem new file mode 100644 index 00000000..e8734901 --- /dev/null +++ b/platform-manageability-agent/test/_dummy-key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQCkQAEczF5dvQaM +s+KhKvWvs7HhRlMXEqLM3+vmj/oN1aY9l/u9+uhdLLAgjLsYx+3p4+z4nu0dF+5e +loe0RZU4jyW468HtBcdsiL+acXMdzYDjxMp4DyiYdrN86ImwswXQPk7xJPOkE46Y +thfAhcNjTeK6/onW7wzidrL7ZuIg/6p2cXDGp4k2HjFzmEqP7AwuKVB5wnkfY8Eo +z5IPG697AcHfw0gb7+m3anBKSHD/7JtJuJarttxw6Soyz7K+sFFh3he3tcCN+KDi +Oorkt1marJBx/HmlA17YP8xfH+4/yzC3+mUOFx3LiAQIYRHEvfj9WAYPdRwcPDDv +etWtaAmD5cOw1t/ywPLtRhM9aYoS7fKfj/NL3S5qIaz4smyY22T8mCZzV47vdJot +Yv8RCNZCBbvcwvk0Fa/AVz3BnU90vhoFw0yudaWFuhnKO/Qyp/+IYE8F8SyGE+YQ +mWRE8w8o46GPexV5WVGJtF1w6DYtukGJ1f2DLiJbTuVxzQVgvWzlPPfA+nhNj/uO +ouRe0bfRHjVR8bfDwoARy7OjUbPuMrE+ISDfV8feKl/wU7/er+GRsNYuiPsLPk9X +YDylIJlt9wk1cOZNwUlv2r2S+bqA3KPPY8zgqVxDCJ2twvsqRvhDEo5G9pss5w9o +R4K3vF62vCrDXynDILEWKq4QG1o3uQIDAQABAoICAAP6C7zZguBkovu1oENUMbIh +GOmyJPFeUWRB+Klyq3R5y4Zav2zcXhNhcVs8ZQxGzO8YhmSHnoiRi4MtRFhcRe5i +YuCtDpsxRGCPmAoCfK3IS7pYCanp/grg9twLmkfVscqWs50Zjq+WjKGMaRtPccld ++aMa5hX24Do8cv1swoVXg1ZOTVN1+3e8t+hLU+TTq4DTnb/w8jNNHEBjdC8a9b71 +7i5/gSeyAZfgAxt9UQUVQrZAep9qNECkmJG13ydISBQlRzA6vZCCaEzJ65J5kPkS +fAXSfdR9tiZotwdHk+JmBkYyEQ/MBfvnTWexh/daPSTPJIhrxrQ1Td8Ce5lw9yf5 +KRbR0mTp7cbpS5OHogPTMP2i0kX0RZW7HXNi0oWziOen47NIMp45UKVZgwSF2tGb +ZqwOpv4vTJXjsBe7G318RUAp5IIi0FqdQdGH0AaPX+UL1hV7yeBirSDJP8pAr6Be +zy8aA+7PBBdH3Z1mokpu5xiLPTxwRjs1mR4Zr/ox6MOvpROb1taEoe9EAGzvCDSZ +ARrs+KOe2MSQojaEevUgwz0oK+QMyfg86nSXy1b+XvrVFOlGdHnjcpD8+LTFc0nN +U4udd61TlhLgqcYhjte/+XUPSmkEIzRr/PgGPQ+MDOqNaEEPbgCV5xgkW1V3vAJf +ft5dEpJmcjGjgMrC0e+NAoIBAQDfaZcBuTpsh8Ja3DKy7TtiMeHio0jvtVIyvUiX ++3ib5h3nKid0MJCtC5vkVLQbZIqsMPrMuyYWL1u6UTCdTBqUWfW/prPPKOYTv8IK +9ILWVnuDkxHvfgMFhsXBpQaQP7vMjJuhRzyWqCRZDzrSJWX94uKd8v7Fc7R1sryI +7Z4wN+nt/gztho52PRSQ/GOQAd7ao/l7vxyjZjgHNCHmywO+vkmLY22jPT2vNNxQ +VNyQC2Mo5Ew2qzwTpDlz9mIhlGThP/DAJxhQ8YlU4ucABhOGH7Jnp7kFg3yw8CcR +EV/lLh6XjNzHyVxXV7kMamf/mckJgBFgN47ntTUJ0Lg1UDflAoIBAQC8NT0jpadM +CehleCtPT138edI8wUPmAUK8XlLtkJ9+Hf9GKfebRH0Kc6OUTlCvwjqkUSvh00bI +FE7J2+vrxSRlaBB5o0/8R2TyJnBQDAJbxXVKQqhR+A2YT/izwb59nI5LoNhkOfUi +rECJ6Hx4IH/o+Xa5RDTeWXgMVEY3sp8JHp8J9jvou6QphbP1pDsSarPVV63gPbCa +LZaWRVHK1AFeEbPxp04uq3RVdxDoXV6JMYFoUvNnhviaHlyOOXobhrV+mT1tn9Jg +WMstsy15hwrAMrayvfikqHxCE5G2ibWNPDsYlNdBueTnsoO35G4Ou0/KGikxQMdw +WOqTOQUyHhtFAoIBAQCZaN4idtihcc+JqK6/opsmYG5lvA33XCDnaoaQpkM5ehiK +ha214StXSCNx9KAAN1fpyXBOaSxMC5UHKX0iZgHSYLuZMJVD6Sej5AcQkrnNCHGj +9bdZJsRYZSUiRXluT/VbDipH4qy+HxNmfEi9yKRyY/uMzjEX0M3YDDXeKJx5K8vc +epjR+ZWVp3eZ31VWq5IlvHcx/BdkNGrAt5GX80vNDMlfh5TfhMv4pWmrV7pkxwz+ +JVYYLBkCu8Af2s2jlUx4R/m/WJkf69uLiItqvsFlVhqhhtBL8vAKZ73LwIyCyNqE +4lswbRqVd01AM0jagPVFKsXDtiLRBOM4rDPxl/XpAoIBAQCcPf5Bh5W49EivY/Fg +Z9z4ahF0SmUZnlZOlmd1vKgLRM/U6aYaieLcOF/GELW/ExknrBMn6ANMuj8mKKJU +Glc6sAdtU3xlTMeqluqKKU4T7XqPaYLeeXSfy1QFtoNzN/KRKjg6DDtKy4DQe+ZI +u4I0YFSFPPA+3jtg0N1yA6EvKvKQjI/zh0KjDdH1zJ8VjOs54w3/qHbhp5LpsSZT +oWh5NW0S4fdvmvp6sNuia1C9yBFMEADtIwliuG3RzDlofI5TSNfMF+/H+C7EA4lu +6AKux+sPu9GcsBpnqGNBDFVhqGzO3VFNyQsUpgffM42CQVHdz5X+w25OACLMkXuU +vZWZAoIBAQC7i3YueBkEg/7yP/4x9cPQE4hP5C2o3Nj3R4QOQiGakNUB5e5aq7fZ +cDrkvL/04jWU7/DcHoulvecm40jNXzYiePeiwADUi4FVKCCl+HtB/EpZVAfI/pZv +oDO7XtBCWmjlRGXENg3WceLtEK0oABtF9ZSzK8gymnFSV4IUqK+bu7X/VlawjRV/ +71oDSMW0VUVrfl8eJ04DdEzX4wBJoWphOGEBoRo6ooNXHC0EAYfeWK9jkDqC+u6h +beq4ZdoovF/HZSVMw6vjMGLMPoMwBjSUtiAwGtwGpmTOWkkVKVsMVFd4kHUXIc3d +UOHyPTqnXNFx31y2kQkDAi4OsDZdFwdo +-----END PRIVATE KEY-----