internal/system/discovery.go (620 lines of code) (raw):

/* Copyright 2022 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 https://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. */ // Package system contains types and functions needed to perform SAP System discovery operations. package system import ( "bytes" "context" "encoding/json" "fmt" "io/ioutil" "strings" "sync" "time" "github.com/cenkalti/backoff/v4" "cloud.google.com/go/logging" "golang.org/x/exp/slices" "google.golang.org/protobuf/encoding/protojson" "github.com/GoogleCloudPlatform/sapagent/internal/configuration" "github.com/GoogleCloudPlatform/sapagent/internal/system/appsdiscovery" "github.com/GoogleCloudPlatform/sapagent/internal/system/clouddiscovery" "github.com/GoogleCloudPlatform/sapagent/internal/usagemetrics" "github.com/GoogleCloudPlatform/sapagent/internal/workloadmanager" "github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/log" "github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/recovery" timestamppb "google.golang.org/protobuf/types/known/timestamppb" cpb "github.com/GoogleCloudPlatform/sapagent/protos/configuration" ipb "github.com/GoogleCloudPlatform/sapagent/protos/instanceinfo" sappb "github.com/GoogleCloudPlatform/sapagent/protos/sapapp" dwpb "github.com/GoogleCloudPlatform/workloadagentplatform/sharedprotos/datawarehouse" spb "github.com/GoogleCloudPlatform/workloadagentplatform/sharedprotos/system" ) const systemDiscoveryOverride = "/etc/google-cloud-sap-agent/system.json" // SapSystemDiscoveryInterface is an interface for retrieving the list of SAP systems. type SapSystemDiscoveryInterface interface { GetSAPSystems() []*spb.SapDiscovery } // SapAppDiscoveryInterface is an interface for retrieving the list of SAP instances. type SapAppDiscoveryInterface interface { GetSAPInstances() *sappb.SAPInstances } // Discovery is a type used to perform SAP System discovery operations. type Discovery struct { WlmService WlmInterface CloudLogInterface CloudLogInterface CloudDiscoveryInterface CloudDiscoveryInterface HostDiscoveryInterface HostDiscoveryInterface SapDiscoveryInterface SapDiscoveryInterface AppsDiscovery func(context.Context, SapSystemDiscoveryInterface) *sappb.SAPInstances OSStatReader workloadmanager.OSStatReader FileReader workloadmanager.ConfigFileReader systems []*spb.SapDiscovery systemMu sync.Mutex sapInstances *sappb.SAPInstances sapMu sync.Mutex sapInstancesRoutine *recovery.RecoverableRoutine systemDiscoveryRoutine *recovery.RecoverableRoutine } // GetSAPSystems returns the current list of SAP Systems discovered on the current host. func (d *Discovery) GetSAPSystems() []*spb.SapDiscovery { d.systemMu.Lock() defer d.systemMu.Unlock() return d.systems } // GetSAPInstances returns the current list of SAP Instances discovered on the current host. func (d *Discovery) GetSAPInstances() *sappb.SAPInstances { d.sapMu.Lock() defer d.sapMu.Unlock() return d.sapInstances } // StartSAPSystemDiscovery Initializes the discovery object and starts the discovery subroutine. // Returns true if the discovery goroutine is started, and false otherwise. func StartSAPSystemDiscovery(ctx context.Context, config *cpb.Configuration, d *Discovery) bool { d.sapInstancesRoutine = &recovery.RecoverableRoutine{ Routine: updateSAPInstances, RoutineArg: updateSapInstancesArgs{config, d}, ErrorCode: usagemetrics.DiscoverSapInstanceFailure, UsageLogger: *usagemetrics.Logger, ExpectedMinDuration: 5 * time.Second, } d.sapInstancesRoutine.StartRoutine(ctx) // Ensure SAP instances is populated before starting system discovery backoff.Retry(func() error { if d.GetSAPInstances() != nil { return nil } return fmt.Errorf("SAP Instances not ready yet") }, backoff.WithMaxRetries(backoff.NewConstantBackOff(time.Second), 120)) d.systemDiscoveryRoutine = &recovery.RecoverableRoutine{ Routine: runDiscovery, RoutineArg: runDiscoveryArgs{config, d}, ErrorCode: usagemetrics.DiscoverSapSystemFailure, UsageLogger: *usagemetrics.Logger, ExpectedMinDuration: 10 * time.Second, } d.systemDiscoveryRoutine.StartRoutine(ctx) // Ensure systems are populated before returning backoff.Retry(func() error { if d.GetSAPSystems() != nil { return nil } return fmt.Errorf("SAP Systems not ready yet") }, backoff.WithMaxRetries(backoff.NewConstantBackOff(time.Second), 120)) return true } // CloudLogInterface is exported to be used by the system discovery OTE. type CloudLogInterface interface { Log(e logging.Entry) Flush() error } // WlmInterface is exported to be used by the system discovery OTE. type WlmInterface interface { WriteInsight(project, location string, writeInsightRequest *dwpb.WriteInsightRequest) error } // CloudDiscoveryInterface is exported to be used by the system discovery OTE. type CloudDiscoveryInterface interface { DiscoverComputeResources(context.Context, *spb.SapDiscovery_Resource, string, []string, *ipb.CloudProperties) []*spb.SapDiscovery_Resource } // HostDiscoveryInterface is exported to be used by the system discovery OTE. type HostDiscoveryInterface interface { DiscoverCurrentHost(context.Context) []string } // SapDiscoveryInterface is exported to be used by the system discovery OTE. type SapDiscoveryInterface interface { DiscoverSAPApps(ctx context.Context, sapApps *sappb.SAPInstances, conf *cpb.DiscoveryConfiguration) []appsdiscovery.SapSystemDetails } type loadBalancerGroup struct { lbRes *spb.SapDiscovery_Resource instanceURIs []string } func removeDuplicates(res []*spb.SapDiscovery_Resource) []*spb.SapDiscovery_Resource { var out []*spb.SapDiscovery_Resource uris := make(map[string]*spb.SapDiscovery_Resource) for _, r := range res { outRes, ok := uris[r.ResourceUri] if !ok { uris[r.ResourceUri] = r out = append(out, r) } else { for _, rel := range r.RelatedResources { if !slices.Contains(outRes.RelatedResources, rel) { outRes.RelatedResources = append(outRes.RelatedResources, rel) } } if r.GetInstanceProperties() != nil { log.Logger.Debugw("Stored instance properties", "properties", outRes.GetInstanceProperties().String()) log.Logger.Debugw("Duplicate instance properties", "properties", r.InstanceProperties.String()) if outRes.InstanceProperties == nil { outRes.InstanceProperties = r.InstanceProperties } else { outRes.InstanceProperties.InstanceRole |= r.InstanceProperties.InstanceRole if r.InstanceProperties.GetVirtualHostname() != "" { outRes.InstanceProperties.VirtualHostname = r.InstanceProperties.VirtualHostname } apps := make(map[string]string, len(outRes.InstanceProperties.AppInstances)) for _, app := range outRes.InstanceProperties.AppInstances { apps[app.Name] = app.Number log.Logger.Debugw("App instance", "app", app.String()) } for _, app := range r.InstanceProperties.AppInstances { if _, o := apps[app.Name]; !o { outRes.InstanceProperties.AppInstances = append(outRes.InstanceProperties.AppInstances, app) apps[app.Name] = app.Number log.Logger.Debugw("Adding app instance", "app", app.String()) } else { log.Logger.Debugw("Duplicate app instance", "app", app.String()) } } } log.Logger.Debugw("Merged properties", "properties", outRes.InstanceProperties.String()) } } } return out } type updateSapInstancesArgs struct { config *cpb.Configuration d *Discovery } type runDiscoveryArgs struct { config *cpb.Configuration d *Discovery } func updateSAPInstances(ctx context.Context, a any) { var args updateSapInstancesArgs var ok bool if args, ok = a.(updateSapInstancesArgs); !ok { log.CtxLogger(ctx).Warn("args is not of type updateSapInstancesArgs") return } if fileInfo, err := args.d.OSStatReader(systemDiscoveryOverride); fileInfo != nil && err == nil { args.d.sapInstances = &sappb.SAPInstances{} return } log.CtxLogger(ctx).Info("Starting SAP Instances update") updateTicker := time.NewTicker(args.config.GetDiscoveryConfiguration().GetSapInstancesUpdateFrequency().AsDuration()) for { log.CtxLogger(ctx).Info("Updating SAP Instances") sapInst := args.d.AppsDiscovery(ctx, args.d) args.d.sapMu.Lock() args.d.sapInstances = sapInst args.d.sapMu.Unlock() select { case <-ctx.Done(): log.CtxLogger(ctx).Info("SAP Discovery cancellation requested") return case <-updateTicker.C: continue } } } func runDiscovery(ctx context.Context, a any) { log.CtxLogger(ctx).Info("Starting SAP System Discovery") var args runDiscoveryArgs var ok bool if args, ok = a.(runDiscoveryArgs); !ok { log.CtxLogger(ctx).Warn("args is not of type runDiscoveryArgs") return } cp := args.config.GetCloudProperties() if cp == nil { log.CtxLogger(ctx).Warn("No Metadata Cloud Properties found, cannot collect resource information from the Compute API") return } updateTicker := time.NewTicker(args.config.GetDiscoveryConfiguration().GetSystemDiscoveryUpdateFrequency().AsDuration()) for { sapSystems := args.d.discoverSAPSystems(ctx, cp, args.config) log.CtxLogger(ctx).Debugw("Discovered SAP Systems", "systems", sapSystems) locationParts := strings.Split(cp.GetZone(), "-") region := strings.Join([]string{locationParts[0], locationParts[1]}, "-") // Write SAP system discovery data only if sap_system_discovery is enabled. if args.config.GetDiscoveryConfiguration().GetEnableDiscovery().GetValue() { log.CtxLogger(ctx).Info("Sending systems to WLM API") for _, sys := range sapSystems { sys.ProjectNumber = cp.GetNumericProjectId() sys.UpdateTime = timestamppb.Now() // Remove fields only used for local discovery: // resource.instanceProperties.diskDeviceNames var comps []*spb.SapDiscovery_Component if sys.ApplicationLayer != nil { comps = append(comps, sys.ApplicationLayer) } if sys.DatabaseLayer != nil { comps = append(comps, sys.DatabaseLayer) } for len(comps) > 0 { comp := comps[len(comps)-1] comps = comps[:len(comps)-1] if comp.ReplicationSites != nil { for _, site := range comp.ReplicationSites { comps = append(comps, site.Component) } } for _, res := range comp.GetResources() { if res.GetInstanceProperties() != nil { res.InstanceProperties.DiskDeviceNames = nil } } } log.CtxLogger(ctx).Debugw("System to send to WLM", "system", sys) // Send System to DW API insightRequest := &dwpb.WriteInsightRequest{ Insight: &dwpb.Insight{ SapDiscovery: sys, InstanceId: cp.GetInstanceId(), }, } insightRequest.AgentVersion = configuration.AgentVersion err := args.d.WlmService.WriteInsight(cp.ProjectId, region, insightRequest) if err != nil { log.CtxLogger(ctx).Infow("Encountered error writing to WLM", "error", err) } if args.d.CloudLogInterface == nil { continue } err = args.d.writeToCloudLogging(sys) if err != nil { log.CtxLogger(ctx).Infow("Encountered error writing to cloud logging", "error", err) } } } log.CtxLogger(ctx).Info("Done SAP System Discovery") args.d.systemMu.Lock() args.d.systems = sapSystems args.d.systemMu.Unlock() select { case <-ctx.Done(): log.CtxLogger(ctx).Info("SAP Discovery cancellation requested") return case <-updateTicker.C: continue } } } func (d *Discovery) discoverOverrideSystem(ctx context.Context, overrideFile string, instanceResource *spb.SapDiscovery_Resource) []*spb.SapDiscovery { file, err := d.FileReader(overrideFile) if err != nil { log.CtxLogger(ctx).Warnf("Failed to open override file: %v", err) return nil } defer file.Close() var fileBytes []byte if fileBytes, err = ioutil.ReadAll(file); err != nil { log.CtxLogger(ctx).Warnf("Failed to read override file: %v", err) return nil } log.CtxLogger(ctx).Debugf("File bytes: %s", fileBytes) var system spb.SapDiscovery if err := protojson.Unmarshal(fileBytes, &system); err != nil { log.CtxLogger(ctx).Warnf("Failed to decode override file: %v", err) return nil } // Make the instances without URI refer to this one. for _, r := range system.GetApplicationLayer().GetResources() { if r.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE && r.GetResourceUri() == "" { r.ResourceUri = instanceResource.GetResourceUri() r.InstanceProperties = instanceResource.InstanceProperties } } for _, r := range system.GetDatabaseLayer().GetResources() { if r.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE && r.GetResourceUri() == "" { r.ResourceUri = instanceResource.GetResourceUri() r.InstanceProperties = instanceResource.InstanceProperties } } return []*spb.SapDiscovery{&system} } func (d *Discovery) discoverSAPSystems(ctx context.Context, cp *ipb.CloudProperties, config *cpb.Configuration) []*spb.SapDiscovery { instanceURI := fmt.Sprintf("projects/%s/zones/%s/instances/%s", cp.GetProjectId(), cp.GetZone(), cp.GetInstanceName()) log.CtxLogger(ctx).Debug("Discovering current host") hostInstanceResources := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, nil, "", []string{instanceURI}, cp) log.CtxLogger(ctx).Debugw("Host Resources", "hostResources", hostInstanceResources) var instanceResource *spb.SapDiscovery_Resource var instanceNetwork string // Find the instance resource and its subnetwork for _, r := range hostInstanceResources { if r.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE && strings.Contains(r.ResourceUri, cp.GetInstanceName()) { log.CtxLogger(ctx).Debugf("Instance Resource: %v", r) instanceResource = r } if r.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_NETWORK { instanceNetwork = r.ResourceUri } if instanceResource != nil && instanceNetwork != "" { break } } if instanceResource == nil { log.CtxLogger(ctx).Debug("No instance resource found") } if fileInfo, err := d.OSStatReader(systemDiscoveryOverride); fileInfo != nil && err == nil { log.CtxLogger(ctx).Info("Discovering system from override file") return d.discoverOverrideSystem(ctx, systemDiscoveryOverride, instanceResource) } log.CtxLogger(ctx).Info("Starting host discovery") hostResourceNames := d.HostDiscoveryInterface.DiscoverCurrentHost(ctx) log.CtxLogger(ctx).Debugw("Host Resource Names", "names", hostResourceNames) log.CtxLogger(ctx).Infow("Discovering other host resources") hostResources := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceNetwork, hostResourceNames, cp) hostResources = removeDuplicates(append(hostResources, hostInstanceResources...)) log.CtxLogger(ctx).Debugw("Host Resources", "hostResources", hostResources) sapSystems := []*spb.SapDiscovery{} log.CtxLogger(ctx).Info("Starting SAP Discovery") sapDetails := d.SapDiscoveryInterface.DiscoverSAPApps(ctx, d.GetSAPInstances(), config.GetDiscoveryConfiguration()) log.CtxLogger(ctx).Debugw("SAP Details", "details", sapDetails) for _, s := range sapDetails { system := &spb.SapDiscovery{} if s.AppComponent != nil { log.CtxLogger(ctx).Info("Discovering cloud resources for app") appRes := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceNetwork, s.AppHosts, cp) log.CtxLogger(ctx).Debugf("App Resources: %v", appRes) if s.AppOnHost { appRes = append(appRes, hostResources...) log.CtxLogger(ctx).Debugf("App On Host Resources: %v", appRes) } if s.AppComponent.GetApplicationProperties().GetNfsUri() != "" { log.CtxLogger(ctx).Info("Discovering cloud resources for app NFS") nfsRes := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceNetwork, []string{s.AppComponent.GetApplicationProperties().GetNfsUri()}, cp) if len(nfsRes) > 0 { appRes = append(appRes, nfsRes...) s.AppComponent.GetApplicationProperties().NfsUri = nfsRes[0].GetResourceUri() } } if s.AppComponent.GetApplicationProperties().GetAscsUri() != "" { log.CtxLogger(ctx).Info("Discovering cloud resources for app ASCS") ascsRes := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceNetwork, []string{s.AppComponent.GetApplicationProperties().GetAscsUri()}, cp) if len(ascsRes) > 0 { log.CtxLogger(ctx).Debugw("ASCS Resources", "res", ascsRes) appRes = append(appRes, ascsRes...) s.AppComponent.GetApplicationProperties().AscsUri = ascsRes[0].GetResourceUri() } } if len(s.AppComponent.GetHaHosts()) > 0 { haRes := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceNetwork, s.AppComponent.GetHaHosts(), cp) // Find the instances var haURIs []string for _, res := range haRes { if res.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE { haURIs = append(haURIs, res.GetResourceUri()) } } appRes = append(appRes, haRes...) s.AppComponent.HaHosts = haURIs } s.AppComponent.HostProject = cp.GetNumericProjectId() s.AppComponent.Resources = removeDuplicates(appRes) system.ApplicationLayer = s.AppComponent } if s.DBComponent != nil { log.CtxLogger(ctx).Info("Discovering cloud resources for database") dbRes := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceNetwork, s.DBHosts, cp) log.CtxLogger(ctx).Debugw("Database Resources", "res", dbRes) if s.DBOnHost { dbRes = removeDuplicates(append(dbRes, hostResources...)) } if s.DBDiskMap != nil { log.CtxLogger(ctx).Debugw("DB Disk Map", "map", s.DBDiskMap) for mountPath, deviceNames := range s.DBDiskMap { for _, deviceName := range deviceNames { // instanceResource instance properties disk mounts is a list of the attached disks with // where name is the disk's source and mountPoint is the device name. mount := &spb.SapDiscovery_Resource_InstanceProperties_DiskMount{ MountPoint: mountPath, } for _, disk := range instanceResource.InstanceProperties.DiskDeviceNames { if strings.Contains(deviceName, disk.GetDeviceName()) { log.CtxLogger(ctx).Debugw("Disk for device name", "disk", disk, "mountPath", mountPath, "deviceName", deviceName) mount.DiskNames = append(mount.DiskNames, disk.GetSource()) } } if len(mount.DiskNames) > 0 { instanceResource.InstanceProperties.DiskMounts = append(instanceResource.InstanceProperties.DiskMounts, mount) } } } } // Make a resource map for quicker lookup resourcesByType := make(map[spb.SapDiscovery_Resource_ResourceKind][]*spb.SapDiscovery_Resource) resourceByURI := make(map[string]*spb.SapDiscovery_Resource) for _, r := range dbRes { resourcesByType[r.GetResourceKind()] = append(resourcesByType[r.GetResourceKind()], r) resourceByURI[r.GetResourceUri()] = r } log.CtxLogger(ctx).Debugw("Resources by type", "resources", resourcesByType) log.CtxLogger(ctx).Debugw("Resources by URI", "resources", resourceByURI) // Find the load balancer groups var lbGroups []loadBalancerGroup for _, lb := range resourcesByType[spb.SapDiscovery_Resource_RESOURCE_KIND_BACKEND_SERVICE] { log.CtxLogger(ctx).Debugw("LB", "lb", lb) lbGroup := loadBalancerGroup{lbRes: lb, instanceURIs: []string{}} for _, related := range lb.RelatedResources { rr := resourceByURI[related] if rr.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE { log.CtxLogger(ctx).Debugw("LB Instance", "instance", rr) lbGroup.instanceURIs = append(lbGroup.instanceURIs, related) } else if rr.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE_GROUP { log.CtxLogger(ctx).Debugw("LB Instance Group", "instanceGroup", rr) for _, igr := range rr.RelatedResources { igres := resourceByURI[igr] if igres.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE { log.CtxLogger(ctx).Debugw("LB Instance in Group", "instance", igres) lbGroup.instanceURIs = append(lbGroup.instanceURIs, igr) } } } } log.CtxLogger(ctx).Debugw("LB Group", "group", lbGroup) lbGroups = append(lbGroups, lbGroup) } log.CtxLogger(ctx).Debugw("LB Groups", "groups", lbGroups) if s.DBComponent.GetDatabaseProperties().GetSharedNfsUri() != "" { log.CtxLogger(ctx).Debug("Discovering cloud resources for database NFS") nfsRes := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceNetwork, []string{s.DBComponent.GetDatabaseProperties().GetSharedNfsUri()}, cp) if len(nfsRes) > 0 { dbRes = append(dbRes, nfsRes...) s.DBComponent.GetDatabaseProperties().SharedNfsUri = nfsRes[0].GetResourceUri() } } s.DBComponent.Resources = removeDuplicates(dbRes) if s.DBInstance != nil && s.DBInstance.HanaReplicationTree != nil { log.CtxLogger(ctx).Debug("Discovering cloud resources for database replication sites") primarySite := d.discoverReplicationSite(ctx, s.DBInstance.HanaReplicationTree, s.DBComponent.GetSid(), instanceResource, instanceNetwork, lbGroups, cp) primarySite.Component.Properties = s.DBComponent.Properties s.DBComponent = primarySite.Component // Find the site this instance belongs to. addHostResourcesforSite(ctx, s.DBComponent, dbRes, cp) } for _, r := range s.DBComponent.GetResources() { if r.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE { if r.InstanceProperties == nil { r.InstanceProperties = &spb.SapDiscovery_Resource_InstanceProperties{} } r.InstanceProperties.InstanceRole |= spb.SapDiscovery_Resource_InstanceProperties_INSTANCE_ROLE_DATABASE } } log.CtxLogger(ctx).Debug("Done discovering DB") s.DBComponent.HostProject = cp.GetNumericProjectId() system.DatabaseLayer = s.DBComponent } if len(s.InstanceProperties) > 0 { for _, iProp := range s.InstanceProperties { log.CtxLogger(ctx).Debugw("Discovering instance properties", "props", iProp) res := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceNetwork, []string{iProp.VirtualHostname}, cp) log.CtxLogger(ctx).Debugf("Discovered instance properties: %s", res) if res == nil { continue } for _, r := range res { if r.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE { if r.InstanceProperties == nil { r.InstanceProperties = &spb.SapDiscovery_Resource_InstanceProperties{} } if iProp.VirtualHostname != "" { r.InstanceProperties.VirtualHostname = iProp.VirtualHostname } r.InstanceProperties.InstanceRole |= iProp.InstanceRole appInstances := make(map[string]*spb.SapDiscovery_Resource_InstanceProperties_AppInstance) for _, app := range r.InstanceProperties.AppInstances { appInstances[app.GetName()] = app } for _, app := range iProp.AppInstances { if _, ok := appInstances[app.GetName()]; !ok { r.InstanceProperties.AppInstances = append(r.InstanceProperties.AppInstances, app) appInstances[app.GetName()] = app } } log.CtxLogger(ctx).Debugw("Adding instance properties to resource", "resource_uri", r.ResourceUri, "instance_properties", r.InstanceProperties) } } if (iProp.InstanceRole & spb.SapDiscovery_Resource_InstanceProperties_INSTANCE_ROLE_DATABASE) != 0 { log.CtxLogger(ctx).Debug("Instance properties are for a database instance") if system.GetDatabaseLayer() != nil { system.DatabaseLayer.Resources = removeDuplicates(append(system.GetDatabaseLayer().Resources, res...)) } } if (iProp.InstanceRole & spb.SapDiscovery_Resource_InstanceProperties_INSTANCE_ROLE_ASCS_ERS_APP_SERVER) != 0 { log.CtxLogger(ctx).Debug("Instance properties are for a application instance") if system.GetApplicationLayer() != nil { system.ApplicationLayer.Resources = removeDuplicates(append(system.GetApplicationLayer().Resources, res...)) } } } } system.WorkloadProperties = s.WorkloadProperties system.ProjectNumber = cp.GetNumericProjectId() system.UpdateTime = timestamppb.Now() sapSystems = append(sapSystems, system) } log.CtxLogger(ctx).Debug("Done discovering systems") return sapSystems } func (d *Discovery) discoverReplicationSite(ctx context.Context, site *sappb.HANAReplicaSite, sid string, instanceResource *spb.SapDiscovery_Resource, instanceSubnetwork string, lbGroups []loadBalancerGroup, cp *ipb.CloudProperties) *spb.SapDiscovery_Component_ReplicationSite { siteRes := d.CloudDiscoveryInterface.DiscoverComputeResources(ctx, instanceResource, instanceSubnetwork, []string{site.Name}, cp) log.CtxLogger(ctx).Debugw("Site Resources", "site", site.Name, "resources", siteRes) repComp := &spb.SapDiscovery_Component{ Resources: removeDuplicates(siteRes), Sid: sid, } // Find the region of the instance that the site name corresponds to. for _, r := range repComp.Resources { if r.GetResourceKind() == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE { if r.InstanceProperties == nil { r.InstanceProperties = &spb.SapDiscovery_Resource_InstanceProperties{} } r.InstanceProperties.InstanceRole |= spb.SapDiscovery_Resource_InstanceProperties_INSTANCE_ROLE_DATABASE if r.GetInstanceProperties().GetVirtualHostname() == site.Name { instanceZone := clouddiscovery.ExtractFromURI(r.GetResourceUri(), "zones") regionParts := strings.Split(instanceZone, "-") instanceRegion := strings.Join([]string{regionParts[0], regionParts[1]}, "-") repComp.Region = instanceRegion } } } // Check if this site is part of a known load balancer group var lbGroup *loadBalancerGroup resLoop: for _, r := range siteRes { if r.ResourceKind != spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE { continue } for _, lb := range lbGroups { if slices.Contains(lb.instanceURIs, r.GetResourceUri()) { log.CtxLogger(ctx).Debugw("Site is part of a load balancer group", "site", site.Name, "lbGroup", lb.lbRes.GetResourceUri()) lbGroup = &lb break resLoop } } } if lbGroup == nil { log.CtxLogger(ctx).Debugw("Site not part of existing LB group", "site", site.Name) } for _, childSite := range site.Targets { childRepSite := d.discoverReplicationSite(ctx, childSite, sid, instanceResource, instanceSubnetwork, lbGroups, cp) inLbGroup := false if lbGroup != nil { // Check if the child site is part of the same load balancer group for _, r := range childRepSite.Component.Resources { if r.ResourceKind != spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE { continue } if slices.Contains(lbGroup.instanceURIs, r.GetResourceUri()) { log.CtxLogger(ctx).Debugw("Child site is part of the same LB group", "site", childSite.Name, "lbGroup", lbGroup.lbRes.GetResourceUri()) inLbGroup = true // Child is part of the same load balancer group, therefore belongs in the same component. repComp.Resources = removeDuplicates(append(repComp.Resources, childRepSite.Component.Resources...)) // Add the child site's replication sites to this component. repComp.ReplicationSites = append(repComp.ReplicationSites, childRepSite.Component.ReplicationSites...) repComp.HaHosts = append(repComp.HaHosts, lbGroup.instanceURIs...) break } } } if !inLbGroup { log.CtxLogger(ctx).Debugw("Child site not part of the same LB group", "site", childSite.Name) // Child is not part of the same load balancer group, therefore belongs in a separate component. childRepSite.SourceSite = site.Name repComp.ReplicationSites = append(repComp.ReplicationSites, childRepSite) } } log.CtxLogger(ctx).Debugw("Site", "site", site.Name, "component", repComp) return &spb.SapDiscovery_Component_ReplicationSite{ Component: repComp, } } func (d *Discovery) writeToCloudLogging(sys *spb.SapDiscovery) error { s, err := protojson.Marshal(sys) if err != nil { return err } var buf bytes.Buffer json.Indent(&buf, s, "", " ") payload := make(map[string]string) payload["type"] = "SapDiscovery" payload["discovery"] = buf.String() d.CloudLogInterface.Log(logging.Entry{ Timestamp: time.Now(), Severity: logging.Info, Payload: payload, }) return nil } func addHostResourcesforSite(ctx context.Context, site *spb.SapDiscovery_Component, hostResources []*spb.SapDiscovery_Resource, cp *ipb.CloudProperties) { for _, r := range site.Resources { instanceName := clouddiscovery.ExtractFromURI(r.GetResourceUri(), "instances") if instanceName == cp.GetInstanceName() { // Add the host resources to this site. log.CtxLogger(ctx).Debugw("Adding host resources to site", "resources", hostResources) site.Resources = removeDuplicates(append(site.Resources, hostResources...)) return } } // Host is not in the site, check children for _, rep := range site.ReplicationSites { addHostResourcesforSite(ctx, rep.Component, hostResources, cp) } }