cmd/core_plugin/diagnostics/diagnostics_windows.go (138 lines of code) (raw):

// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build windows // Package diagnostics implements the diagnostics tooling module. package diagnostics import ( "context" "encoding/json" "errors" "fmt" "slices" "sync/atomic" "github.com/GoogleCloudPlatform/galog" "github.com/GoogleCloudPlatform/google-guest-agent/cmd/core_plugin/manager" "github.com/GoogleCloudPlatform/google-guest-agent/internal/cfg" "github.com/GoogleCloudPlatform/google-guest-agent/internal/events" "github.com/GoogleCloudPlatform/google-guest-agent/internal/metadata" "github.com/GoogleCloudPlatform/google-guest-agent/internal/reg" "github.com/GoogleCloudPlatform/google-guest-agent/internal/run" "github.com/GoogleCloudPlatform/google-guest-agent/internal/utils/ssh" "golang.org/x/sys/windows/registry" ) const ( // diagnosticsCmd is the path to the diagnostics executable, the program used // to collect diagnostics metrics data. diagnosticsCmd = `C:\Program Files\Google\Compute Engine\diagnostics\diagnostics.exe` // diagnosticsRegKey is the registry key used to store the list of diagnostics // entries. diagnosticsRegKey = "Diagnostics" // diagnosticsModuleID is the ID of the diagnostics module. diagnosticsModuleID = "diagnostics" ) var ( // module is the diagnostics implementation instance. module = &diagnosticsModule{} ) type diagnosticsModule struct { // Indicate whether an existing job is running to collect logs. isDiagnosticsRunning atomic.Bool // prevMetadata is the previously seen metadata descriptor. prevMetadata *metadata.Descriptor } // NewModule returns the diagnostic module for late stage registration. func NewModule(context.Context) *manager.Module { return &manager.Module{ ID: diagnosticsModuleID, Setup: module.moduleSetup, Description: "Collects diagnostics data from the system and uploads it to the specified URL", } } // moduleSetup is the module's Setup callback. It registers a subscriber to // metadata's longpoll event. func (mod *diagnosticsModule) moduleSetup(ctx context.Context, data any) error { eManager := events.FetchManager() desc, ok := data.(*metadata.Descriptor) if !ok { return fmt.Errorf("diagnostics module expects a metadata descriptor in the data pointer") } // Do the initial first setup execution in the module initialization, it will // be handled by the metadata longpoll event handler/subscriber after the // first setup. if err := mod.handleDiagnosticsRequest(ctx, cfg.Retrieve(), desc); err != nil { galog.Errorf("Failed to handle diagnostics request on setup: %v", err) } sub := events.EventSubscriber{Name: diagnosticsModuleID, Callback: mod.metadataSubscriber} eManager.Subscribe(metadata.LongpollEvent, sub) return nil } // metadataSubscriber is the callback for the metadata event and handles the // diagnostics configuration changes or execution. func (mod *diagnosticsModule) metadataSubscriber(ctx context.Context, evType string, data any, evData *events.EventData) bool { desc, ok := evData.Data.(*metadata.Descriptor) // If the event manager is passing a non expected data type we log it and // don't renew the handler. if !ok { galog.Errorf("event's data is not a metadata descriptor: %+v", evData.Data) return false } // If the event manager is passing/reporting an error we log it and keep // renewing the handler. if evData.Error != nil { galog.Debugf("Metadata event watcher reported error: %s, skiping.", evData.Error) return true } if err := mod.handleDiagnosticsRequest(ctx, cfg.Retrieve(), desc); err != nil { galog.Errorf("Failed to handle diagnostics request: %v", err) } return true } // diagnosticsEntry is the structure of the diagnostics metadata entry. type diagnosticsEntry struct { // SignedURL is the URL to the signed URL to upload the logs to. SignedURL string // ExpireOn is the expiration time of the diagnostics request. ExpireOn string // Trace is the flag to enable tracing. Trace bool } // handleDiagnosticsRequest is the actual diagnostics configuration entry point. func (mod *diagnosticsModule) handleDiagnosticsRequest(ctx context.Context, config *cfg.Sections, desc *metadata.Descriptor) error { defer func() { mod.prevMetadata = desc }() // If there is an existing job running, reject the request. if mod.isDiagnosticsRunning.Load() { galog.Infof("Diagnostics: reject the request, as an existing process is collecting logs from the system") return nil } // Ignore/return if diagnostics configuration is disabled or the // metadata flags haven't changed. if !mod.diagnosticsEnabled(desc, config) || !mod.metadataChanged(desc) { return nil } galog.Infof("Diagnostics: logs export requested.") // Fetch from the registry the list of the existing/seen request entries. regEntries, err := reg.ReadMultiString(diagnosticsRegKey, diagnosticsRegKey) if err != nil && !errors.Is(err, registry.ErrNotExist) { return fmt.Errorf("failed to read diagnostics registry key: %v", err) } // Check if we've dealt with this entry already. metadataNewEntry := desc.Instance().Attributes().Diagnostics() if slices.Contains(regEntries, metadataNewEntry) { galog.Debugf("Diagnostics: request already seen %q, ignoring.", metadataNewEntry) return nil } // Unmarshall the new entry to extract the request details. var entry diagnosticsEntry if err := json.Unmarshal([]byte(metadataNewEntry), &entry); err != nil { return fmt.Errorf("failed to unmarshal diagnostics entry: %w", err) } expired, err := ssh.CheckExpired(entry.ExpireOn) if err != nil { return fmt.Errorf("failed to check diagnostics request expiration(%v): %w", entry, err) } // Has the request already expired or is it malformed (no signed URL)? if entry.SignedURL == "" || expired { return fmt.Errorf("diagnostics: request %v is malformed or expired, ignoring", metadataNewEntry) } cmd := []string{diagnosticsCmd, "-signedUrl", entry.SignedURL} if entry.Trace { cmd = append(cmd, "-trace") } // Set flag job is running only when it is about to start. mod.isDiagnosticsRunning.Store(true) go func() { galog.Infof("Diagnostics: collecting logs from the system.") // Job is done, unblock the upcoming requests. defer func() { mod.isDiagnosticsRunning.Swap(false) }() // Actually run the diagnostics command. opts := run.Options{Name: cmd[0], Args: cmd[1:], OutputType: run.OutputCombined} res, err := run.WithContext(ctx, opts) if err != nil { galog.Errorf("Error collecting logs: %v", err) return } galog.Infof(res.Output) }() regEntries = append(regEntries, metadataNewEntry) if err := reg.WriteMultiString(reg.GCEKeyBase, diagnosticsRegKey, regEntries); err != nil { return fmt.Errorf("failed to write diagnostics registry key: %v", err) } return nil } // metadataChanged returns true if the diagnostics metadata flags have changed. func (mod *diagnosticsModule) metadataChanged(desc *metadata.Descriptor) bool { return mod.prevMetadata == nil || desc.Instance().Attributes().Diagnostics() != mod.prevMetadata.Instance().Attributes().Diagnostics() } // diagnosticsEnabled returns true if the diagnostics feature is enabled. func (mod *diagnosticsModule) diagnosticsEnabled(desc *metadata.Descriptor, config *cfg.Sections) bool { // Diagnostics are opt-in and enabled by default. if config.Diagnostics != nil { return config.Diagnostics.Enable } if desc.Instance().Attributes().EnableDiagnostics() != nil { return *desc.Instance().Attributes().EnableDiagnostics() } if desc.Project().Attributes().EnableDiagnostics() != nil { return *desc.Project().Attributes().EnableDiagnostics() } return false }