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
}