internal/system/appsdiscovery/apps_discovery.go (1,455 lines of code) (raw):

/* Copyright 2023 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 appsdiscovery contains a set of functionality to discover SAP application details running on the current host, and their related components. package appsdiscovery import ( "context" "encoding/json" "errors" "fmt" "path/filepath" "regexp" "strconv" "strings" "golang.org/x/exp/slices" "google.golang.org/protobuf/encoding/prototext" "google.golang.org/protobuf/proto" "github.com/GoogleCloudPlatform/sapagent/internal/utils/filesystem" sappb "github.com/GoogleCloudPlatform/sapagent/protos/sapapp" "github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/commandlineexecutor" "github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/log" spb "github.com/GoogleCloudPlatform/workloadagentplatform/sharedprotos/system" cpb "github.com/GoogleCloudPlatform/sapagent/protos/configuration" ) var ( fsMountRegex = regexp.MustCompile(`([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):(/[a-zA-Z0-9]+)`) headerLineRegex = regexp.MustCompile(`[^-]+`) hanaVersionRegex = regexp.MustCompile(`version:\s+(([0-9]+\.?)+)`) netweaverKernelRegex = regexp.MustCompile(`kernel release\s+([0-9]+)`) netweaverPatchNumberRegex = regexp.MustCompile(`patch number\s+([0-9]+)`) sapDbHostRegex = regexp.MustCompile(`SAPDBHOST\s+=\s+(.*)`) hostnameRegex = regexp.MustCompile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`) landscapeIDRegex = regexp.MustCompile(`id\s+=\s+([a-zA-Z0-9\-]+)`) ) const ( haNodes = "HANodes:" r3transSuccessResult = "R3trans finished (0000)" r3transTmpFolder = "/tmp/r3trans/" tmpControlFilePath = r3transTmpFolder + "export_products.ctl" r3transOutputPath = r3transTmpFolder + "output.txt" profileDBIDNameKey = "dbid" profileDBMSNameKey = "dbms/name" profileDBMSTypeKey = "dbms/type" profileDBHostKey = "SAPDBHOST" profileJ2EEDBNameKey = "j2ee/dbname" profileDBSHDBNameKey = "dbs/hdb/dbname" maxDBName = "ada" db2DBName = "db2" db4DBName = "db4" db6DBName = "db6" hanaDBName = "hdb" sqlServerName = "mss" oracleName = "ora" sybaseASEName = "syb" dataPathName = "basepath_datavolumes" logPathName = "basepath_logvolumes" logBackupPathName = "basepath_logbackup" hanaConfigDir = "/usr/sap/%s/SYS/global/hdb/custom/config" ) type lsblkdevice struct { Name string Type string Mountpoints []string `json:"mountpoints"` Size json.RawMessage Children []lsblkdevice } type lsblk struct { BlockDevices []lsblkdevice `json:"blockdevices"` } type fileReader func(filename string) ([]byte, error) // SapDiscovery contains variables and methods to discover SAP applications running on the current host. type SapDiscovery struct { Execute commandlineexecutor.Execute FileSystem filesystem.FileSystem } // SapSystemDetails contains information about an ASP system running on the current host. type SapSystemDetails struct { AppComponent *spb.SapDiscovery_Component DBComponent *spb.SapDiscovery_Component AppHosts, DBHosts []string AppOnHost, DBOnHost bool DBDiskMap map[string][]string WorkloadProperties *spb.SapDiscovery_WorkloadProperties InstanceProperties []*spb.SapDiscovery_Resource_InstanceProperties AppInstance *sappb.SAPInstance DBInstance *sappb.SAPInstance } func removeDuplicates[T comparable](s []T) []T { m := make(map[string]bool) var o []T for _, s := range s { sStr := fmt.Sprintf("%v", s) if _, ok := m[sStr]; ok { continue } m[sStr] = true o = append(o, s) } return o } func mergeAppProperties(old, new *spb.SapDiscovery_Component_ApplicationProperties) *spb.SapDiscovery_Component_ApplicationProperties { log.Logger.Debugw("Merging app properties.", "old", prototext.Format(old), "new", prototext.Format(new)) if new == nil { return old } merged := proto.Clone(new).(*spb.SapDiscovery_Component_ApplicationProperties) if merged.GetApplicationType() == spb.SapDiscovery_Component_ApplicationProperties_APPLICATION_TYPE_UNSPECIFIED { log.Logger.Debugw("Merging app properties, using old type.", "old", prototext.Format(old), "new", prototext.Format(new)) merged.ApplicationType = old.ApplicationType } if merged.AscsUri == "" { log.Logger.Debugw("Merging app properties, using old ASCS URI.", "old", prototext.Format(old), "new", prototext.Format(new)) merged.AscsUri = old.AscsUri } if merged.NfsUri == "" { log.Logger.Debugw("Merging app properties, using old NFS URI.", "old", prototext.Format(old), "new", prototext.Format(new)) merged.NfsUri = old.NfsUri } if merged.KernelVersion == "" { log.Logger.Debugw("Merging app properties, using old kernel version.", "old", prototext.Format(old), "new", prototext.Format(new)) merged.KernelVersion = old.KernelVersion } if merged.AscsInstanceNumber == "" { log.Logger.Debugw("Merging app properties, using old ASCS instance number.", "old", prototext.Format(old), "new", prototext.Format(new)) merged.AscsInstanceNumber = old.AscsInstanceNumber } if merged.ErsInstanceNumber == "" { log.Logger.Debugw("Merging app properties, using old ERS instance number.", "old", prototext.Format(old), "new", prototext.Format(new)) merged.ErsInstanceNumber = old.ErsInstanceNumber } return merged } func mergeDBProperties(old, new *spb.SapDiscovery_Component_DatabaseProperties) *spb.SapDiscovery_Component_DatabaseProperties { if new == nil { return old } merged := proto.Clone(new).(*spb.SapDiscovery_Component_DatabaseProperties) if merged.GetDatabaseType() == spb.SapDiscovery_Component_DatabaseProperties_DATABASE_TYPE_UNSPECIFIED { merged.DatabaseType = old.DatabaseType } if merged.PrimaryInstanceUri == "" { merged.PrimaryInstanceUri = old.PrimaryInstanceUri } if merged.SharedNfsUri == "" { merged.SharedNfsUri = old.SharedNfsUri } if merged.DatabaseVersion == "" { merged.DatabaseVersion = old.DatabaseVersion } if merged.InstanceNumber == "" { merged.InstanceNumber = old.InstanceNumber } if merged.DatabaseSid == "" { merged.DatabaseSid = old.DatabaseSid } return merged } func mergeComponent(old, new *spb.SapDiscovery_Component) *spb.SapDiscovery_Component { if new == nil { return old } if old == nil { return new } merged := proto.Clone(new).(*spb.SapDiscovery_Component) if merged.GetProperties() == nil { merged.Properties = old.Properties } else if old.GetProperties() != nil { switch x := old.Properties.(type) { case *spb.SapDiscovery_Component_ApplicationProperties_: merged.Properties = &spb.SapDiscovery_Component_ApplicationProperties_{ ApplicationProperties: mergeAppProperties(x.ApplicationProperties, new.GetApplicationProperties()), } case *spb.SapDiscovery_Component_DatabaseProperties_: merged.Properties = &spb.SapDiscovery_Component_DatabaseProperties_{ DatabaseProperties: mergeDBProperties(x.DatabaseProperties, new.GetDatabaseProperties()), } } } if old.GetTopologyType() == spb.SapDiscovery_Component_TOPOLOGY_SCALE_OUT || new.GetTopologyType() == spb.SapDiscovery_Component_TOPOLOGY_SCALE_OUT { merged.TopologyType = spb.SapDiscovery_Component_TOPOLOGY_SCALE_OUT } if merged.GetTopologyType() == spb.SapDiscovery_Component_TOPOLOGY_TYPE_UNSPECIFIED { merged.TopologyType = old.GetTopologyType() } if merged.HostProject == "" { merged.HostProject = old.HostProject } if merged.Sid == "" { merged.Sid = old.Sid } merged.HaHosts = removeDuplicates(append(merged.GetHaHosts(), new.GetHaHosts()...)) merged.ReplicationSites = removeDuplicates(append(merged.GetReplicationSites(), new.GetReplicationSites()...)) return merged } func mergeWorkloadProperties(old, new *spb.SapDiscovery_WorkloadProperties) *spb.SapDiscovery_WorkloadProperties { if new == nil { return old } merged := new productMap := make(map[string]*spb.SapDiscovery_WorkloadProperties_ProductVersion) for _, prod := range new.GetProductVersions() { productMap[prod.GetName()] = prod } for _, prod := range old.GetProductVersions() { if _, ok := productMap[prod.GetName()]; !ok { new.ProductVersions = append(new.ProductVersions, prod) } } componentMap := make(map[string]*spb.SapDiscovery_WorkloadProperties_SoftwareComponentProperties) for _, comp := range new.GetSoftwareComponentVersions() { componentMap[comp.GetName()] = comp } for _, comp := range old.GetSoftwareComponentVersions() { if _, ok := componentMap[comp.GetName()]; !ok { new.SoftwareComponentVersions = append(new.SoftwareComponentVersions, comp) } } return merged } func mergeInstanceProperties(old, new []*spb.SapDiscovery_Resource_InstanceProperties) []*spb.SapDiscovery_Resource_InstanceProperties { if new == nil { return old } merged := new vHostNames := make(map[string]*spb.SapDiscovery_Resource_InstanceProperties) for _, iProp := range merged { vHostNames[iProp.GetVirtualHostname()] = iProp } for _, iProp := range old { if p, ok := vHostNames[iProp.GetVirtualHostname()]; ok { p.InstanceRole |= iProp.GetInstanceRole() var appNames []string for _, app := range p.GetAppInstances() { appNames = append(appNames, app.GetName()) } for _, app := range iProp.GetAppInstances() { if !slices.Contains(appNames, app.GetName()) { p.AppInstances = append(p.AppInstances, app) appNames = append(appNames, app.GetName()) } } } else { merged = append(merged, iProp) } } return merged } func mergeSystemDetails(old, new SapSystemDetails) SapSystemDetails { merged := new merged.AppOnHost = old.AppOnHost || new.AppOnHost merged.DBOnHost = old.DBOnHost || new.DBOnHost merged.AppComponent = mergeComponent(old.AppComponent, new.AppComponent) merged.DBComponent = mergeComponent(old.DBComponent, new.DBComponent) merged.AppHosts = removeDuplicates(append(old.AppHosts, new.AppHosts...)) merged.DBHosts = removeDuplicates(append(old.DBHosts, new.DBHosts...)) merged.WorkloadProperties = mergeWorkloadProperties(old.WorkloadProperties, new.WorkloadProperties) merged.InstanceProperties = mergeInstanceProperties(old.InstanceProperties, new.InstanceProperties) if new.AppInstance == nil { merged.AppInstance = old.AppInstance } if new.DBInstance == nil { merged.DBInstance = old.DBInstance } log.Logger.Debugf("Merged System Details. %s", merged) return merged } // hasExecutePermission checks if the given path has execute permission for the owner. func (d *SapDiscovery) hasExecutePermission(path string) bool { fileInfo, err := d.FileSystem.Stat(path) if err != nil { log.Logger.Debugw("Error getting directory info", "path", path, "error", err) return false } return fileInfo.Mode()&0100 != 0 // 0100 is the executable bit for the owner } // DiscoverSAPApps attempts to identify the different SAP Applications running on the current host. func (d *SapDiscovery) DiscoverSAPApps(ctx context.Context, sapApps *sappb.SAPInstances, conf *cpb.DiscoveryConfiguration) []SapSystemDetails { sapSystems := []SapSystemDetails{} if sapApps == nil { log.CtxLogger(ctx).Debugw("No SAP applications found") return sapSystems } if !d.hasExecutePermission("/usr/sap") { log.CtxLogger(ctx).Warnw("No execute permission for /usr/sap directory, some of the discovery operations will fail. Please ensure that the root user has execute permission for /usr/sap directory.") return sapSystems } log.CtxLogger(ctx).Debugw("SAP Apps found", "apps", sapApps) for _, app := range sapApps.Instances { switch app.Type { case sappb.InstanceType_NETWEAVER: log.CtxLogger(ctx).Infow("discovering netweaver", "sid", app.Sapsid) sys := d.discoverNetweaver(ctx, app, conf) log.CtxLogger(ctx).Debugf("Netweaver system: %s", sys) // See if a system with the same SID already exists found := false for i, s := range sapSystems { log.CtxLogger(ctx).Infow("Comparing to system", "dbSid", s.DBComponent.GetSid(), "appSID", s.AppComponent.GetSid()) if (s.AppComponent.GetSid() == "" || s.AppComponent.GetSid() == sys.AppComponent.GetSid()) && (s.DBComponent.GetSid() == "" || s.DBComponent.GetSid() == sys.DBComponent.GetSid()) { log.CtxLogger(ctx).Infow("Found existing system", "sid", sys.AppComponent.GetSid()) sapSystems[i] = mergeSystemDetails(s, sys) sapSystems[i].AppOnHost = true found = true break } } if !found { log.CtxLogger(ctx).Infow("No existing system", "sid", app.Sapsid) sys.AppOnHost = true sapSystems = append(sapSystems, sys) } case sappb.InstanceType_HANA: log.CtxLogger(ctx).Infow("discovering hana", "sid", app.Sapsid) for _, sys := range d.discoverHANA(ctx, app) { // See if a system with the same SID already exists found := false for i, s := range sapSystems { if s.DBComponent.GetSid() == sys.DBComponent.GetSid() { log.CtxLogger(ctx).Infow("Found existing system", "sid", sys.DBComponent.GetSid()) sapSystems[i] = mergeSystemDetails(s, sys) sapSystems[i].DBOnHost = true found = true break } } if !found { log.CtxLogger(ctx).Infow("No existing system", "sid", sys.DBComponent.GetSid()) sys.DBOnHost = true sapSystems = append(sapSystems, sys) } } } } return sapSystems } func (d *SapDiscovery) discoverNetweaver(ctx context.Context, app *sappb.SAPInstance, conf *cpb.DiscoveryConfiguration) SapSystemDetails { appProps := &spb.SapDiscovery_Component_ApplicationProperties{ ApplicationType: spb.SapDiscovery_Component_ApplicationProperties_NETWEAVER, } ascsHost, err := d.discoverASCS(ctx, app.Sapsid) if err != nil { log.CtxLogger(ctx).Infow("Encountered error during call to discoverASCS.", "error", err) } else { appProps.AscsUri = ascsHost } nfsHost, err := d.discoverAppNFS(ctx, app.Sapsid) if err != nil { log.CtxLogger(ctx).Infow("Encountered error during call to discoverAppNFS.", "error", err) } else { appProps.NfsUri = nfsHost } kernelVersion, err := d.discoverNetweaverKernelVersion(ctx, app.Sapsid) if err != nil { log.CtxLogger(ctx).Infow("Encountered error during call to discoverNetweaverKernelVersion.", "error", err) } else { appProps.KernelVersion = kernelVersion } ha, haNodes := d.discoverNetweaverHA(ctx, app) if !ha { haNodes = nil } ascsHosts, ersHosts, appHosts := d.discoverNetweaverHosts(ctx, app) log.CtxLogger(ctx).Debugw("ascsHosts", "ascsHosts", ascsHosts) log.CtxLogger(ctx).Debugw("ersHosts", "ersHosts", ersHosts) log.CtxLogger(ctx).Debugw("appHosts", "appHosts", appHosts) var iProps []*spb.SapDiscovery_Resource_InstanceProperties for _, a := range ascsHosts { iProps = append(iProps, &spb.SapDiscovery_Resource_InstanceProperties{ VirtualHostname: a.Name, InstanceRole: spb.SapDiscovery_Resource_InstanceProperties_INSTANCE_ROLE_ASCS, }) appProps.AscsInstanceNumber = a.Number } for _, e := range ersHosts { iProps = append(iProps, &spb.SapDiscovery_Resource_InstanceProperties{ VirtualHostname: e.Name, InstanceRole: spb.SapDiscovery_Resource_InstanceProperties_INSTANCE_ROLE_ERS, }) appProps.ErsInstanceNumber = e.Number } for _, a := range appHosts { iProps = append(iProps, &spb.SapDiscovery_Resource_InstanceProperties{ VirtualHostname: a.Name, InstanceRole: spb.SapDiscovery_Resource_InstanceProperties_INSTANCE_ROLE_APP_SERVER, AppInstances: []*spb.SapDiscovery_Resource_InstanceProperties_AppInstance{a}, }) } details := SapSystemDetails{ AppComponent: &spb.SapDiscovery_Component{ Sid: app.Sapsid, Properties: &spb.SapDiscovery_Component_ApplicationProperties_{ ApplicationProperties: appProps, }, HaHosts: haNodes, }, AppHosts: haNodes, InstanceProperties: iProps, AppInstance: app, } log.CtxLogger(ctx).Debugw("Checking config", "config", conf) var isABAP bool var wlProps *spb.SapDiscovery_WorkloadProperties if conf.GetEnableWorkloadDiscovery().GetValue() { isABAP, wlProps, err = d.discoverNetweaverABAP(ctx, app) if err != nil { log.CtxLogger(ctx).Infow("Encountered error during call to discoverNetweaverABAP.", "error", err) } } if isABAP { appProps.ApplicationType = spb.SapDiscovery_Component_ApplicationProperties_NETWEAVER_ABAP } else { isJava, javaProps, err := d.discoverNetweaverJava(ctx, app) if err != nil { log.CtxLogger(ctx).Infow("Encountered error during call to discoverNetweaverJava.", "error", err) } if isJava { wlProps = javaProps appProps.ApplicationType = spb.SapDiscovery_Component_ApplicationProperties_NETWEAVER_JAVA } } details.WorkloadProperties = wlProps dbSID, err := d.discoverDatabaseSID(ctx, app.Sapsid, isABAP) if err != nil { return details } details.DBComponent = &spb.SapDiscovery_Component{ Sid: dbSID, } dbHosts, err := d.discoverAppToDBConnection(ctx, app.Sapsid, isABAP) if err != nil { log.CtxLogger(ctx).Infow("Encountered error during call to discoverAppToDBConnection.", "error", err) } else { details.DBHosts = dbHosts } dbType, err := d.discoverDBType(ctx, app.Sapsid) if err != nil { log.CtxLogger(ctx).Infow("Encountered error during call to discoverDBType.", "error", err) return details } details.DBComponent.Properties = &spb.SapDiscovery_Component_DatabaseProperties_{ DatabaseProperties: &spb.SapDiscovery_Component_DatabaseProperties{ DatabaseType: dbType, }, } if dbType == spb.SapDiscovery_Component_DatabaseProperties_HANA { return details } // For non-HANA DBs, we just check for the SAPDBHOST in the DEFAULT.PFL file. dbhost, err := d.discoverDBHost(ctx, app.Sapsid) if err != nil { log.CtxLogger(ctx).Infow("Encountered error during call to discoverDBHost.", "error", err) return details } details.DBHosts = []string{dbhost} // For non-HANA DBs, we assume scale-up topology. if dbType != spb.SapDiscovery_Component_DatabaseProperties_HANA { details.DBComponent.TopologyType = spb.SapDiscovery_Component_TOPOLOGY_SCALE_UP } return details } func (d *SapDiscovery) discoverNetweaverHosts(ctx context.Context, app *sappb.SAPInstance) ([]*spb.SapDiscovery_Resource_InstanceProperties_AppInstance, []*spb.SapDiscovery_Resource_InstanceProperties_AppInstance, []*spb.SapDiscovery_Resource_InstanceProperties_AppInstance) { sidLower := strings.ToLower(app.Sapsid) sidAdm := fmt.Sprintf("%sadm", sidLower) cmd := commandlineexecutor.Params{ Executable: "sudo", Args: []string{"-i", "-u", sidAdm, "sapcontrol", "-nr", app.InstanceNumber, "-function", "GetSystemInstanceList"}, } res := d.Execute(ctx, cmd) if res.Error != nil { return nil, nil, nil } log.CtxLogger(ctx).Debugw("GetSystemInstanceList", "stdout", res.StdOut) var ascsHosts, ersHosts, appHosts []*spb.SapDiscovery_Resource_InstanceProperties_AppInstance lines := strings.Split(res.StdOut, "\n") for _, line := range lines { parts := strings.Split(line, ",") if len(parts) < 6 { continue } name := strings.TrimSpace(parts[0]) if name == "hostname" { continue } n, err := strconv.Atoi(strings.TrimSpace(parts[1])) if err != nil { log.CtxLogger(ctx).Debugw("Failed to parse instance number", "name", name, "number", parts[1], "err", err) continue } instanceNumber := fmt.Sprintf("%02d", n) features := strings.TrimSpace(parts[5]) log.CtxLogger(ctx).Debugw("features", "name", name, "features", features) inst := &spb.SapDiscovery_Resource_InstanceProperties_AppInstance{ Name: name, Number: instanceNumber, } switch { case strings.Contains(features, "MESSAGESERVER"): ascsHosts = append(ascsHosts, inst) case strings.Contains(features, "ENQREP"): ersHosts = append(ersHosts, inst) case strings.Contains(features, "ABAP"): appHosts = append(appHosts, inst) } } return ascsHosts, ersHosts, appHosts } func hanaSystemDetails(app *sappb.SAPInstance, dbProps *spb.SapDiscovery_Component_DatabaseProperties, dbHosts []string, sid, dbProductVersion string, diskMap map[string][]string) SapSystemDetails { t := spb.SapDiscovery_Component_TOPOLOGY_SCALE_UP if len(dbHosts) > 1 { t = spb.SapDiscovery_Component_TOPOLOGY_SCALE_OUT } return SapSystemDetails{ DBComponent: &spb.SapDiscovery_Component{ Sid: sid, Properties: &spb.SapDiscovery_Component_DatabaseProperties_{ DatabaseProperties: dbProps, }, HaHosts: app.HanaHaMembers, TopologyType: t, }, DBHosts: dbHosts, WorkloadProperties: &spb.SapDiscovery_WorkloadProperties{ ProductVersions: []*spb.SapDiscovery_WorkloadProperties_ProductVersion{{ Name: "SAP HANA", Version: dbProductVersion, }}, }, DBInstance: app, DBDiskMap: diskMap, } } func (d *SapDiscovery) discoverHANA(ctx context.Context, app *sappb.SAPInstance) []SapSystemDetails { dbHosts, err := d.discoverDBNodes(ctx, app.Sapsid, app.InstanceNumber) if err != nil || len(dbHosts) == 0 { return nil } dbNFS, _ := d.discoverDatabaseNFS(ctx) version, dbProductVersion, _ := d.discoverHANAVersion(ctx, app) landscapeID, _ := d.discoverHANALandscapeId(ctx, app) dbProps := &spb.SapDiscovery_Component_DatabaseProperties{ DatabaseType: spb.SapDiscovery_Component_DatabaseProperties_HANA, SharedNfsUri: dbNFS, DatabaseVersion: version, DatabaseSid: app.Sapsid, InstanceNumber: app.InstanceNumber, LandscapeId: landscapeID, } dbSIDs, err := d.discoverHANATenantDBs(ctx, app, dbHosts[0]) if err != nil { log.CtxLogger(ctx).Infow("Encountered error during call to discoverHANATenantDBs. Only discovering primary HANA system.", "error", err) return []SapSystemDetails{hanaSystemDetails(app, dbProps, dbHosts, app.Sapsid, dbProductVersion, nil)} } diskMap, err := d.discoverHANADisks(ctx, app) if err != nil { log.CtxLogger(ctx).Infow("Encountered error during call to discoverHANADisks. Unable to determine HANA disk map.", "error", err) } systems := []SapSystemDetails{} for _, s := range dbSIDs { systems = append(systems, hanaSystemDetails(app, dbProps, dbHosts, s, dbProductVersion, diskMap)) } return systems } func (d *SapDiscovery) discoverNetweaverHA(ctx context.Context, app *sappb.SAPInstance) (bool, []string) { log.CtxLogger(ctx).Debugw("Checking HA nodes", "app", app) sidLower := strings.ToLower(app.Sapsid) sidAdm := fmt.Sprintf("%sadm", sidLower) params := commandlineexecutor.Params{ Executable: "sudo", Args: []string{"-i", "-u", sidAdm, "sapcontrol", "-nr", app.InstanceNumber, "-function", "HAGetFailoverConfig"}, } res := d.Execute(ctx, params) if res.Error != nil { return false, nil } ha := strings.Contains(res.StdOut, "HAActive: TRUE") if !ha { return false, nil } i := strings.Index(res.StdOut, haNodes) i += len(haNodes) lines := strings.Split(res.StdOut[i:], "\n") var nodes []string if len(lines) > 0 { for _, n := range strings.Split(lines[0], ",") { n = strings.TrimSpace(n) if len(n) > 0 { nodes = append(nodes, n) } } } log.CtxLogger(ctx).Debugw("HA nodes", "nodes", nodes) if len(nodes) == 0 { log.CtxLogger(ctx).Debug("No HA nodes found in failover config, checking PCS") params = commandlineexecutor.Params{ Executable: "pcs", Args: []string{"config", "show"}, } res = d.Execute(ctx, params) lines := strings.Split(res.StdOut, "\n") for i, line := range lines { if strings.Contains(line, "Pacemaker Nodes:") && i < len(lines)-1 { nodes = strings.Split(strings.TrimSpace(lines[i+1]), " ") break } } } return ha, nodes } func (d *SapDiscovery) discoverAppToDBConnection(ctx context.Context, sid string, abap bool) (dbHosts []string, err error) { sidLower := strings.ToLower(sid) sidAdm := fmt.Sprintf("%sadm", sidLower) if abap { result := d.Execute(ctx, commandlineexecutor.Params{ Executable: "sudo", Args: []string{"-i", "-u", sidAdm, "hdbuserstore", "list", "DEFAULT"}, }) if result.Error != nil { log.CtxLogger(ctx).Infow("Error retrieving hdbuserstore info", "sid", sid, "error", result.Error, "stdout", result.StdOut, "stderr", result.StdErr) return nil, result.Error } dbHosts = parseDBHosts(result.StdOut) if len(dbHosts) == 0 { log.CtxLogger(ctx).Infow("Unable to find DB hostname and port in hdbuserstore output", "sid", sid) return nil, errors.New("Unable to find DB hostname and port in hdbuserstore output") } } else { sidUpper := strings.ToUpper(sid) profilePath := fmt.Sprintf("/usr/sap/%s/SYS/profile/DEFAULT.PFL", sidUpper) result := d.Execute(ctx, commandlineexecutor.Params{ Executable: "sh", ArgsToSplit: `-c 'grep "SAPDBHOST" ` + profilePath + `'`, }) if result.Error != nil { log.CtxLogger(ctx).Infow("Error retrieving DB hosts from profile", "sid", sid, "error", result.Error, "stdout", result.StdOut, "stderr", result.StdErr) return nil, result.Error } matches := sapDbHostRegex.FindAllStringSubmatch(result.StdOut, -1) if len(matches) == 0 { log.CtxLogger(ctx).Infow("Unable to find DB hostname and port in profile output", "sid", sid) return nil, errors.New("Unable to find DB hostname and port in profile output") } for _, m := range matches { if len(m) > 1 { dbHosts = append(dbHosts, m[1]) } } } return dbHosts, nil } func (d *SapDiscovery) discoverNetweaverJava(ctx context.Context, app *sappb.SAPInstance) (bool, *spb.SapDiscovery_WorkloadProperties, error) { sidLower := strings.ToLower(app.Sapsid) sidUpper := strings.ToUpper(app.Sapsid) sidAdm := fmt.Sprintf("%sadm", sidLower) cmdPath := fmt.Sprintf("/usr/sap/%s/J%s/j2ee/configtool/batchconfig.csh", sidUpper, app.InstanceNumber) log.CtxLogger(ctx).Debugw("cmdPath", "cmdPath", cmdPath) params := commandlineexecutor.Params{ Executable: "sudo", Args: []string{"-i", "-u", sidAdm, cmdPath, "-task", "get.versions.of.deployed.units"}, } result := d.Execute(ctx, params) log.CtxLogger(ctx).Debugw("batchconfig.csh result", "result", result) if result.Error != nil { return false, nil, result.Error } return true, parseBatchConfigOutput(ctx, result.StdOut), nil } func parseBatchConfigOutput(ctx context.Context, s string) *spb.SapDiscovery_WorkloadProperties { scvs := []*spb.SapDiscovery_WorkloadProperties_SoftwareComponentProperties{} pv := &spb.SapDiscovery_WorkloadProperties_ProductVersion{} lines := strings.Split(s, "\n") scaLines := false for _, l := range lines { l = strings.TrimSpace(l) if strings.Contains(l, "Listing the SCA versions:") { scaLines = true continue } if scaLines && strings.Contains(l, "Listing the versions of SDAs/EARs per SCA:") { break } if !scaLines || len(l) == 0 { continue } // At this point we have actual SCA Lines to parse. scv := parseSCALine(l) scvs = append(scvs, scv) if scv.GetName() == "SERVERCORE" { // we can use this for the product version pv = &spb.SapDiscovery_WorkloadProperties_ProductVersion{ Name: "SAP Netweaver", Version: scv.GetVersion(), } } } wlProps := &spb.SapDiscovery_WorkloadProperties{ ProductVersions: []*spb.SapDiscovery_WorkloadProperties_ProductVersion{pv}, SoftwareComponentVersions: scvs, } log.CtxLogger(ctx).Debugw("NW Java Workload Properties", "wlProps", prototext.Format(wlProps)) return wlProps } func parseSCALine(l string) *spb.SapDiscovery_WorkloadProperties_SoftwareComponentProperties { // Example SCA Line - "ESCONF_BUILDT : 1000.7.50.25.0.20220803154300" words := strings.Split(l, " ") name := words[0] versions := strings.Split(words[len(words)-1], ".") // Example Version - "1000.7.50.25.0.20220803154300" // Format is - AAAA.B.CC.DD.E.FFFFFFFFFFFFF // We utilize B, CC, DD, and E. var version, extVersion, typeVal string if len(versions) > 1 { version = versions[1] } if len(versions) > 2 { version += "." + versions[2] } if len(versions) > 3 { extVersion = versions[3] } if len(versions) > 4 { typeVal = versions[4] } return &spb.SapDiscovery_WorkloadProperties_SoftwareComponentProperties{ Name: name, Version: version, ExtVersion: extVersion, Type: typeVal, } } func (d *SapDiscovery) discoverNetweaverABAP(ctx context.Context, app *sappb.SAPInstance) (bool, *spb.SapDiscovery_WorkloadProperties, error) { if err := d.FileSystem.MkdirAll(r3transTmpFolder, 0777); err != nil { return false, nil, fmt.Errorf("error creating r3trans tmp folder: %v", err) } defer d.FileSystem.RemoveAll(r3transTmpFolder) if err := d.FileSystem.Chmod(r3transTmpFolder, 0777); err != nil { return false, nil, fmt.Errorf("error changing r3trans tmp folder permissions: %v", err) } sidLower := strings.ToLower(app.Sapsid) sidAdm := fmt.Sprintf("%sadm", sidLower) // First check if the db is responding params := commandlineexecutor.Params{ Executable: "sudo", Args: []string{"-i", "-u", sidAdm, "R3trans", "-d", "-w", r3transTmpFolder + "tmp.log"}, } result := d.Execute(ctx, params) log.CtxLogger(ctx).Debugw("R3trans result", "result", result) if result.Error != nil { return false, nil, result.Error } if !strings.Contains(result.StdOut, r3transSuccessResult) { return false, nil, fmt.Errorf("R3trans returned unexpected result, database may not be connected and working:\n%s", result.StdOut) } log.CtxLogger(ctx).Debugw("DB appears good", "stdOut", result.StdOut) // Now create the control file in /tmp contents := `EXPORT file='/tmp/r3trans/export_products.dat' CLIENT=all SELECT * FROM PRDVERS SELECT * FROM CVERS` file, err := d.FileSystem.Create(tmpControlFilePath) if err != nil { log.CtxLogger(ctx).Infow("Error creating control file", "error", err) return false, nil, err } defer file.Close() if _, err = d.FileSystem.WriteStringToFile(file, contents); err != nil { log.CtxLogger(ctx).Infow("Error writing control file", "error", err) return false, nil, err } log.CtxLogger(ctx).Debugw("Control file created") // Run R3trans with the control file params.Args = []string{"-i", "-u", sidAdm, "R3trans", "-w", r3transOutputPath, tmpControlFilePath} if result = d.Execute(ctx, params); result.Error != nil { log.CtxLogger(ctx).Infow("Error running R3trans with control file", "error", result.Error) return false, nil, result.Error } // Export the data params.Args = []string{"-i", "-u", sidAdm, "R3trans", "-w", r3transOutputPath, "-v", "-l", r3transTmpFolder + "export_products.dat"} if result = d.Execute(ctx, params); result.Error != nil { log.CtxLogger(ctx).Infow("Error exporting data", "error", result.Error) return false, nil, result.Error } log.CtxLogger(ctx).Debugw("R3trans exported data", "stdOut", result.StdOut) // Read output.txt fileBytes, err := d.FileSystem.ReadFile(r3transOutputPath) if err != nil { log.CtxLogger(ctx).Infow("Error reading r3trans output file", "r3transOutputPath", r3transOutputPath, "error", err) return false, nil, err } fileString := string(fileBytes[:]) wlProps := parseR3transOutput(ctx, fileString) log.CtxLogger(ctx).Infow("Workload Properties", "wlProps", prototext.Format(wlProps)) // Command success indicates system is ABAP. return true, wlProps, nil } func parseR3transOutput(ctx context.Context, s string) (wlProps *spb.SapDiscovery_WorkloadProperties) { log.CtxLogger(ctx).Debugw("R3trans exported data", "fileString", s) lines := strings.Split(s, "\n") cversLines := false prdversLines := false cversEntries := []*spb.SapDiscovery_WorkloadProperties_SoftwareComponentProperties{} prdversEntries := []*spb.SapDiscovery_WorkloadProperties_ProductVersion{} for _, l := range lines { if strings.Contains(l, "CVERS") && strings.Contains(l, "REP") { cversLines, prdversLines = true, false } else if strings.Contains(l, "PRDVERS") && strings.Contains(l, "REP") { prdversLines, cversLines = true, false } else if !strings.Contains(l, "**") { cversLines, prdversLines = false, false } else { if cversLines { // Example line : "4 ETW000 ** 102 ** SAP_ABA 750 0025 S" cversSplit := strings.Split(l, "**") re := regexp.MustCompile("\\s+") if len(cversSplit) < 1 { log.CtxLogger(ctx).Infow("cvers entry does not have enough fields", "fields", cversSplit, "len(fields)", len(cversSplit)) continue } // Taking everything after the "**" which is "SAP_ABA 750 0025 S" // And splitting that on any number of spaces. fields := re.Split(strings.TrimSpace(cversSplit[len(cversSplit)-1]), -1) cversEntry := map[string]string{} if len(fields) > 0 { cversEntry["name"] = fields[0] } if len(fields) > 1 { cversEntry["version"] = fields[1] } if len(fields) > 2 { cversEntry["ext_version"] = fields[2] } if len(fields) == 3 { // The last two fields are combined. if len(fields[2]) < 1 { log.CtxLogger(ctx).Infow("Parsing component encountered ext_version that is too short.", "fields[2]", fields[2], "len(fields[2])", len(fields[2])) continue } // This looks like "0000000000S" where "0000000000" is the ext_version and "S" is the type. cversEntry["ext_version"] = string(fields[2][0 : len(fields[2])-1]) cversEntry["type"] = string(fields[2][len(fields[2])-1]) } if len(fields) > 3 { cversEntry["type"] = fields[3] } cversObj := &spb.SapDiscovery_WorkloadProperties_SoftwareComponentProperties{ Name: cversEntry["name"], Version: cversEntry["version"], ExtVersion: cversEntry["ext_version"], Type: cversEntry["type"], } cversEntries = append(cversEntries, cversObj) } if prdversLines { re := regexp.MustCompile("\\s\\s+") // Example of what this looks like: // 4 ETW000 ** 394 ** 73554900100900000414SAP NETWEAVER 7.5 sap.com SAP NETWEAVER 7.5 +20220927121631 // We split on multiple consecutive spaces so that we don't split in the middle of a given field. prdversSplit := re.Split(l, -1) if len(prdversSplit) < 2 { log.CtxLogger(ctx).Infow("prdvers entry does not have enough fields", "fields", prdversSplit, "len(fields)", len(prdversSplit)) continue } // Extracting the second to last element here gives us "SAP NETWEAVER 7.5" fields := prdversSplit[len(prdversSplit)-2] // Find the last space in the product description. This separates the name from the version. lastIndex := strings.LastIndex(fields, " ") if lastIndex < 0 { log.CtxLogger(ctx).Infow("Failed to distinguish name from version for prdvers entry", "fields", fields, "len(fields)", len(fields)) prdversEntries = append(prdversEntries, &spb.SapDiscovery_WorkloadProperties_ProductVersion{Name: fields}) continue } prvdersObj := &spb.SapDiscovery_WorkloadProperties_ProductVersion{ Name: fields[:lastIndex], Version: fields[lastIndex+1:], } prdversEntries = append(prdversEntries, prvdersObj) } } } return &spb.SapDiscovery_WorkloadProperties{ ProductVersions: prdversEntries, SoftwareComponentVersions: cversEntries, } } func parseDBHosts(s string) (dbHosts []string) { lines := strings.Split(s, "\n") for _, l := range lines { t := strings.TrimSpace(l) if strings.Index(t, "ENV") < 0 { continue } // Trim up to the first colon _, hosts, _ := strings.Cut(t, ":") p := strings.Split(hosts, ";") // Each semicolon part contains the pattern <host>:<port> // The first part will contain "ENV : <host>:port; <host2>:<port2>" for _, h := range p { c := strings.Split(h, ":") if len(c) < 2 { continue } dbHosts = append(dbHosts, strings.TrimSpace(c[0])) } } return dbHosts } func (d *SapDiscovery) discoverDatabaseSID(ctx context.Context, appSID string, abap bool) (string, error) { sidLower := strings.ToLower(appSID) sidUpper := strings.ToUpper(appSID) sidAdm := fmt.Sprintf("%sadm", sidLower) if abap { sid, _ := d.discoverDatabaseSIDUserStore(ctx, sidUpper, sidAdm) if sid != "" { return sid, nil } } sid, _ := d.discoverDatabaseSIDProfiles(ctx, sidUpper, sidAdm, abap) if sid != "" { return sid, nil } return "", errors.New("no database SID found") } func (d *SapDiscovery) discoverDatabaseSIDUserStore(ctx context.Context, sidUpper string, sidAdm string) (string, error) { result := d.Execute(ctx, commandlineexecutor.Params{ Executable: "sudo", Args: []string{"-i", "-u", sidAdm, "hdbuserstore", "list"}, }) if result.Error != nil { log.CtxLogger(ctx).Infow("Error retrieving hdbuserstore info", "sid", sidUpper, "error", result.Error, "stdOut", result.StdOut, "stdErr", result.StdErr) return "", result.Error } re, err := regexp.Compile(`DATABASE\s*:\s*([a-zA-Z][a-zA-Z0-9]{2})`) if err != nil { log.CtxLogger(ctx).Infow("Error compiling regex", "error", err) return "", err } sid := re.FindStringSubmatch(result.StdOut) if len(sid) > 1 { return sid[1], nil } return "", errors.New("no database SID found in userstore") } func (d *SapDiscovery) discoverDatabaseSIDProfiles(ctx context.Context, sidUpper string, sidAdm string, abap bool) (string, error) { // No DB SID in userstore, check profiles profilePath := fmt.Sprintf("/usr/sap/%s/SYS/profile/*", sidUpper) result := d.Execute(ctx, commandlineexecutor.Params{ Executable: "sh", ArgsToSplit: `-c 'grep "dbid\|dbms/name\|j2ee/dbname\|dbs/hdb/dbname" ` + profilePath + `'`, }) log.CtxLogger(ctx).Debugw("Profile grep output", "sid", sidUpper, "error", result.Error, "stdOut", result.StdOut, "stdErr", result.StdErr) if result.Error != nil { log.CtxLogger(ctx).Infow("Error retrieving sap profile info", "sid", sidUpper, "error", result.Error, "stdOut", result.StdOut, "stdErr", result.StdErr) return "", result.Error } re, err := regexp.Compile(`(dbid|dbms\/name|j2ee\/dbname|dbs\/hdb\/dbname)\s*=\s*([a-zA-Z][a-zA-Z0-9]{2})`) if err != nil { log.CtxLogger(ctx).Infow("Error compiling regex", "error", err) return "", err } matches := re.FindAllStringSubmatch(result.StdOut, -1) log.CtxLogger(ctx).Debugw("Profile grep matches", "sid", sidUpper, "matches", matches) var sidKey, sidValue string if len(matches) == 1 { log.CtxLogger(ctx).Debugw("Single match", "sid", sidUpper, "match", matches[0]) match := matches[0] if len(match) > 2 { sidValue = match[2] log.CtxLogger(ctx).Debugw("Match", "sid", sidValue, "match", match) } } else if len(matches) > 1 { log.CtxLogger(ctx).Debugw("Multiple matches", "sid", sidUpper, "matches", matches) // Multiple matches, prioritize based on abap or Java // ABAP order: dbid, dbms/name, j2ee/dbname, dbs/hdb/dbname // Java order: j2ee/dbname, dbs/hdb/dbname, dbid, dbms/name matchLoop: for _, match := range matches { log.CtxLogger(ctx).Debugw("Match", "sid", sidUpper, "match", match) if len(match) > 2 { if abap { switch match[1] { case profileDBIDNameKey: sidValue = match[2] break matchLoop case profileDBMSNameKey: if sidValue == "" || sidKey == "j2ee/dbname" || sidKey == "dbs/hdb/dbname" { sidKey = match[1] sidValue = match[2] } case profileJ2EEDBNameKey: if sidValue == "" || sidKey == "dbs/hdb/dbname" { sidKey = match[1] sidValue = match[2] } case profileDBSHDBNameKey: if sidValue == "" { sidKey = match[1] sidValue = match[2] } } } else { switch match[1] { case profileJ2EEDBNameKey: sidValue = match[2] break matchLoop case profileDBSHDBNameKey: if sidValue == "" || sidKey == "dbid" || sidKey == "dbms/name" { sidKey = match[1] sidValue = match[2] } case profileDBIDNameKey: if sidValue == "" || sidKey == "dbms/name" { sidKey = match[1] sidValue = match[2] } case profileDBMSNameKey: if sidValue == "" { sidValue = match[2] sidKey = match[1] } } } } } } if sidValue == "" { return "", errors.New("No database SID found in profiles") } return sidValue, nil } func (d *SapDiscovery) discoverDBNodes(ctx context.Context, sid, instanceNumber string) ([]string, error) { if sid == "" || instanceNumber == "" { return nil, errors.New("To discover additional HANA nodes, SID and instance number must be provided") } sidLower := strings.ToLower(sid) sidAdm := fmt.Sprintf("%sadm", sidLower) cmd := commandlineexecutor.Params{ Executable: "sudo", Args: []string{"-i", "-u", sidAdm, "sapcontrol", "-nr", instanceNumber, "-function", "GetSystemInstanceList"}, } result := d.Execute(ctx, cmd) if result.Error != nil || result.ExitCode != 0 { log.CtxLogger(ctx).Infow("Error running GetSystemInstanceList", "sid", sid, "error", result.Error, "stdOut", result.StdOut, "stdErr", result.StdErr, "exitcode", result.ExitCode) return nil, result.Error } // Example output: // // 24.07.2024 13:57:24 // GetSystemInstanceList // OK // hostname, instanceNr, httpPort, httpsPort, startPriority, features, dispstatus // sap-ph1hdbw1, 0, 50013, 50014, 0.3, HDB|HDB_WORKER, GRAY // sap-ph1hdbw2, 0, 50013, 50014, 0.3, HDB|HDB_STANDBY, GREEN // sap-ph1hdb, 0, 50013, 50014, 0.3, HDB|HDB_WORKER, GREEN var hosts []string lines := strings.Split(result.StdOut, "\n") for _, line := range lines { parts := strings.Split(line, ",") if len(parts) < 6 { continue } name := strings.TrimSpace(parts[0]) if name == "hostname" { continue } hosts = append(hosts, name) } return hosts, nil } func (d *SapDiscovery) discoverASCS(ctx context.Context, sid string) (string, error) { // The ASCS of a Netweaver server is identified by the entry "rdisp/mshost" in the DEFAULT.PFL profilePath := fmt.Sprintf("/sapmnt/%s/profile/DEFAULT.PFL", sid) p := commandlineexecutor.Params{ Executable: "grep", Args: []string{"rdisp/mshost", profilePath}, } res := d.Execute(ctx, p) if res.Error != nil { log.CtxLogger(ctx).Infow("Error executing grep", "error", res.Error, "stdOut", res.StdOut, "stdErr", res.StdErr, "exitcode", res.ExitCode) return "", res.Error } lines := strings.Split(res.StdOut, "\n") for _, line := range lines { if strings.HasPrefix(line, "#") { // Commented line, skip continue } parts := strings.Split(line, "=") if len(parts) < 2 { continue } part := strings.TrimSpace(parts[1]) if !hostnameRegex.MatchString(part) { continue } return part, nil } return "", errors.New("no ASCS found in default profile") } func (d *SapDiscovery) discoverDBType(ctx context.Context, sid string) (spb.SapDiscovery_Component_DatabaseProperties_DatabaseType, error) { // The DB type of a SAP System is identified by the entry "dbms/type" in the DEFAULT.PFL profilePath := fmt.Sprintf("/sapmnt/%s/profile/DEFAULT.PFL", sid) p := commandlineexecutor.Params{ Executable: "grep", Args: []string{profileDBMSTypeKey, profilePath}, } res := d.Execute(ctx, p) if res.Error != nil { log.CtxLogger(ctx).Infow("Error executing grep", "error", res.Error, "stdOut", res.StdOut, "stdErr", res.StdErr, "exitcode", res.ExitCode) return spb.SapDiscovery_Component_DatabaseProperties_DATABASE_TYPE_UNSPECIFIED, res.Error } log.CtxLogger(ctx).Debugw("DB type grep output", "stdOut", res.StdOut) lines := strings.Split(res.StdOut, "\n") part := "" for _, line := range lines { if strings.HasPrefix(line, "#") { // Commented line, skip continue } if !strings.Contains(line, profileDBMSTypeKey) { continue } parts := strings.Split(line, "=") if len(parts) < 2 { continue } part = strings.TrimSpace(parts[1]) break } switch part { case hanaDBName: return spb.SapDiscovery_Component_DatabaseProperties_HANA, nil case db2DBName, db4DBName, db6DBName: return spb.SapDiscovery_Component_DatabaseProperties_DB2, nil case oracleName: return spb.SapDiscovery_Component_DatabaseProperties_ORACLE, nil case sqlServerName: return spb.SapDiscovery_Component_DatabaseProperties_SQLSERVER, nil case sybaseASEName: return spb.SapDiscovery_Component_DatabaseProperties_ASE, nil case maxDBName: return spb.SapDiscovery_Component_DatabaseProperties_MAXDB, nil default: return spb.SapDiscovery_Component_DatabaseProperties_DATABASE_TYPE_UNSPECIFIED, errors.New("no DB type found in default profile") } } func (d *SapDiscovery) discoverDBHost(ctx context.Context, sid string) (string, error) { // The DB Host of a SAP System is identified by the entry "SAPDBHOST" in the DEFAULT.PFL profilePath := fmt.Sprintf("/sapmnt/%s/profile/DEFAULT.PFL", sid) p := commandlineexecutor.Params{ Executable: "grep", Args: []string{profileDBHostKey, profilePath}, } res := d.Execute(ctx, p) if res.Error != nil { log.CtxLogger(ctx).Infow("Error executing grep", "error", res.Error, "stdOut", res.StdOut, "stdErr", res.StdErr, "exitcode", res.ExitCode) return "", res.Error } log.CtxLogger(ctx).Debugw("DB host grep output", "stdOut", res.StdOut) lines := strings.Split(res.StdOut, "\n") part := "" for _, line := range lines { if strings.HasPrefix(line, "#") { // Commented line, skip continue } if !strings.Contains(line, profileDBHostKey) { continue } parts := strings.Split(line, "=") if len(parts) < 2 { continue } part = strings.TrimSpace(parts[1]) return part, nil } return "", errors.New("no SAP DB host found in default profile") } func (d *SapDiscovery) discoverAppNFS(ctx context.Context, sid string) (string, error) { // The primary NFS of a Netweaver server is identified as the one that is mounted to the /sapmnt/<SID> directory. p := commandlineexecutor.Params{ Executable: "df", Args: []string{"-h"}, } res := d.Execute(ctx, p) if res.Error != nil { log.CtxLogger(ctx).Infow("Error executing df -h", "error", res.Error, "stdOut", res.StdOut, "stdErr", res.StdErr, "exitcode", res.ExitCode) return "", res.Error } mntPath := filepath.Join("/sapmnt", sid) lines := strings.Split(res.StdOut, "\n") for _, line := range lines { if strings.Contains(line, mntPath) { matches := fsMountRegex.FindStringSubmatch(line) if len(matches) < 2 { continue } return matches[1], nil } } return "", errors.New("no NFS found") } func (d *SapDiscovery) discoverNetweaverKernelVersion(ctx context.Context, sid string) (string, error) { sidLower := strings.ToLower(sid) sidAdm := fmt.Sprintf("%sadm", sidLower) p := commandlineexecutor.Params{ Executable: "sudo", Args: []string{"-i", "-u", sidAdm, "disp+work"}, } res := d.Execute(ctx, p) if res.Error != nil { log.CtxLogger(ctx).Infow("Error executing disp+work command", "error", res.Error, "stdOut", res.StdOut, "stdErr", res.StdErr, "exitcode", res.ExitCode) return "", res.Error } kernelMatches := netweaverKernelRegex.FindStringSubmatch(res.StdOut) if len(kernelMatches) < 2 { return "", errors.New("unable to identify Netweaver kernel version") } kernelNumber, _ := strconv.Atoi(kernelMatches[1]) patchMatches := netweaverPatchNumberRegex.FindStringSubmatch(res.StdOut) if len(patchMatches) < 2 { return "", errors.New("unable to identify Netweaver kernel version") } patchNumber, _ := strconv.Atoi(patchMatches[1]) version := fmt.Sprintf("SAP Kernel %d Patch %d", kernelNumber, patchNumber) return version, nil } func (d *SapDiscovery) discoverDatabaseNFS(ctx context.Context) (string, error) { // The primary NFS of a Netweaver server is identified as the one that is mounted to the /sapmnt/<SID> directory. p := commandlineexecutor.Params{ Executable: "df", Args: []string{"-h"}, } res := d.Execute(ctx, p) if res.Error != nil { log.CtxLogger(ctx).Infow("Error executing df -h", "error", res.Error, "stdOut", res.StdOut, "stdErr", res.StdErr, "exitcode", res.ExitCode) return "", res.Error } mntPath := "/hana/shared" lines := strings.Split(res.StdOut, "\n") for _, line := range lines { if strings.Contains(line, mntPath) { matches := fsMountRegex.FindStringSubmatch(line) if len(matches) < 2 { continue } return matches[1], nil } } return "", errors.New("unable to identify main database NFS") } func (d *SapDiscovery) discoverHANAVersion(ctx context.Context, app *sappb.SAPInstance) (string, string, error) { log.CtxLogger(ctx).Debug("Entered discoverHANAVersion") sidLower := strings.ToLower(app.Sapsid) sidUpper := strings.ToUpper(app.Sapsid) sidAdm := fmt.Sprintf("%sadm", sidLower) path := fmt.Sprintf("/usr/sap/%s/HDB%s/HDB", sidUpper, app.GetInstanceNumber()) p := commandlineexecutor.Params{ Executable: "sudo", Args: []string{"-i", "-u", sidAdm, path, "version"}, } res := d.Execute(ctx, p) if res.Error != nil { log.CtxLogger(ctx).Infow("Error executing HDB version command", "error", res.Error, "stdOut", res.StdOut, "stdErr", res.StdErr, "exitcode", res.ExitCode) return "", "", res.Error } log.CtxLogger(ctx).Debugw("HDB version output", "stdOut", res.StdOut) match := hanaVersionRegex.FindStringSubmatch(res.StdOut) if len(match) < 2 { return "", "", errors.New("unable to identify HANA version") } parts := strings.Split(match[1], ".") // Ignore atoi errors since the regex enforces these parts to be numeric. majorVersion, _ := strconv.Atoi(parts[0]) minorVersion, _ := strconv.Atoi(parts[1]) revision, _ := strconv.Atoi(parts[2]) revisionMinor, _ := strconv.Atoi(parts[3]) version := fmt.Sprintf("HANA %d.%d Rev %d", majorVersion, minorVersion, revision) s := fmt.Sprintf("%d.%d SPS%02d Rev%d.%02d", majorVersion, minorVersion, int(revision/10), revision, revisionMinor) log.CtxLogger(ctx).Debugw("HANA version", "version", version, "s", s) return version, s, nil } func (d *SapDiscovery) readAndUnmarshalJson(ctx context.Context, filepath string) (map[string]any, error) { file, err := d.FileSystem.ReadFile(filepath) if err != nil { log.CtxLogger(ctx).Infow("Error reading file", "filepath", filepath, "error", err) return nil, err } data := map[string]any{} err = json.Unmarshal(file, &data) if err != nil { log.CtxLogger(ctx).Infow("Error unmarshalling file", "filepath", filepath, "error", err, "contents", string(file)) return nil, err } return data, nil } func (d *SapDiscovery) discoverHANATenantDBs(ctx context.Context, app *sappb.SAPInstance, dbHost string) ([]string, error) { // The hdb topology containing sid info is contained in the nameserver_topology_HDBHostname.json // Abstract path: /hana/shared/SID/HDBXX/HDBHostname/trace/nameserver_topology_HDBHostname.json // Concrete example: /hana/shared/DEH/HDB00/dnwh75rdbci/trace/nameserver_topology_dnwh75rdbci.json log.CtxLogger(ctx).Debugw("Entered discoverHANATenantDBs") instanceID := app.GetInstanceId() sidUpper := strings.ToUpper(app.Sapsid) topologyPath := fmt.Sprintf("/hana/shared/%s/%s/%s/trace/nameserver_topology_%s.json", sidUpper, instanceID, dbHost, dbHost) log.CtxLogger(ctx).Debugw("hdb topology file", "filepath", topologyPath) data, err := d.readAndUnmarshalJson(ctx, topologyPath) if err != nil { return nil, err } databasesData := map[string]any{} if topology, ok := data["topology"]; ok { if topologyMap, ok := topology.(map[string]any); ok { if databases, ok := topologyMap["databases"]; ok { if databasesMap, ok := databases.(map[string]any); ok { databasesData = databasesMap } } } } log.CtxLogger(ctx).Debugw("databasesData", "databasesData", databasesData) var dbSids []string for _, db := range databasesData { if dbMap, ok := db.(map[string]any); ok { if dbSid, ok := dbMap["name"]; ok { dbSidString, ok := dbSid.(string) if ok { // Ignore the SYSTEMDB if len(dbSidString) == 3 { dbSids = append(dbSids, dbSidString) } } } } } log.CtxLogger(ctx).Debugw("End of discoverHANATenantDBs", "dbSids", dbSids) return dbSids, nil } func (d *SapDiscovery) discoverHANALandscapeId(ctx context.Context, app *sappb.SAPInstance) (string, error) { log.CtxLogger(ctx).Debugw("Entered discoverHANALandscapeId") sidUpper := strings.ToUpper(app.Sapsid) path := fmt.Sprintf("/usr/sap/%s/SYS/global/hdb/custom/config/nameserver.ini", sidUpper) p := commandlineexecutor.Params{ Executable: "sh", Args: []string{"-c", `grep "id =" ` + path}, } res := d.Execute(ctx, p) if res.Error != nil { log.CtxLogger(ctx).Infow("Error executing grep", "error", res.Error, "stdOut", res.StdOut, "stdErr", res.StdErr) return "", res.Error } log.CtxLogger(ctx).Debugw("HANA landscape id output", "stdOut", res.StdOut) lid := landscapeIDRegex.FindStringSubmatch(res.StdOut) if len(lid) < 2 { return "", errors.New("unable to identify HANA landscape id") } return lid[1], nil } func (d *SapDiscovery) discoverHANADisks(ctx context.Context, app *sappb.SAPInstance) (map[string][]string, error) { mountMap := make(map[string][]string) log.CtxLogger(ctx).Debugw("Entered discoverHANADisks") sidUpper := strings.ToUpper(app.Sapsid) configPath := fmt.Sprintf(hanaConfigDir, sidUpper) globalINIPath := filepath.Join(configPath, "global.ini") deviceNames, err := findDisksForHANABasePath(ctx, logPathName, globalINIPath, d.Execute) if err != nil { log.CtxLogger(ctx).Infow("Error finding disk for log path", "error", err) } else { mountMap[logPathName] = append(mountMap[logPathName], deviceNames...) } deviceNames, err = findDisksForHANABasePath(ctx, dataPathName, globalINIPath, d.Execute) if err != nil { log.CtxLogger(ctx).Infow("Error finding disk for data path", "error", err) } else { mountMap[dataPathName] = append(mountMap[dataPathName], deviceNames...) } deviceNames, err = findDisksForHANABasePath(ctx, logBackupPathName, globalINIPath, d.Execute) if err != nil { log.CtxLogger(ctx).Infow("Error finding disk for log backup path", "error", err) } else { mountMap[logBackupPathName] = append(mountMap[logBackupPathName], deviceNames...) } log.CtxLogger(ctx).Debugw("End of discoverHANADisks", "mountMap", mountMap) return mountMap, nil } func findDisksForHANABasePath(ctx context.Context, pathName string, globalINIPath string, exec commandlineexecutor.Execute) ([]string, error) { // Get paths for desired mounts from global.ini p := commandlineexecutor.Params{ Executable: "grep", Args: []string{pathName, globalINIPath}, } res := exec(ctx, p) if res.Error != nil { log.CtxLogger(ctx).Infow("Error executing grep", "error", res.Error, "stdOut", res.StdOut, "stdErr", res.StdErr, "exitcode", res.ExitCode) return nil, res.Error } if res.StdOut == "" { return nil, errors.New("path not found in global.ini " + pathName) } // Expected output should be like: // basepath_datavolumes = /path/to/mount parts := strings.Split(res.StdOut, "=") if len(parts) < 2 { return nil, errors.New("unable to find path for mount") } mount := strings.TrimSpace(parts[1]) // Remove trailing slash if present mount = strings.TrimSuffix(mount, "/") log.CtxLogger(ctx).Debugw("Found mount", "mount", mount) // Find what is mounted to that path. p = commandlineexecutor.Params{ Executable: "lsblk", Args: []string{"--output=NAME,MOUNTPOINTS", "--json"}, } res = exec(ctx, p) if res.Error != nil { log.CtxLogger(ctx).Infow("Error executing lsblk", "error", res.Error, "stdOut", res.StdOut, "stdErr", res.StdErr, "exitcode", res.ExitCode) return nil, res.Error } // Output is json var result lsblk err := json.Unmarshal([]byte(res.StdOut), &result) if err != nil { log.CtxLogger(ctx).Infow("Error unmarshalling lsblk output", "error", err, "stdOut", res.StdOut) return nil, err } var deviceNames []string bestMatchLength := 0 // Find the block device with the best match to mount for _, blockDevice := range result.BlockDevices { log.CtxLogger(ctx).Debugw("Block device", "blockDevice", blockDevice) blockDeviceName, blockMatchLen, err := findMountPointInBlockDevice(ctx, mount, blockDevice) if err != nil { return nil, err } if blockDeviceName == "" { continue } if blockMatchLen > bestMatchLength { log.CtxLogger(ctx).Debugw("Found better match", "blockDeviceName", blockDeviceName, "blockMatchLen", blockMatchLen, "bestMatchLength", bestMatchLength) bestMatchLength = blockMatchLen deviceNames = []string{blockDeviceName} } else if blockMatchLen == bestMatchLength { log.CtxLogger(ctx).Debugw("Found match with same length", "blockDeviceName", blockDeviceName, "blockMatchLen", blockMatchLen, "bestMatchLength", bestMatchLength) deviceNames = append(deviceNames, blockDeviceName) } } if len(deviceNames) == 0 { return nil, errors.New("unable to find disk for mount") } log.CtxLogger(ctx).Debugw("Found device name", "deviceName", deviceNames) // Find disk name for that device. p = commandlineexecutor.Params{ Executable: "ls", Args: []string{"-lart", "/dev/disk/by-id/"}, } res = exec(ctx, p) if res.Error != nil { log.CtxLogger(ctx).Infow("Error executing ls", "error", res.Error, "stdOut", res.StdOut, "stdErr", res.StdErr, "exitcode", res.ExitCode) return nil, res.Error } // Output will look like: // lrwxrwxrwx 1 root root 9 Feb 5 07:32 /dev/disk/by-id/google-persistent-disk-0 -> ../../sda // lrwxrwxrwx 1 root root 9 Feb 5 07:32 /dev/disk/by-id/google-sap-posdb00-hana-shared -> ../../sdf // lrwxrwxrwx 1 root root 9 Feb 5 07:32 /dev/disk/by-id/google-sap-posdb00-hana-data-0 -> ../../sdc // lrwxrwxrwx 1 root root 9 Feb 5 07:32 /dev/disk/by-id/google-sap-posdb00-usr-sap -> ../../sdb devicePaths := []string{} for _, deviceName := range deviceNames { log.CtxLogger(ctx).Debugw("deviceName", "deviceName", deviceName) for _, line := range strings.Split(res.StdOut, "\n") { parts := strings.Fields(line) // Expected parts: // 0: permissions // 1: links // 2: owner // 3: group // 4: size // 5: month // 6: day // 7: time // 8: path // 9: -> // 10: device log.CtxLogger(ctx).Debugw("parts", "parts", parts) if len(parts) < 11 { continue } device := parts[10] if strings.HasSuffix(device, deviceName) { log.CtxLogger(ctx).Debugw("ls output", "line", line) log.CtxLogger(ctx).Debugw("Found device name in ls output") devicePath := parts[8] // Strip up up to the end of /google- devicePath = strings.TrimPrefix(devicePath, "/dev/disk/by-id/") devicePath = strings.TrimPrefix(devicePath, "google-") devicePath = strings.TrimPrefix(devicePath, "scsi-0Google_PersistentDisk_") // Maybe need to handle disk partitions if !slices.Contains(devicePaths, devicePath) { devicePaths = append(devicePaths, devicePath) } } } } return devicePaths, nil } func findMountPointInBlockDevice(ctx context.Context, mount string, blockDevice lsblkdevice) (deviceName string, bestMatchLen int, err error) { log.CtxLogger(ctx).Debugw("findMountPointInBlockDevice", "mount", mount, "blockDevice", blockDevice) splitFn := func(c rune) bool { return c == '/' } mountParts := strings.FieldsFunc(mount, splitFn) bestMatchLen = 0 for _, mountpoint := range blockDevice.Mountpoints { log.CtxLogger(ctx).Debugw("mountpoint", "mountpoint", mountpoint) mountPointParts := strings.FieldsFunc(mountpoint, splitFn) minLen := min(len(mountParts), len(mountPointParts)) matchLen := 0 for i := 0; i < minLen; i++ { if mountParts[i] != mountPointParts[i] { log.CtxLogger(ctx).Debugw("Mount parts mismatch", "mountParts", mountParts[i], "mountPointParts", mountPointParts[i]) // Mount is for a different path. matchLen = 0 break } matchLen++ } if matchLen > bestMatchLen { log.CtxLogger(ctx).Debugw("Found better match", "matchLen", matchLen, "bestMatchLen", bestMatchLen) bestMatchLen = matchLen deviceName = blockDevice.Name } } for _, child := range blockDevice.Children { _, childBestMatchLen, err := findMountPointInBlockDevice(ctx, mount, child) if err != nil { return "", 0, err } if childBestMatchLen > bestMatchLen { // Prefer using the block device's name, since the child may be a volume group. log.CtxLogger(ctx).Debugw("Found better match in child", "childBestMatchLen", childBestMatchLen, "bestMatchLen", bestMatchLen) bestMatchLen = childBestMatchLen deviceName = blockDevice.Name } } log.CtxLogger(ctx).Debugw("Returning device name", "deviceName", deviceName, "bestMatchLen", bestMatchLen) return deviceName, bestMatchLen, err }