diff --git a/.github/workflows/discovery.yml b/.github/workflows/discovery.yml index 5aa7d5c..ccae2f2 100644 --- a/.github/workflows/discovery.yml +++ b/.github/workflows/discovery.yml @@ -16,8 +16,9 @@ jobs: steps: - uses: actions/checkout@v3 with: - ref: ${{ github.head_ref }} # checkout the correct branch name - fetch-depth: 0 # fetch the whole repo history + fetch-depth: 0 + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} - name: Set up Go uses: actions/setup-go@v4 @@ -57,9 +58,16 @@ jobs: output: both thresholds: '70 80' + - name: Check if forked + id: fork_check + if: github.event_name == 'pull_request' + run: | + is_forked=$(echo "${{ github.event.pull_request.head.repo.fork }}" | tr '[:upper:]' '[:lower:]') + echo "::set-output name=is_forked::${is_forked}" + - name: Add Coverage PR Comment uses: marocchino/sticky-pull-request-comment@v2 - if: github.event_name == 'pull_request' + if: ${{ github.event_name == 'pull_request' && steps.fork_check.outputs.is_forked != 'true' }} with: recreate: true path: code-coverage-results.md @@ -74,5 +82,6 @@ jobs: path: bin - name: Push Coverage Badge + if: ${{ steps.fork_check.outputs.is_forked != 'true' }} # skip if is a forked repo. run: | .github/workflows/badge.sh diff --git a/Makefile b/Makefile index 7d8cd72..b5dea29 100644 --- a/Makefile +++ b/Makefile @@ -53,6 +53,12 @@ build: fmt vet yaml2go ## Build binary for release CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -a -o bin/discovery_darwin_amd64 cli/*.go CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o bin/discovery-l cli/*.go CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -a -o bin/discovery.exe cli/*.go + # weblogic + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -a -o bin/weblogic_darwin_arm64 weblogiccli/*.go + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -a -o bin/weblogic_darwin_amd64 weblogiccli/*.go + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o bin/weblogic-l weblogiccli/*.go + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -a -o bin/weblogic.exe weblogiccli/*.go + .PHONY: yaml2go yaml2go: yaml2go-cli ## Generate yaml config struct diff --git a/README_WEBLOGIC.md b/README_WEBLOGIC.md new file mode 100644 index 0000000..a9ed764 --- /dev/null +++ b/README_WEBLOGIC.md @@ -0,0 +1,133 @@ +![](https://img.shields.io/badge/go%20report-A+-brightgreen.svg?style=flat) +![Coverage](https://github.com/Azure/discover-java-apps/blob/badge/badge.svg?branch=badge) + +## What's this project for? + +A script to discover java apps from your linux system by following steps: + +1. SSH login to your linux system +2. Find the process for weblogic server +3. Collect information of runtime env, configuration, jar/war/ear files +4. Print the info to console (or specified file) in json or csv format + +## Download and run + +Download the binary files from [releases](https://github.com/Azure/discover-java-apps/releases) + +- For Linux: + +```bash +weblogic-l -server 'servername' -port 'port' -username 'userwithsudo' -password 'password' -weblogicusername "weblogic" -weblogicpassword "weblogicpassword" +``` + +- For Windows: + +```bash +weblogic.exe -server 'servername' -port 'port' -username 'userwithsudo' -password 'password' -weblogicusername "weblogic" -weblogicpassword "weblogicpassword" +``` + +- For Mac (Intel chip): + +```bash +weblogic_darwin_amd64 -server 'servername' -port 'port' -username 'userwithsudo' -password 'password' -weblogicusername "weblogic" -weblogicpassword "weblogicpassword" +``` + +- For Mac (Apple silicon): + +```bash +weblogic_darwin_arm64 -server 'servername' -port 'port' -username 'userwithsudo' -password 'password' -weblogicusername "weblogic" -weblogicpassword "weblogicpassword" +``` + +> You can find the running log from __weblogic.log__ in the same folder + +## Sample output + +The default output will be a json like + +```javascript +[ + { + "server": "20.39.48.129", + // Application Name + "appName": "shoppingcart", + "appType": "war", + // Application Port + "appPort": 7001, + // Runtime Memory + "jvmMemoryInMB": 512, + // OS Name + "osName": "ol", + // OS Version + "osVersion": "7.6", + // appliation absolute path + "jarFileLocation": "/u01/domains/adminDomain/servers/admin/upload/shoppingcart.war/app/shoppingcart.war", + "lastModifiedTime": "2023-06-01T06:04:21Z", + // Weblogic Version + "weblogicVersion": "14.1.1.0.0", + "runtimeJdkVersion": "11.0.11", + // Weblogic Patches + "weblogicPatches": "32697788;24150631;Mon Jan 09 07:56:07 UTC 2023;WLS PATCH SET UPDATE 14.1.1.0.210329\n32581868;24146453;Mon Jan 09 07:55:16 UTC 2023;Bundle patch for Oracle Coherence Version 14.1.1.0.4\n", + "deploymentTarget": "admin", + "serverType": "weblogic" + }, + { + ... + } +] +``` + +CSV format is also supported when `-format csv` is received in command arguments +```csv +Server,AppName,AppType,AppPort,JvmHeapMemory(MB),OsName,OsVersion,JarFileLocation,JarFileModifiedTime,WeblogicVersion,WeblogicPatches,DeploymentTarget,RuntimeJdkVersion,ServerType +20.39.48.129,testwebapp,war,7001,512,ol,7.6,/u01/domains/adminDomain/servers/admin/upload/testwebapp.war/app/testwebapp.war,2023-05-29T07:24:19Z,14.1.1.0.0,"32697788;24150631;Mon Jan 09 07:56:07 UTC 2023;WLS PATCH SET UPDATE 14.1.1.0.210329 +32581868;24146453;Mon Jan 09 07:55:16 UTC 2023;Bundle patch for Oracle Coherence Version 14.1.1.0.4 +",admin,11.0.11,weblogic +20.39.48.129,shoppingcart,war,7001,512,ol,7.6,/u01/domains/adminDomain/servers/admin/upload/shoppingcart.war/app/shoppingcart.war,2023-06-01T06:04:21Z,14.1.1.0.0,"32697788;24150631;Mon Jan 09 07:56:07 UTC 2023;WLS PATCH SET UPDATE 14.1.1.0.210329 +32581868;24146453;Mon Jan 09 07:55:16 UTC 2023;Bundle patch for Oracle Coherence Version 14.1.1.0.4 +",admin,11.0.11,weblogic + + +``` + +## Contributing + +We appreciate your help on the java app weblogic. Before your contributing, please be noted: + +1. Ensure you have Golang `1.20+` installed before starting try from source code +2. Run Makefile in `wsl` if you're Windows user +3. `70%` test coverage is mandatory in the PR build, so when you do PR, remember to include test cases as well. +4. Recommend to use [Ginkgo](https://onsi.github.io/ginkgo/) for a BDD style test case + +## Build + +```bash +make build +``` + +## Check code coverage + +```bash +go tool cover -func=coverage.out | grep total: | grep -Eo '[0-9]+\.[0-9]+' +``` + +## Limitation + +Only support to discover the weblogic apps from Linux VM + +## Road map + +- More java app runtime are coming. + +| Type | Readiness | Ready Date | +|----------------| -- | -- | +| SpringBoot App | Ready | 2023-04 | +| WebLogic App | Alpha | 2023-06 | +| Tomcat App | Planned | - | +| WebSphere App | Planned | - | +| JBoss EAP App | Planned | - | + +- More source operating systems are coming. + +## Support + +Report the issue to diff --git a/springboot/yaml_cfg.go b/springboot/yaml_cfg.go index 93c8759..f1746e1 100644 --- a/springboot/yaml_cfg.go +++ b/springboot/yaml_cfg.go @@ -1,9 +1,23 @@ package springboot +// Pattern +type Pattern struct { + Cert []string `yaml:"cert"` + Static Static `yaml:"static"` + App []string `yaml:"app"` + Logging Logging `yaml:"logging"` +} + +// Static +type Static struct { + Extension []string `yaml:"extension"` + Folder []string `yaml:"folder"` +} + // Logging type Logging struct { - ConsoleOutput ConsoleOutput `yaml:"console_output"` FilePatterns []string `yaml:"file_patterns"` + ConsoleOutput ConsoleOutput `yaml:"console_output"` } // ConsoleOutput @@ -12,17 +26,18 @@ type ConsoleOutput struct { Yamlpath []string `yaml:"yamlpath"` } -// Static -type Static struct { - Folder []string `yaml:"folder"` - Extension []string `yaml:"extension"` -} - // Env type Env struct { Denylist []string `yaml:"denylist"` } +// YamlConfig +type YamlConfig struct { + Server Server `yaml:"server"` + Pattern Pattern `yaml:"pattern"` + Env Env `yaml:"env"` +} + // Server type Server struct { Connect Connect `yaml:"connect"` @@ -33,19 +48,3 @@ type Connect struct { Parallel bool `yaml:"parallel"` Parallelism int `yaml:"parallelism"` } - -// YamlConfig -type YamlConfig struct { - Pattern Pattern `yaml:"pattern"` - Env Env `yaml:"env"` - Server Server `yaml:"server"` -} - -// Pattern -type Pattern struct { - Logging Logging `yaml:"logging"` - Cert []string `yaml:"cert"` - Static Static `yaml:"static"` - App []string `yaml:"app"` -} - diff --git a/weblogic/config.go b/weblogic/config.go new file mode 100644 index 0000000..0aecf0d --- /dev/null +++ b/weblogic/config.go @@ -0,0 +1,10 @@ +package weblogic + +import ( + _ "embed" +) + +//go:embed config.yml +var defaultConfigYaml string + +var ConfigPathEnvKey = "CONFIG_PATH" diff --git a/weblogic/config.yml b/weblogic/config.yml new file mode 100644 index 0000000..e3a2f8f --- /dev/null +++ b/weblogic/config.yml @@ -0,0 +1,96 @@ +server: + connect: + parallel: true + parallelism: 50 +pattern: + app: + - "application\\.ya?ml" + - "application-\\w+\\.ya?ml" + - "application\\.properties" + - "application-\\w+\\.properties" + logging: + file_patterns: + - "log4j\\.xml" + - "log4j-\\w+\\.xml" + - "log4j2\\.xml" + - "log4j2-\\w+\\.xml" + - "log4j2\\.ya?ml" + - "log4j2-\\w+\\.ya?ml" + - "log4j2\\.jso?n" + - "log4j2-\\w+\\.jso?n" + - "log4j\\.properties" + - "log4j-\\w+\\.properties" + - "log4j2\\.properties" + - "log4j2-\\w+\\.properties" + - "logback\\.xml" + - "logback-\\w+\\.xml" + console_output: + patterns: + - (?i)(appender.*=.*ConsoleAppender|appender.*\.type\s*=\s*Console) + - (?i)()([\s\S])*( 0 { + process := processes[0] + azureLogger.Info("begin to discover process", "processId", process.GetProcessId()) + + tempfolderPath := process.CreateTempFolder() + println("tempfolderPath: " + tempfolderPath) + defer process.DeleteTempFolder(tempfolderPath) + + weblogicName, _ := process.GetWeblogicName() + println("WeblogicServerName: " + weblogicName) + + javaHome, _ := process.GetJavaHome() + println("Java_Home: " + javaHome) + + // Get weblogic Home + weblogicHome, _ := process.GetWeblogicHome() + println("Weblogic_Home: " + weblogicHome) + oracleHome := getOracleHome(weblogicHome) + println("Oracle_Home: " + oracleHome) + + connection := buildConnection(cred.WeblogicUsername, cred.WeblogicPassword, cred.Weblogicport) + domainHome, _ := process.GetDomainHome(oracleHome, tempfolderPath, connection) + println("The Domain_Home is: " + domainHome) + applicationPathMap := process.GetApplicationsAndPath(domainHome, tempfolderPath, connection) + print("The total number of applications detected is: ") + println(len(applicationPathMap)) + + if len(applicationPathMap) > 0 { + process.UploadAndInstallWDT(tempfolderPath) + process.RunDiscoverDomainCommand(tempfolderPath, javaHome, cred.WeblogicUsername, cred.WeblogicPassword, oracleHome, cred.Weblogicport) + discoverDomainResult := process.GetDiscoverDomainResult(tempfolderPath) + // Iterate over the map + for application, app_path := range applicationPathMap { + fmt.Printf("application: %s, app_path: %s\n", application, app_path) + var app = new(WeblogicApp) + + app.Server = getServer(process) + + app.OsName, _ = process.Executor().GetOsName() + app.OsVersion, _ = process.Executor().GetOsVersion() + memory, _ := getJvmMemory(process) + app.JvmMemoryInMB = memory + app.RuntimeJdkVersion, _ = process.GetRuntimeJdkVersion() + app.AppFileLocation = app_path + app.LastModifiedTime, _ = process.GetLastModifiedTime(app_path) + + app.AppName = application + app.AppType = getAppType(discoverDomainResult, application) + app.DeploymentTarget = getDeploymentTarget(discoverDomainResult, application) + app.ServerType = getServerType(discoverDomainResult) + app.AppPort = getAppPort(discoverDomainResult) + + app.WeblogicVersion = process.GetWeblogicVersion(domainHome) + app.WeblogicPatches = process.GetWeblogicPatch(domainHome) + + app.OracleHome = oracleHome + app.DomainHome = domainHome + + azureLogger.Info("finished to discover process, found app", "processId", process.GetProcessId(), "app", app.AppName) + + apps = append(apps, app) + } + } + + } else { + println("No weblogic process detected") + } + + println("------------------------------------------------------") + return apps, Join(errs...) +} + +func getOracleHome(home string) string { + return strings.TrimSuffix(home, "/wlserver/server") +} + +func buildConnection(weblogicuserName, weblogicPassword string, port int) string { + return "connect('" + weblogicuserName + "','" + weblogicPassword + "','t3://localhost:" + strconv.Itoa(port) + "')" +} + +func (s *webLogicDiscoveryExecutor) tryConnect(ctx context.Context, serverConnectionInfos []ServerConnectionInfo) (ServerDiscovery, *Credential, error) { + azureLogger := GetAzureLogger(ctx) + var serverDiscovery ServerDiscovery + var cred *Credential + var err error + for _, info := range serverConnectionInfos { + if len(info.Server) == 0 || info.Port == 0 { + azureLogger.Warning(err, "invalid connection info", "server", info.Server, "port", info.Port) + continue + } + serverDiscovery = NewLinuxServerDiscovery( + ctx, + s.serverConnectorFactory.Create(ctx, info.Server, info.Port), + s.credentialProvider, + ) + + if cred, err = serverDiscovery.Prepare(); err != nil { + _ = serverDiscovery.Finish() + if IsCredentialError(err) { + // if credential error, the server is connectable, just break to avoid nonsense try + break + } + azureLogger.Warning(err, "failed to connect to", "server", info.Server) + continue + } else { + return serverDiscovery, cred, nil + } + } + + // every connection info has been tried, but still failed + return nil, cred, err +} + +var getAppPort = func(discoverDomainResult string) int { + value, _ := getYamlValue(discoverDomainResult, "topology.Server.admin.WebServer.FrontendHTTPPort") + return value.(int) +} + +var getAppType = func(discoverDomainResult string, application string) string { + value, _ := getYamlValue(discoverDomainResult, "appDeployments.Application."+application+".ModuleType") + return value.(string) +} + +var getDeploymentTarget = func(discoverDomainResult string, application string) string { + value, _ := getYamlValue(discoverDomainResult, "appDeployments.Application."+application+".Target") + return value.(string) +} + +var getServerType = func(discoverDomainResult string) string { + return "weblogic" +} + +var getArtifactName = func(process WebLogicProcess, discoverDomainResult string, application string) string { + value, _ := getYamlValue(discoverDomainResult, "appDeployments.Application."+application+".SourcePath") + return value.(string) +} + +func getYamlValue(yamlString string, path string) (interface{}, bool) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlString), &data); err != nil { + log.Fatal(err) + } + return getNestedValue(data, path) +} + +func getNestedValue(data map[string]interface{}, path string) (interface{}, bool) { + keys := splitPath(path) + + for _, key := range keys { + value, ok := data[key] + if !ok { + return nil, false + } + + if nestedData, ok := value.(map[string]interface{}); ok { + data = nestedData + } else { + return value, true + } + } + + return nil, false +} + +func splitPath(path string) []string { + return strings.Split(path, ".") +} + +var getServer = func(process WebLogicProcess) string { + return process.Executor().Server().FQDN() +} + +var getJvmMemory = func(process WebLogicProcess) (int64, error) { + return process.GetJvmMemory() +} diff --git a/weblogic/discover_absolute_path.py b/weblogic/discover_absolute_path.py new file mode 100644 index 0000000..1b346b1 --- /dev/null +++ b/weblogic/discover_absolute_path.py @@ -0,0 +1,12 @@ +# Get the list of deployed applications +deployments = cmo.getAppDeployments() + +for deployment in deployments: + deploymentName = deployment.getName() + deploymentPath = deployment.getAbsoluteSourcePath() + # Print the application name and its absolute path + print("-----------------------------------------") + print("application_name is: " + deploymentName + "; absolute_path is: " + deploymentPath + ";") + print("-----------------------------------------") + # Disconnect from the WebLogic Server disconnect() +disconnect() diff --git a/weblogic/discover_wls_domain_home.py b/weblogic/discover_wls_domain_home.py new file mode 100644 index 0000000..e94b939 --- /dev/null +++ b/weblogic/discover_wls_domain_home.py @@ -0,0 +1,3 @@ +domain_home = get('RootDirectory') +print("The domain_home is: " + domain_home) +disconnect() diff --git a/weblogic/errors.go b/weblogic/errors.go new file mode 100644 index 0000000..df6d5df --- /dev/null +++ b/weblogic/errors.go @@ -0,0 +1,98 @@ +package weblogic + +import ( + "fmt" + "github.com/pkg/errors" +) + +type SshError struct { + error + message string +} + +func (se SshError) Error() string { + return fmt.Sprintf("failed to execute command, cause: %s, message: %s", se.error, se.message) +} + +func (se SshError) Unwrap() error { + return se.error +} + +type PermissionDenied struct { + error + message string +} + +func (pe PermissionDenied) Error() string { + return fmt.Sprintf("permission deinied when executing command, cause: %s, message: %s", pe.error, pe.message) +} + +func (pe PermissionDenied) Unwrap() error { + return pe.error +} + +type ConnectionError struct { + error + message string +} + +func (ce ConnectionError) Error() string { + return fmt.Sprintf("failed to connect to target server, cause: %s, message: %s", ce.error, ce.message) +} + +func (ce ConnectionError) Unwrap() error { + return ce.error +} + +type ConnectionTimeoutError struct { + message string +} + +func (ce ConnectionTimeoutError) Error() string { + return ce.message +} + +type CredentialError struct { + error + message string +} + +func (ce CredentialError) Error() string { + return fmt.Sprintf("failed to connect with credential, cause: %s, message: %s ", ce.error, ce.message) +} + +func (ce CredentialError) Unwrap() error { + return ce.error +} + +func Join(errs ...error) error { + if len(errs) == 0 { + return nil + } + + e := JoinErrors{} + for _, err := range errs { + e.errors = append(e.errors, err) + } + return e +} + +type JoinErrors struct { + errors []error +} + +func (je JoinErrors) Error() string { + return fmt.Sprintf("joined errors, total %v ", len(je.errors)) +} + +func (je JoinErrors) Unwrap() []error { + return je.errors +} + +func IsCredentialError(err error) bool { + return is(err, &CredentialError{}) +} + +func is(from, to error) bool { + return errors.As(from, to) +} diff --git a/weblogic/linux_cmd.go b/weblogic/linux_cmd.go new file mode 100644 index 0000000..2b3e75a --- /dev/null +++ b/weblogic/linux_cmd.go @@ -0,0 +1,57 @@ +package weblogic + +import "fmt" + +const ( + WeblogicProcessScanCmd = "ps axo pid,uid,cmd|grep weblogic.Server| grep -v grep" + + LinuxSha256Cmd = "sha256sum %s | awk '{print $1}'" + LinuxGetJdkVersionCmd = "%s -version 2>&1 | head -n 1 | awk -F '\"' '{print $2}'" + LinuxGetTotalMemoryCmd = "cat /proc/meminfo | grep MemTotal | awk '{print $2}'" + LinuxGetDefaultMaxHeapCmd = "%s -XX:+PrintFlagsFinal 2>1 | grep ' MaxHeapSize ' | awk '{print $4}'" + LinuxGetPortsCmd = `ls -lta /proc/%[1]d/fd | grep socket | awk -F'[\\[\\]]' '{print $2}' | xargs -I {} grep {} /proc/%[1]d/net/tcp /proc/%[1]d/net/tcp6 | awk '{print $3}' | awk -F':' '{print $2}' | sort | uniq | xargs -I {} printf '%%d\n' '0x{}'` + LinuxGetOsName = "grep '^ID=' /etc/os-release | awk -F= '{print $2}'" + LinuxGetOsVersion = "grep '^VERSION_ID=' /etc/os-release | awk -F= '{print $2}'" + OracleOsGetName = "cat /etc/oracle-release | awk '{print $1}'" + OracleGetVersion = "cat /etc/oracle-release | awk '{print $3}'" +) + +func GetWeblogicProcessScanCmd() string { + return WeblogicProcessScanCmd +} + +func GetSha256Cmd(filename string) string { + return fmt.Sprintf(LinuxSha256Cmd, filename) +} + +func GetJdkVersionCmd(javaCmd string) string { + return fmt.Sprintf(LinuxGetJdkVersionCmd, javaCmd) +} + +func GetTotalMemoryCmd() string { + return LinuxGetTotalMemoryCmd +} + +func GetDefaultMaxHeap(javaCmd string) string { + return fmt.Sprintf(LinuxGetDefaultMaxHeapCmd, javaCmd) +} + +func GetPortsCmd(pid int) string { + return fmt.Sprintf(LinuxGetPortsCmd, pid) +} + +func GetOsName() string { + return fmt.Sprintf(LinuxGetOsName) +} + +func GetOsVersion() string { + return fmt.Sprintf(LinuxGetOsVersion) +} + +func GetOracleOsName() string { + return fmt.Sprintf(OracleOsGetName) +} + +func GetOracleOsVersion() string { + return fmt.Sprintf(OracleGetVersion) +} diff --git a/weblogic/logger.go b/weblogic/logger.go new file mode 100644 index 0000000..7263dd9 --- /dev/null +++ b/weblogic/logger.go @@ -0,0 +1,78 @@ +package weblogic + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + "strings" +) + +type CustomLogger struct { + logr logr.Logger +} + +func GetAzureLogger(ctx context.Context, annotationsMaps ...map[string]string) *CustomLogger { + log, _ := logr.FromContext(ctx) + c := &CustomLogger{logr: log} + if annotationsMaps != nil { + for _, annotationsMap := range annotationsMaps { + c.addAzureAnnotations(annotationsMap) + } + } + return c +} + +// addAzureAnnotations Adding annotation in custom logger. +func (c *CustomLogger) addAzureAnnotations(annotationsMap map[string]string) { + for annotationkey, annotationvalue := range annotationsMap { + // only adding azure annotations + if strings.HasPrefix(annotationkey, "management.azure.com/") { + scrubbedKey := strings.TrimPrefix(annotationkey, "management.azure.com/") + // strcase.ToCamel() and serialize the system data. + c.logr = c.logr.WithValues(scrubbedKey, annotationvalue) + + } else { + c.logr = c.logr.WithValues(annotationkey, annotationvalue) + } + } +} + +func toFields(keysAndValues ...interface{}) map[string]interface{} { + var m = make(map[string]interface{}) + if len(keysAndValues) == 0 { + return m + } + + length := len(keysAndValues) + for i := 0; i < length; i = i + 2 { + var key = fmt.Sprintf("%v", keysAndValues[i]) + var val any + if i+1 < length { + val = keysAndValues[i+1] + } + m[key] = val + } + + return m +} + +func (c *CustomLogger) Info(msg string, keysAndValues ...interface{}) { + _, l := c.logr.WithCallStackHelper() + l.V(0).Info(msg, "Fields", toFields(keysAndValues...)) +} + +func (c *CustomLogger) Debug(msg string, keysAndValues ...interface{}) { + _, l := c.logr.WithCallStackHelper() + l.V(1).Info(msg, "Fields", toFields(keysAndValues...)) +} + +func (c *CustomLogger) Warning(err error, msg string, keysAndValues ...interface{}) { + if c.logr.GetSink() != nil { + c.logr.GetSink().WithValues("err", err).Info(-1, msg, "Fields", toFields(keysAndValues...)) + } +} + +func (c *CustomLogger) Error(err error, msg string, keysAndValues ...interface{}) { + _, l := c.logr.WithCallStackHelper() + l.Error(err, msg, "Fields", toFields(keysAndValues...)) +} diff --git a/weblogic/process.go b/weblogic/process.go new file mode 100644 index 0000000..def8307 --- /dev/null +++ b/weblogic/process.go @@ -0,0 +1,539 @@ +package weblogic + +import ( + "bufio" + "bytes" + "crypto/rand" + _ "embed" + "fmt" + "github.com/docker/go-units" + "github.com/pkg/errors" + "github.com/pkg/sftp" + "io" + "math" + "math/big" + "regexp" + "strconv" + "strings" + "time" +) + +const ( + JavaCmd = "java" + JarOption = "-jar" + JvmOptionXmx = "-Xmx" + JvmOptionMaxRamPercentage = "-XX:MaxRAMPercentage" + KiB = 1024 + MiB = KiB * 1024 + WeblogicName = "-Dweblogic.Name=" + WeblogicHome = "-Dweblogic.home=" + LetterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +) + +//go:embed weblogic-deploy.zip +var wdtFileData []byte + +//go:embed discover_wls_domain_home.py +var discoverDomainHome []byte + +//go:embed discover_absolute_path.py +var discoverAbsolutePath []byte + +type javaProcess struct { + pid int + uid int + options []string + environments []string + javaCmd string + executor ServerDiscovery +} + +func (p *javaProcess) GetLastModifiedTime(path string) (time.Time, error) { + println("GetLastModifiedTime of application ....") + + commands := []string{ + "file=" + path, + "stat -c %Y $file", + } + command := strings.Join(commands, "; ") + + output, err := p.executor.Server().RunCmd(command) + if err != nil { + panic(err) + } + + timestamp, _ := strconv.ParseInt(strings.TrimSpace(output), 10, 64) + + println("last modified time: " + time.Unix(timestamp, 0).String()) + return time.Unix(timestamp, 0), nil +} + +func (p *javaProcess) GetApplicationsAndPath(DomainHome string, Randomfolder string, connection string) map[string]string { + println("Getting application names and app paths ......") + + var sshClient = p.executor.Server().Client() + sftpClient, err := sftp.NewClient(sshClient) + if err != nil { + panic(err) + } + defer sftpClient.Close() + + remoteFilePath := Randomfolder + "/discover_absolute_path.py" + + remoteFile, err := sftpClient.Create(remoteFilePath) + if err != nil { + panic(err) + } + defer remoteFile.Close() + + buf := bytes.NewBuffer(discoverAbsolutePath) + _, err = io.Copy(remoteFile, buf) + if err != nil { + panic(err) + } + + filePath := Randomfolder + "/discover_absolute_path.py" + commands := []string{ + "echo \"" + connection + "\" | cat - " + filePath + " > temp && mv temp " + filePath, + ". " + DomainHome + "/bin/setDomainEnv.sh; java $WLST_ARGS weblogic.WLST " + filePath, + } + command := strings.Join(commands, "; ") + + output, err := p.executor.Server().RunCmd(command) + if err != nil { + panic(err) + } + + return extractValues(output) +} + +func extractValues(input string) map[string]string { + regex := regexp.MustCompile(`application_name is: (.*?); absolute_path is: (.*?);`) + matches := regex.FindAllStringSubmatch(input, -1) + + result := make(map[string]string) + for _, match := range matches { + applicationName := match[1] + absolutePath := match[2] + result[applicationName] = absolutePath + } + + return result +} + +func (p *javaProcess) DeleteTempFolder(path string) string { + + println("Deleting RandomFolder ....") + + commands := []string{ + "rm -rf " + path + "/", + } + command := strings.Join(commands, "; ") + + output, err := p.executor.Server().RunCmd(command) + if err != nil { + panic(err) + } + return strings.Trim(output, " \n") +} + +func (p *javaProcess) CreateTempFolder() string { + randomString := generateRandomString(5) + randomFolder := "discover_weblogic_" + randomString + + commands := []string{ + "mkdir " + randomFolder, + "cd " + randomFolder, + "pwd", + } + command := strings.Join(commands, "; ") + + output, err := p.executor.Server().RunCmd(command) + if err != nil { + panic(err) + } + return strings.Trim(output, " \n") +} + +func (p *javaProcess) GetDomainHome(Oracle_Home string, Randomfolder string, connection string) (string, error) { + println("Getting Domain_Home ......") + + var sshClient = p.executor.Server().Client() + sftpClient, err := sftp.NewClient(sshClient) + if err != nil { + panic(err) + } + defer sftpClient.Close() + + remoteFilePath := Randomfolder + "/discover_wls_domain_home.py" + + remoteFile, err := sftpClient.Create(remoteFilePath) + if err != nil { + panic(err) + } + defer remoteFile.Close() + + buf := bytes.NewBuffer(discoverDomainHome) + _, err = io.Copy(remoteFile, buf) + if err != nil { + panic(err) + } + + commands := []string{ + "cd " + Randomfolder, + "export Oracle_Home=" + Oracle_Home, + "echo \"" + connection + "\" | cat - " + remoteFilePath + " > temp && mv temp " + remoteFilePath, + "bash $Oracle_Home/oracle_common/common/bin/wlst.sh " + remoteFilePath, + } + command := strings.Join(commands, "; ") + + output, err := p.executor.Server().RunCmd(command) + if err != nil { + panic(err) + } + return extractDomainHome(output) +} + +func (p *javaProcess) GetWeblogicPatch(DomainHome string) string { + println("Getting Weblogic Patch ......") + + commands := []string{ + ". " + DomainHome + "/bin/setDomainEnv.sh", + "java weblogic.version -verbose", + } + command := strings.Join(commands, "; ") + + output, err := p.executor.Server().RunCmd(command) + if err != nil { + panic(err) + } + + return findOPatchPatches(output) +} + +func (p *javaProcess) GetWeblogicVersion(DomainHome string) string { + println("Getting Weblogic Version ......") + + commands := []string{ + ". " + DomainHome + "/bin/setDomainEnv.sh", + "java weblogic.version -verbose", + } + command := strings.Join(commands, "; ") + + output, err := p.executor.Server().RunCmd(command) + if err != nil { + panic(err) + } + + return findWebLogicVersion(output) +} + +func (p *javaProcess) GetDiscoverDomainResult(Randomfolder string) string { + println("Getting discoverDomain.sh runing result....start") + + command := "cat " + Randomfolder + "/wlsdModel.yaml" + output, err := p.executor.Server().RunCmd(command) + if err != nil { + panic(err) + } + println("output is: ", output) + return output +} + +func (p *javaProcess) RunDiscoverDomainCommand(randomfolder, javaHome, weblogicUser, weblogicPassword, oracleHome string, port int) string { + + println("Running RunDiscoverDomainCommand in WDT....") + + commands := []string{ + "export PSW=" + weblogicPassword, + "export JAVA_HOME=" + javaHome, + randomfolder + "/weblogic-deploy/bin/discoverDomain.sh " + + "-oracle_home " + oracleHome + " " + + "-remote " + + "-model_file " + randomfolder + "/wlsdModel.yaml " + + "-admin_user " + weblogicUser + " " + + "-admin_url t3://localhost:" + strconv.Itoa(port) + " " + + "-admin_pass_env PSW", + } + command := strings.Join(commands, "; ") + + output, err := p.executor.Server().RunCmd(command) + if err != nil { + panic(err) + } + + return output + +} + +func (p *javaProcess) UploadAndInstallWDT(Randomfolder string) string { + + var sshClient = p.executor.Server().Client() + println("Uploading WebLogic Deploy Tooling (WDT) to the remote server ......") + sftpClient, err := sftp.NewClient(sshClient) + if err != nil { + panic(err) + } + defer sftpClient.Close() + + remoteFilePath := Randomfolder + "/weblogic-deploy.zip" // Replace with the remote file path where you want to upload + + remoteFile, err := sftpClient.Create(remoteFilePath) + if err != nil { + panic(err) + } + defer remoteFile.Close() + + buf := bytes.NewBuffer(wdtFileData) + _, err = io.Copy(remoteFile, buf) + if err != nil { + panic(err) + } + + // Create a new SSH session to run the unzip command + session, err := sshClient.NewSession() + if err != nil { + panic(err) + } + defer session.Close() + + commands := []string{ + "cd " + Randomfolder, + "unzip " + remoteFilePath, + } + command := strings.Join(commands, "; ") + + if err := session.Run(command); err != nil { + panic(err) + } + + return "success upload wdt" +} + +func (p *javaProcess) GetWeblogicName() (string, error) { + + var result string + for _, option := range p.options { + if strings.HasPrefix(option, WeblogicName) { + return strings.TrimPrefix(option, WeblogicName), nil + } + } + return result, nil +} + +func (p *javaProcess) GetWeblogicHome() (string, error) { + var result string + for _, option := range p.options { + if strings.HasPrefix(option, WeblogicHome) { + return strings.TrimPrefix(option, WeblogicHome), nil + } + } + return result, nil +} + +func (p *javaProcess) GetJavaHome() (string, error) { + return strings.TrimSuffix(p.javaCmd, "/bin/java"), nil +} + +func (p *javaProcess) GetRuntimeJdkVersion() (string, error) { + buf, err := runWithSudo(p.executor.Server(), GetJdkVersionCmd(p.javaCmd)) + if err != nil { + return "", err + } + + return CleanOutput(buf), nil +} + +func (p *javaProcess) GetJvmOptions() ([]string, error) { + var jvmOptions []string + var jarOpIdx = -1 + for idx, option := range p.options { + if strings.EqualFold(option, JarOption) { + jarOpIdx = idx + continue + } + if jarOpIdx != -1 && idx == jarOpIdx+1 { + // this is jar file + continue + } + jvmOptions = append(jvmOptions, option) + } + + return jvmOptions, nil +} + +var envSplitter bufio.SplitFunc = func(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + + // Find the index of the input of a newline followed by a + // pound sign. + if i := strings.Index(string(data), "\000"); i >= 0 { + return i + 1, data[0:i], nil + } + + // If at end of file with data return the data + if atEOF { + return len(data), data, nil + } + + return +} + +func (p *javaProcess) GetJavaCmd() (string, error) { + return p.javaCmd, nil +} + +func (p *javaProcess) GetJvmMemory() (int64, error) { + for _, option := range p.options { + if strings.HasPrefix(option, JvmOptionXmx) { + bs, err := units.RAMInBytes(option[len(JvmOptionXmx):]) + if err != nil { + return 0, errors.Wrap(err, fmt.Sprintf("failed to parse -Xmx from pid %v", p.pid)) + } + return bs, nil + } + } + + // Do a second iteration here due to -Xmx has higher priority than -XX:MaxRamPercentage + // If both are set, -XX:MaxRamPercentage will be ignored + // So if nothing found in the first iteration, we try another round + for _, option := range p.options { + if strings.HasPrefix(option, JvmOptionMaxRamPercentage) { + total, err := p.executor.GetTotalMemory() + if err != nil { + return 0, err + } + + percent, err := strconv.ParseFloat(option[len(JvmOptionMaxRamPercentage)+1:], 64) + if err != nil { + return 0, errors.Wrap(err, "failed to parse -XX:MaxRAMPercentage") + } + return int64(math.Round(float64(total)*percent) / 100), nil + } + } + + defaultMaxHeap, err := p.getDefaultMaxHeapSize() + if err != nil { + return 0, err + } + return defaultMaxHeap, nil +} + +func (p *javaProcess) Executor() ServerDiscovery { + return p.executor +} + +func (p *javaProcess) GetProcessId() int { + return p.pid +} + +func (p *javaProcess) GetPorts() ([]int, error) { + output, err := runWithSudo(p.executor.Server(), GetPortsCmd(p.pid)) + if err != nil { + return nil, err + } + scanner := bufio.NewScanner(strings.NewReader(output)) + var ports []int + for scanner.Scan() { + text := scanner.Text() + if len(strings.TrimSpace(text)) > 0 { + if port, e := strconv.Atoi(strings.TrimSpace(text)); e == nil { + ports = append(ports, port) + } + } + } + + return ports, nil +} + +func (p *javaProcess) getDefaultMaxHeapSize() (int64, error) { + output, err := runWithSudo(p.executor.Server(), GetDefaultMaxHeap(p.javaCmd)) + if err != nil { + return 0, err + } + if len(output) == 0 { + return 0, errors.New("failed to get default MaxHeapSize, output is empty") + } + + size, err := strconv.ParseFloat(CleanOutput(output), 64) + if err != nil { + return 0, errors.Wrap(err, fmt.Sprintf("failed to parse default MaxHeapSize, output: %s", output)) + } + + return int64(size), nil +} + +func (p *javaProcess) String() string { + s := []string{strconv.Itoa(p.pid)} + s = append(s, p.options...) + return strings.Join(s, " ") +} + +func (p *javaProcess) GetUid() int { + return p.uid +} + +func runWithSudo(server ServerConnector, cmd string) (string, error) { + output, err := server.RunCmd(cmd) + if err != nil { + if errors.As(err, &PermissionDenied{}) { + output, err = server.RunCmd(sudo(cmd)) + } + if err != nil { + return "", err + } + } + return output, nil +} + +func findWebLogicVersion(str string) string { + index := strings.Index(str, "WebLogic Server ") + if index != -1 { + startIndex := index + len("WebLogic Server ") + endIndex := strings.Index(str[startIndex:], " ") + if endIndex != -1 { + return str[startIndex : startIndex+endIndex] + } + } + return "" +} + +func findOPatchPatches(str string) string { + index := strings.Index(str, "OPatch Patches:") + if index != -1 { + startIndex := index + len("OPatch Patches:\n") + endIndex := strings.Index(str[startIndex:], "\nSERVICE NAME") + if endIndex != -1 { + return str[startIndex : startIndex+endIndex] + } + } + return "" +} + +func generateRandomString(length int) string { + b := make([]byte, length) + for i := range b { + num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(LetterBytes)))) + b[i] = LetterBytes[num.Int64()] + } + return string(b) +} + +func extractDomainHome(text string) (string, error) { + // Define the regular expression pattern to match the domain_home value + pattern := `The domain_home is:\s+(.+)` + + // Compile the regular expression + regex := regexp.MustCompile(pattern) + + // Find the first match + match := regex.FindStringSubmatch(text) + + if len(match) > 1 { + return match[1], nil + } else { + return "", fmt.Errorf("No domain_home found") + } +} diff --git a/weblogic/server_connector.go b/weblogic/server_connector.go new file mode 100644 index 0000000..0a58930 --- /dev/null +++ b/weblogic/server_connector.go @@ -0,0 +1,197 @@ +package weblogic + +import ( + "bytes" + "context" + "fmt" + "github.com/pkg/sftp" + "golang.org/x/crypto/ssh" + "io" + "os" + "strings" + "sync" + "time" +) + +type linuxServerFactory struct { + opts []SshOption +} + +type SshOption func(s *linuxServer) + +func DefaultServerConnectorFactory(opts ...SshOption) ServerConnectorFactory { + return &linuxServerFactory{opts: opts} +} + +func (f *linuxServerFactory) Create(ctx context.Context, host string, port int) ServerConnector { + s := &linuxServer{ + server: host, + port: port, + ctx: ctx, + } + + for _, opt := range f.opts { + opt(s) + } + return s +} + +type linuxServer struct { + client *ssh.Client + username string + cb ssh.HostKeyCallback + keyAlgos []string + timeout time.Duration + server string + port int + ctx context.Context + mux sync.Mutex +} + +func (s *linuxServer) RunCmd(cmd string) (string, error) { + if s.client == nil { + return "", ConnectionError{error: fmt.Errorf("server %s is not connected", s.server), message: "ssh client is nil"} + } + var session *ssh.Session + var err error + + session, err = s.client.NewSession() + if err != nil { + return "", ConnectionError{error: err, message: fmt.Sprintf("failed to create new session, server: %s", s.server)} + } + defer session.Close() + var b bytes.Buffer + var e bytes.Buffer + session.Stdout = &b + session.Stderr = &e + err = session.Run(cmd) + output := b.String() + azureLogger := GetAzureLogger(s.ctx) + azureLogger.logr.V(1).Info("Running cmd on server", "cmd", cmd, "server", s.server) + if strings.Contains(e.String(), "Permission denied") { + err = PermissionDenied{error: fmt.Errorf("run cmd by user %s permission denied", s.username), message: CleanOutput(e.String())} + azureLogger.Warning(err, "Running cmd permission denied", "cmd", cmd, "server", s.server, "output", CleanOutput(e.String()), "username", s.username) + return "", err + } + if err != nil { + if exitError, ok := err.(*ssh.ExitError); ok { + if exitError.ExitStatus() == 1 && strings.Contains(cmd, "grep") { + // for a grep command, exit code = 1 means no lines were returned + // https://linuxcommand.org/lc3_man_pages/grep1.html + return "", nil + } + } + azureLogger.Warning(err, "Running cmd on server failed", "cmd", cmd, "server", s.server, "output", e.String()) + return "", toSshError(err, &e) + } + + return output, nil +} + +func (s *linuxServer) Close() error { + if s.client == nil { + return nil + } + return s.client.Close() +} + +func (s *linuxServer) Read(location string) (io.ReaderAt, os.FileInfo, error) { + _, _, err := s.client.SendRequest("keepalive", false, nil) + if err != nil { + return nil, nil, err + } + client, err := sftp.NewClient(s.client) + if err != nil { + return nil, nil, ConnectionError{error: err, message: fmt.Sprintf("create sftp client failed, server: %s, location: %s", s.server, location)} + } + + // Read the source file + srcFile, err := client.OpenFile(location, os.O_RDONLY) + if err != nil { + return nil, nil, ConnectionError{error: err, message: fmt.Sprintf("read jar file over sftp failed, server: %s, location: %s", s.server, location)} + } + + stat, err := srcFile.Stat() + if err != nil { + return nil, nil, ConnectionError{error: err, message: fmt.Sprintf("stat jar file failed: server: %s, location: %s", s.server, location)} + } + + return srcFile, stat, nil +} + +func (s *linuxServer) FQDN() string { + return s.server +} + +func (s *linuxServer) Connect(username, password string) error { + azureLogger := GetAzureLogger(s.ctx) + var auth []ssh.AuthMethod + auth = append(auth, ssh.Password(password)) + pemBytes := []byte(password) + signer, err := ssh.ParsePrivateKey(pemBytes) + if err != nil { + //s.log.Info("failed to parse public key, using password login only", "err", err, "server", s.server) + } else { + auth = append(auth, ssh.PublicKeys(signer)) + } + cfg := &ssh.ClientConfig{ + User: username, + Auth: auth, + HostKeyCallback: s.cb, + BannerCallback: func(message string) error { + azureLogger.Info(message) + return nil + }, + Timeout: s.timeout, // connect timeout + } + + cfg.SetDefaults() + if s.keyAlgos != nil { + cfg.KeyExchanges = append(cfg.KeyExchanges, s.keyAlgos...) + } + cfg.MACs = append(cfg.MACs, "ssh-dss") + + // connect ot ssh server + connectString := fmt.Sprintf("%s:%d", s.server, s.port) + client, err := ssh.Dial("tcp", connectString, cfg) + if err != nil { + return err + } + s.mux.Lock() + defer s.mux.Unlock() + s.client = client + s.username = username + + return nil +} + +func (s *linuxServer) Username() string { + return s.username +} + +func (s *linuxServer) Client() *ssh.Client { + return s.client +} + +func toSshError(err error, output *bytes.Buffer) error { + if err == nil { + return nil + } + return &SshError{error: err, message: output.String()} +} + +func WithHostKeyCallback(callback ssh.HostKeyCallback) SshOption { + return func(s *linuxServer) { + s.cb = callback + } +} + +func WithConnectionTimeout(timeout time.Duration) SshOption { + return func(s *linuxServer) { + s.timeout = timeout + } +} + +func (s *linuxServer) String() string { + return s.server +} diff --git a/weblogic/server_discovery.go b/weblogic/server_discovery.go new file mode 100644 index 0000000..9e47afd --- /dev/null +++ b/weblogic/server_discovery.go @@ -0,0 +1,248 @@ +package weblogic + +import ( + "bufio" + "context" + "fmt" + "github.com/pkg/errors" + "golang.org/x/crypto/ssh" + "strconv" + "strings" +) + +type AuthType int32 + +type linuxServerDiscovery struct { + credentialProvider CredentialProvider + server ServerConnector + ctx context.Context +} + +func NewLinuxServerDiscovery( + ctx context.Context, + serverConnector ServerConnector, + credentialProvider CredentialProvider) ServerDiscovery { + return &linuxServerDiscovery{ + ctx: ctx, + server: serverConnector, + credentialProvider: credentialProvider, + } +} + +func (l *linuxServerDiscovery) Server() ServerConnector { + return l.server +} + +func (l *linuxServerDiscovery) Prepare() (*Credential, error) { + creds, err := l.credentialProvider.GetCredentials() + if err != nil { + return nil, CredentialError{error: err, message: "failed to get credentials"} + } + return l.connect(creds...) +} + +func (l *linuxServerDiscovery) ProcessScan() ([]WebLogicProcess, error) { + + var output string + var err error + output, err = runWithSudo(l.Server(), GetWeblogicProcessScanCmd()) + println("Scanning weblogic process ......") + if err != nil { + if exitError, ok := err.(*ssh.ExitError); ok { + if exitError.ExitStatus() == 1 { + // when ps command return empty processes, linux will return the exit status 1 + // we just ignore this kind of error + } + } else { + return nil, err + } + + } + var processes []WebLogicProcess + + if len(output) == 0 { + return processes, nil + } + + scanner := bufio.NewScanner(strings.NewReader(output)) + for scanner.Scan() { + line := scanner.Text() + if len(strings.TrimSpace(line)) == 0 { + continue + } + var process WebLogicProcess + process, err = func(line string) (WebLogicProcess, error) { + splits := strings.Fields(strings.TrimSpace(line)) + var pid int + var uid int + pid, err = strconv.Atoi(splits[0]) + if err != nil { + return nil, errors.Wrap(err, "failed to parse pid from process") + } + uid, err = strconv.Atoi(splits[1]) + if err != nil { + return nil, errors.Wrap(err, "failed to parse uid from process") + } + start := 2 + for _, split := range splits[start:] { + if strings.HasSuffix(split, JavaCmd) { + break + } + start++ + } + if start >= len(splits) { + return nil, errors.New("cannot locate java command in scanned process options") + } + return &javaProcess{ + pid: pid, + uid: uid, + javaCmd: splits[start], + options: splits[start+1:], + executor: l, + }, nil + }(line) + if err != nil { + return nil, err + } + processes = append(processes, process) + } + return processes, nil +} + +func (l *linuxServerDiscovery) GetTotalMemory() (int64, error) { + output, err := runWithSudo(l.server, GetTotalMemoryCmd()) + if err != nil { + return 0, err + } + if len(output) == 0 { + return 0, errors.New("failed to get total memory, output is empty") + } + + size, err := strconv.ParseFloat(CleanOutput(output), 64) + if err != nil { + return 0, errors.Wrap(err, fmt.Sprintf("unable to parse total memory, output is %s", output)) + } + + return int64(size * KiB), nil +} + +func (l *linuxServerDiscovery) getChecksum(absolutePath string) (string, error) { + azureLogger := GetAzureLogger(l.ctx) + output, err := runWithSudo(l.server, GetSha256Cmd(absolutePath)) + if err != nil || len(output) == 0 { + azureLogger.Info("cannot get sha256 checksum", "absolutePath", absolutePath, "err", err) + return "", nil + } + return CleanOutput(output), nil +} + +func (l *linuxServerDiscovery) GetOsName() (string, error) { + azureLogger := GetAzureLogger(l.ctx) + var tryOsRelease tryFunc[ServerConnector, string] = func(in ServerConnector) (string, bool) { + output, err := runWithSudo(in, GetOsName()) + if err != nil { + azureLogger.Warning(err, "cannot get os name", "output", output) + } + return output, len(output) > 0 + } + var tryCentOsRelease tryFunc[ServerConnector, string] = func(in ServerConnector) (string, bool) { + output, err := runWithSudo(in, GetOracleOsName()) + if err != nil { + azureLogger.Warning(err, "cannot get cent os name", "output", output) + } + return output, len(output) > 0 + } + + output, found := tryFuncs[ServerConnector, string]{tryOsRelease, tryCentOsRelease}.try(l.server) + if found { + return CleanOutput(output), nil + } + + return "", nil +} + +func (l *linuxServerDiscovery) GetOsVersion() (string, error) { + azureLogger := GetAzureLogger(l.ctx) + var tryOsRelease tryFunc[ServerConnector, string] = func(in ServerConnector) (string, bool) { + output, err := runWithSudo(in, GetOsVersion()) + if err != nil { + azureLogger.Debug("cannot get os version", "err", err, "output", output) + } + return output, len(output) > 0 + } + var tryCentOsRelease tryFunc[ServerConnector, string] = func(in ServerConnector) (string, bool) { + output, err := runWithSudo(in, GetOracleOsVersion()) + if err != nil { + azureLogger.Debug("cannot get cent os version", "err", err, "output", output) + } + return output, len(output) > 0 + } + + output, found := tryFuncs[ServerConnector, string]{tryOsRelease, tryCentOsRelease}.try(l.server) + if found { + return CleanOutput(output), nil + } + + return "", nil +} + +func (l *linuxServerDiscovery) Finish() error { + return l.Server().Close() +} + +func sudo(command string) string { + return "sudo " + command +} + +func (l *linuxServerDiscovery) connect(creds ...*Credential) (*Credential, error) { + azureLogger := GetAzureLogger(l.ctx) + length := len(creds) + if length == 0 { + return nil, CredentialError{error: fmt.Errorf("credentials are empty"), message: ""} + } + + s := FromSlice[*Credential](l.ctx, creds) + + results, _ := + ToSlice[loginResult]( + s.Map(func(cred *Credential) loginResult { + err := l.server.Connect(cred.Username, cred.Password) + return loginResult{cred: cred, err: err} + }), + ) + + var err error + for _, result := range results { + if result.err != nil { + if isAuthFailure(result.err) { + err = CredentialError{error: result.err, message: fmt.Sprintf("bad credential: %s", result.cred.Username)} + } else { + err = ConnectionError{error: result.err, message: fmt.Sprintf("failed connect to %s", l.server.FQDN())} + } + continue + } + return result.cred, nil + } + + if err != nil { + azureLogger.Warning(err, "error to connect to server with credential", "server", l.server.FQDN()) + return nil, err + } + + return nil, CredentialError{ + error: errors.New(fmt.Sprintf("cannot connect to server %s", l.server)), + message: fmt.Sprintf("tried all credentials, but still cannot connect to server: %s", l.server), + } +} + +func isAuthFailure(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), "ssh: unable to authenticate") +} + +type loginResult struct { + cred *Credential + err error +} diff --git a/weblogic/stream.go b/weblogic/stream.go new file mode 100644 index 0000000..cc01b7c --- /dev/null +++ b/weblogic/stream.go @@ -0,0 +1,522 @@ +package weblogic + +import ( + "bytes" + "context" + "fmt" + "golang.org/x/sync/errgroup" + "reflect" + "runtime" + "sort" + "strconv" + "strings" + "sync" + "time" +) + +type unaryFuncType int + +const ( + unsupported unaryFuncType = iota + zeroInSingleOut // f() any + zeroInErrorOut // f() (any, error) + singleInZeroOut // f(any) + singleInSingleOut // f(any) any + singleInErrorOut // f(any) (any, error) + contextInZeroOut // f(context, any) + contextInSingleOut // f(context, any) any + contextInErrorOut // f(context, any) (any, error) +) + +var stopped = stop{} + +func FromSlice[T any](ctx context.Context, slice []T) Stream { + return stream(func(consumer consumer) error { + for _, i := range slice { + err := consumer(ctx, i) + if err != nil { + if err == stopped { + break + } + return err + } + } + return nil + }) +} + +func IntComparator() Comparator[int] { + return func(a, b int) int { + return a - b + } +} + +func StringComparator() Comparator[string] { + return func(a, b string) int { + return strings.Compare(a, b) + } +} + +func IntSum() Combinator[any] { + return func(a, b any) any { + return a.(int) + b.(int) + } +} + +func StringJoiner(sep string) Combinator[any] { + return func(a, b any) any { + return a.(string) + sep + b.(string) + } +} + +type Stream interface { + consume(consumer consumer) error + ForEach(f interface{}) error + Peek(f interface{}) Stream + Map(f interface{}) Stream + FlatMap(f interface{}) Stream + Distinct() Stream + Filter(f Predicate) Stream + Join(sep string) (string, error) + Retry(policy RetryPolicy) Stream + Take(n int) Stream + First() (any, error) + Sorted(less interface{}) Stream + Reduce(seed any, c Combinator[any]) (any, error) + Parallel(parallelism int) Stream + GroupBy(keyFunc func(t any) string, combinator Combinator[any]) (map[string]any, error) +} + +type stop struct { +} + +func (s stop) Error() string { + return "stopped" +} + +type Comparator[T comparable] func(a, b T) int + +type Combinator[T any] func(a, b T) T + +type Predicate func(t any) bool + +type consumer func(ctx context.Context, t any) error + +func (c consumer) andThen(other consumer) consumer { + return func(ctx context.Context, t any) error { + err := c(ctx, t) + if err != nil { + return err + } + err = other(ctx, t) + if err != nil { + return err + } + return nil + } +} + +type stream func(consumer consumer) error + +type RetryPolicy struct { + max int + interval time.Duration +} + +func (s stream) consume(c consumer) error { + return s(c) +} + +func (s stream) ForEach(f interface{}) error { + fc := toCall(f) + return s.consume(func(ctx context.Context, t any) error { + _, err := fc.call(ctx, t) + return err + }) +} + +func (s stream) Peek(f interface{}) Stream { + fc := toCall(f) + c := consumer(func(ctx context.Context, t any) error { + _, err := fc.call(ctx, t) + return err + }) + return stream(func(other consumer) error { + return s.consume(c.andThen(other)) + }) +} + +func (s stream) Map(f interface{}) Stream { + fc := toCall(f) + return stream(func(consumer consumer) error { + return s.consume(func(ctx context.Context, t any) error { + val, err := fc.call(ctx, t) + if err != nil && err != stopped { + return err + } + return consumer(ctx, val.Interface()) + }) + }) +} + +func (s stream) FlatMap(f interface{}) Stream { + fc := toCall(f) + return stream(func(consumer consumer) error { + return s.consume(func(ctx context.Context, t any) error { + val, err := fc.call(ctx, t) + if err != nil { + return err + } + return val.Interface().(Stream).consume(consumer) + }) + }) +} + +func (s stream) Distinct() Stream { + return stream(func(consumer consumer) error { + var m = make(map[any]bool) + var mux sync.Mutex + return s.consume(func(ctx context.Context, t any) error { + mux.Lock() + _, exists := m[t] + if !exists { + m[t] = true + } + mux.Unlock() + if !exists { + return consumer(ctx, t) + } + return nil + }) + }) +} + +func (s stream) Filter(f Predicate) Stream { + return stream(func(consumer consumer) error { + return s.consume(func(ctx context.Context, t any) error { + if f(t) { + return consumer(ctx, t) + } + return nil + }) + }) +} + +func (s stream) Join(sep string) (string, error) { + var slice []string + var mux sync.Mutex + err := s.ForEach(func(t any) { + mux.Lock() + defer mux.Unlock() + slice = append(slice, fmt.Sprintf("%v", t)) + }) + if err != nil && err != stopped { + return "", err + } + + return strings.Join(slice, sep), nil +} + +func (s stream) Retry(policy RetryPolicy) Stream { + return stream(func(consumer consumer) error { + return s.consume(func(ctx context.Context, t any) error { + var err error + for i := 0; i <= policy.max; i++ { + err = consumer(ctx, t) + if err == nil || err == stopped { + break + } + time.Sleep(policy.interval) + } + return err + }) + }) +} + +func (s stream) Take(n int) Stream { + return stream(func(consumer consumer) error { + i := n + var mux sync.Mutex + return s.consume(func(ctx context.Context, t any) error { + shouldConsume := false + mux.Lock() + i-- + shouldConsume = i >= 0 + mux.Unlock() + if shouldConsume { + return consumer(ctx, t) + } else { + return stopped + } + }) + }) +} + +func (s stream) First() (any, error) { + var result any + var mux sync.Mutex + err := s.consume(func(ctx context.Context, t any) error { + mux.Lock() + defer mux.Unlock() + result = t + return stopped + }) + if err != nil && err != stopped { + return nil, err + } + return result, nil +} + +func (s stream) Sorted(less interface{}) Stream { + return stream(func(consumer consumer) error { + var slice []any + var ctx context.Context + var mux sync.Mutex + _ = s.consume(func(context context.Context, t any) error { + mux.Lock() + defer mux.Unlock() + slice = append(slice, t) + ctx = context + return nil + }) + sortSlice(slice, less) + return FromSlice(ctx, slice).consume(consumer) + + }) +} + +func (s stream) Reduce(initial any, c Combinator[any]) (any, error) { + var result = initial + var mux sync.Mutex + err := s.ForEach(func(t any) { + mux.Lock() + defer mux.Unlock() + result = c(result, t) + }) + if err != nil && err != stopped { + return nil, err + } + return result, nil +} + +func (s stream) Parallel(parallelism int) Stream { + return stream(func(consumer consumer) error { + errs, _ := errgroup.WithContext(context.Background()) + errs.SetLimit(parallelism) + err := s(func(ctx context.Context, t any) error { + errs.Go(func() error { + return consumer(ctx, t) + }) + return nil + }) + + if err != nil { + return err + } + + return errs.Wait() + }) +} + +func (s stream) GroupBy(keyFunc func(t any) string, combinator Combinator[any]) (map[string]any, error) { + var m = make(map[string]any) + var mux sync.Mutex + err := s.consume(func(ctx context.Context, t any) error { + mux.Lock() + defer mux.Unlock() + key := keyFunc(t) + if existed, exists := m[key]; exists { + m[key] = combinator(existed, t) + } else { + m[key] = t + } + return nil + }) + + if err != nil && err != stopped { + return nil, err + } + return m, nil +} + +func isUnaryFunc(refType reflect.Type) unaryFuncType { + if refType.Kind() != reflect.Func { + return unsupported + } + + switch refType.NumIn() { + case 0: + switch refType.NumOut() { + case 0: + return unsupported + case 1: + return zeroInSingleOut + case 2: + if isError(refType.Out(1)) { + return zeroInErrorOut + } + return unsupported + } + case 1: + switch refType.NumOut() { + case 0: + return singleInZeroOut + case 1: + return singleInSingleOut + case 2: + if isError(refType.Out(1)) { + return singleInErrorOut + } + return unsupported + } + case 2: + param0 := refType.In(0) + if param0.Kind() != reflect.Interface { + return unsupported + } + switch refType.NumOut() { + case 0: + return contextInZeroOut + case 1: + return contextInSingleOut + case 2: + if isError(refType.Out(1)) { + return contextInErrorOut + } + return unsupported + } + } + + return unsupported +} + +func callUnaryFunc(ctx context.Context, refVal reflect.Value, data interface{}, typ unaryFuncType) (reflect.Value, error) { + var result reflect.Value + switch typ { + case unsupported: + return result, fmt.Errorf("unsupported function " + refVal.String()) + case zeroInSingleOut: + out := refVal.Call(toReflectParams()) + if hasError(out[0]) { + return result, out[0].Interface().(error) + } + return out[0], nil + case zeroInErrorOut: + out := refVal.Call(toReflectParams()) + if hasError(out[1]) { + return result, out[1].Interface().(error) + } + return out[0], nil + case singleInZeroOut: + refVal.Call(toReflectParams(data)) + case singleInSingleOut: + out := refVal.Call(toReflectParams(data)) + if hasError(out[0]) { + return result, out[0].Interface().(error) + } + return out[0], nil + case singleInErrorOut: + out := refVal.Call(toReflectParams(data)) + if out[1].Interface() == nil { + return out[0], nil + } + return result, out[1].Interface().(error) + case contextInZeroOut: + refVal.Call(toReflectParams(ctx, data)) + case contextInSingleOut: + out := refVal.Call(toReflectParams(ctx, data)) + if hasError(out[0]) { + return result, out[0].Interface().(error) + } + return out[0], nil + case contextInErrorOut: + out := refVal.Call(toReflectParams(ctx, data)) + if out[1].Interface() == nil { + return out[0], nil + } + return result, out[1].Interface().(error) + } + + return result, nil +} + +func ToSlice[T any](s Stream) ([]T, error) { + var slice []T + var err error + var mux sync.Mutex + err = s.consume(func(ctx context.Context, t any) error { + mux.Lock() + defer mux.Unlock() + slice = append(slice, t.(T)) + return nil + }) + if err != nil && err != stopped { + return nil, err + } + return slice, nil +} + +func PolicyOf(max int, interval time.Duration) RetryPolicy { + return RetryPolicy{max: max, interval: interval} +} + +func getGID() uint64 { + b := make([]byte, 64) + b = b[:runtime.Stack(b, true)] + b = bytes.TrimPrefix(b, []byte("goroutine ")) + b = b[:bytes.IndexByte(b, ' ')] + n, _ := strconv.ParseUint(string(b), 10, 64) + return n +} + +func sortSlice(slice []any, less interface{}) { + sort.Slice(slice, func(i, j int) bool { + ret := reflect.ValueOf(less).Call( + []reflect.Value{ + reflect.ValueOf(slice[i]), + reflect.ValueOf(slice[j]), + }, + ) + return ret[0].Interface().(int) < 0 + }) +} + +type funcCall struct { + refTyp unaryFuncType + refVal reflect.Value +} + +func toCall(f interface{}) funcCall { + return funcCall{ + refTyp: isUnaryFunc(reflect.TypeOf(f)), + refVal: reflect.ValueOf(f), + } +} + +func (c funcCall) call(ctx context.Context, t any) (reflect.Value, error) { + return callUnaryFunc(ctx, c.refVal, t, c.refTyp) +} + +func ignoreStopped(err error) error { + if err == stopped { + return nil + } + return err +} + +func isError(typ reflect.Type) bool { + return typ == reflect.TypeOf((*error)(nil)).Elem() +} + +func hasError(value reflect.Value) bool { + return isError(value.Type()) && !value.IsNil() +} + +func toReflectParams(params ...any) []reflect.Value { + var vals []reflect.Value + for _, t := range params { + vals = append(vals, reflect.ValueOf(t)) + } + + return vals +} diff --git a/weblogic/util.go b/weblogic/util.go new file mode 100644 index 0000000..16c9aa4 --- /dev/null +++ b/weblogic/util.go @@ -0,0 +1,63 @@ +package weblogic + +import ( + "fmt" + "regexp" + "strings" +) + +func CleanOutput(raw string) string { + var value = raw + value = strings.TrimSpace(value) + value = strings.TrimSuffix(value, "\r\n") + value = strings.TrimSuffix(value, "\n") + value = strings.ReplaceAll(value, "\"", "") + value = strings.ReplaceAll(value, "'", "") + return value +} + +func Contains[T ~string](slices []T, find T) bool { + for _, t := range slices { + if t == find { + return true + } + } + return false +} + +type tryFunc[In any, Out any] func(in In) (Out, bool) + +type tryFuncs[In any, Out any] []tryFunc[In, Out] + +func (ts tryFuncs[In, Out]) try(in In) (Out, bool) { + var zero Out + for _, f := range ts { + if value, ok := f(in); ok { + return value, true + } + } + + return zero, false +} + +type cmdMatcher struct { + cmd string +} + +func (c cmdMatcher) Matches(x interface{}) bool { + s := x.(string) + pattern := regexp.QuoteMeta(c.cmd) + pattern = strings.ReplaceAll(pattern, "%%d", "^^d") // a dirty trick, we need to replace %% to ^^ to keep it as-is + pattern = strings.ReplaceAll(pattern, "%d", "[0-9]+") + pattern = strings.ReplaceAll(pattern, "%\\[1\\]d", "[0-9]+") + pattern = strings.ReplaceAll(pattern, "%f", "[0-9\\.]+") + pattern = strings.ReplaceAll(pattern, "%s", "[0-9a-zA-Z\\-_\\./]+") + pattern = strings.ReplaceAll(pattern, "^^d", "%d") + r := regexp.MustCompile(pattern) + find := r.FindString(s) + return strings.Compare(s, find) == 0 +} + +func (c cmdMatcher) String() string { + return fmt.Sprintf("has cmd: %s", c.cmd) +} diff --git a/weblogic/weblogic-deploy.zip b/weblogic/weblogic-deploy.zip new file mode 100644 index 0000000..321b40e Binary files /dev/null and b/weblogic/weblogic-deploy.zip differ diff --git a/weblogiccli/main.go b/weblogiccli/main.go new file mode 100644 index 0000000..8beefd6 --- /dev/null +++ b/weblogiccli/main.go @@ -0,0 +1,108 @@ +package main + +import ( + "context" + "flag" + "fmt" + "github.com/Azure/discover-java-apps/weblogic" + "github.com/go-logr/logr" + "github.com/go-logr/zapr" + "os" + "time" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func main() { + var server string + var port int + var username string + var password string + var filename string + var format string + var weblogicusername string + var weblogicpassword string + var weblogicport int + flag.StringVar(&server, "server", "", "Target server to be discovered") + flag.StringVar(&username, "username", "", "Username for ssh login") + flag.StringVar(&password, "password", "", "Password for ssh login") + flag.IntVar(&port, "port", 22, "The ssh port, default 22") + flag.StringVar(&weblogicusername, "weblogicusername", "weblogic", "Username for weblogic login") + flag.StringVar(&weblogicpassword, "weblogicpassword", "", "Password for weblogic login") + flag.IntVar(&weblogicport, "weblogicport", 7001, "The weblogic port, default 7001") + + flag.StringVar(&filename, "file", "", "File name for result, default console") + flag.StringVar(&format, "format", "json", "Output format, default json") + flag.Parse() + cfg := &zap.Config{ + Encoding: "console", + Level: zap.NewAtomicLevelAt(zapcore.DebugLevel), + OutputPaths: []string{"weblogic.log"}, + ErrorOutputPaths: []string{"weblogic.log"}, + EncoderConfig: zapcore.EncoderConfig{ + MessageKey: "message", + LevelKey: "level", + EncodeLevel: zapcore.CapitalLevelEncoder, + TimeKey: "time", + EncodeTime: zapcore.ISO8601TimeEncoder, + CallerKey: "caller", + EncodeCaller: zapcore.ShortCallerEncoder, + EncodeDuration: zapcore.MillisDurationEncoder, + }, + } + logger, _ := cfg.Build() + ctx := logr.NewContext(context.Background(), zapr.NewLogger(logger)) + azureLogger := weblogic.GetAzureLogger(ctx, map[string]string{ + "server": server, + }) + + output, err := NewOutput(filename, format) + if err != nil { + azureLogger.Error(err, "error when creating output", "filename", filename) + os.Exit(1) + } + + var weblogicServerConnectInfo = weblogic.ServerConnectionInfo{ + Server: server, + Port: port, + } + + DoWebLogicDiscovery(ctx, weblogicServerConnectInfo, WebLogicNewUsernamePasswordCredentialProvider(username, password, weblogicusername, weblogicpassword, weblogicport), output) +} + +func DoWebLogicDiscovery(ctx context.Context, info weblogic.ServerConnectionInfo, credentialProvider weblogic.CredentialProvider, output *Output) { + println("Discovering weblogic apps ::start ------------------------------------------------------") + azureLogger := weblogic.GetAzureLogger(ctx) + var executor = weblogic.NewWeblogicDiscoveryExecutor( + credentialProvider, + weblogic.DefaultServerConnectorFactory( + weblogic.WithConnectionTimeout(time.Duration(5)*time.Second), + weblogic.WithHostKeyCallback(MemoryHostKeyCallbackFunction()), + ), + ) + + apps, err := executor.Discover(ctx, info) + + if err != nil { + azureLogger.Error(err, "failed to discover") + fmt.Println("Error occurred during discovery, please check weblogic.log, any issue could report to https://github.com/Azure/azure-discovery-java-apps/issues") + os.Exit(1) + } + + if len(apps) == 0 { + fmt.Print("no weblogic app discovered from " + info.Server) + os.Exit(0) + } + // this is used to append the result + var converter = NewWeblogicAppConverter() + var weblogicCliApp = converter.Convert(apps) + + if err = output.Write(weblogicCliApp); err != nil { + azureLogger.Error(err, "error when write to target file") + fmt.Println("Error occurred while writing to file, please check weblogic.log, any issue could report to https://github.com/Azure/azure-discovery-java-apps/issues") + os.Exit(1) + } + println("\n\nDiscovering weblogic apps ::end ------------------------------------------------------") + +} diff --git a/weblogiccli/output.go b/weblogiccli/output.go new file mode 100644 index 0000000..9de7e54 --- /dev/null +++ b/weblogiccli/output.go @@ -0,0 +1,170 @@ +package main + +import ( + "bytes" + "encoding/csv" + "encoding/json" + "fmt" + "io" + "os" + "reflect" + "strconv" + "strings" + "time" +) + +type Output struct { + writer io.Writer + format string +} + +type FieldWithTag struct { + name string + tag string +} + +type FieldWithTags []FieldWithTag + +func (f FieldWithTags) headers() []string { + var headers []string + for _, fwt := range f { + if len(fwt.tag) == 0 { + headers = append(headers, fwt.name) + } else { + headers = append(headers, fwt.tag) + } + } + return headers +} + +func (f FieldWithTags) fields() []string { + var fields []string + for _, fwt := range f { + fields = append(fields, fwt.name) + } + return fields +} + +func NewOutput(filename string, format string) (*Output, error) { + var writer io.Writer + var err error + if len(filename) == 0 { + writer = os.Stdout + } else { + writer, err = fileWriter(filename) + if err != nil { + return nil, err + } + } + return &Output{writer: writer, format: format}, nil +} + +func fileWriter(filename string) (io.Writer, error) { + file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return nil, err + } + return file, nil +} + +func (o *Output) Write(records any) error { + var err error + switch strings.ToLower(strings.TrimSpace(o.format)) { + case "": + case "json": + err = o.writeJson(records, o.writer) + case "csv": + err = o.writCSV(records, o.writer) + } + return err +} + +func (o *Output) writeJson(records any, writer io.Writer) error { + b, err := json.Marshal(records) + if err != nil { + return err + } + + var out bytes.Buffer + err = json.Indent(&out, b, "", " ") + if err != nil { + return err + } + + _, err = writer.Write(out.Bytes()) + if err != nil { + return err + } + return nil +} + +func (o *Output) writCSV(records any, writer io.Writer) error { + var csvWriter = csv.NewWriter(writer) + defer csvWriter.Flush() + csvWriter.Comma = ',' + + var content [][]string + var fieldWithTags FieldWithTags + + var refTyp = reflect.TypeOf(records) + refVal := reflect.ValueOf(records) + var values []reflect.Value + switch refTyp.Kind() { + case reflect.Slice: + for i := 0; i < refVal.Len(); i++ { + if refVal.Index(i).Kind() == reflect.Ptr { + values = append(values, refVal.Index(i).Elem()) + } else { + values = append(values, refVal.Index(i)) + } + } + case reflect.Ptr: + values = append(values, refVal.Elem()) + default: + values = append(values, refVal) + } + + for i := 0; i < values[0].Type().NumField(); i++ { + field := values[0].Type().Field(i) + fieldWithTags = append(fieldWithTags, FieldWithTag{name: field.Name, tag: field.Tag.Get("csv")}) + } + content = append(content, fieldWithTags.headers()) + fields := fieldWithTags.fields() + for _, v := range values { + var row []string + for _, field := range fields { + value := v.FieldByName(field) + row = append(row, toString(value)) + } + content = append(content, row) + } + for _, record := range content { + err := csvWriter.Write(record) + if err != nil { + return err + } + } + + return nil +} + +func toString(v reflect.Value) string { + switch k := v.Kind(); k { + case reflect.Invalid: + return "" + case reflect.String: + return v.String() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.FormatInt(v.Int(), 10) + case reflect.Float32, reflect.Float64: + return fmt.Sprintf("%.2f", v.Float()) + case reflect.Bool: + return strconv.FormatBool(v.Bool()) + } + if v.Type().String() == "time.Time" { + return v.Interface().(time.Time).String() + } + // If you call String on a reflect.Value of other type, it's better to + // print something than to panic. Useful in debugging. + return "" +} diff --git a/weblogiccli/ssh.go b/weblogiccli/ssh.go new file mode 100644 index 0000000..419ed09 --- /dev/null +++ b/weblogiccli/ssh.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "golang.org/x/crypto/ssh" + "net" +) + +var publicKeyStore = make(map[string]string) + +func MemoryHostKeyCallbackFunction() ssh.HostKeyCallback { + return func(hostname string, remote net.Addr, key ssh.PublicKey) error { + fingerprint := ssh.FingerprintSHA256(key) + if stored, ok := publicKeyStore[hostname]; !ok { + publicKeyStore[hostname] = fingerprint + } else if stored != fingerprint { + return fmt.Errorf("ssh: hostkey mismatch, previous: %s, current: %s", stored, fingerprint) + } + return nil + } +} diff --git a/weblogiccli/weblogic_credential.go b/weblogiccli/weblogic_credential.go new file mode 100644 index 0000000..b377e9c --- /dev/null +++ b/weblogiccli/weblogic_credential.go @@ -0,0 +1,30 @@ +package main + +import ( + "github.com/Azure/discover-java-apps/weblogic" +) + +func WebLogicNewUsernamePasswordCredentialProvider(username, password, weblogicusername, weblogicpassword string, weblogicport int) weblogic.CredentialProvider { + return &weblogicUsernamePasswordCredentialProvider{Username: username, Password: password, WeblogicUsername: weblogicusername, WeblogicPassword: weblogicpassword, Weblogicport: weblogicport} +} + +type weblogicUsernamePasswordCredentialProvider struct { + Username string + Password string + WeblogicUsername string + WeblogicPassword string + Weblogicport int +} + +func (p weblogicUsernamePasswordCredentialProvider) GetCredentials() ([]*weblogic.Credential, error) { + return []*weblogic.Credential{ + { + Id: p.Username, + Username: p.Username, + Password: p.Password, + WeblogicUsername: p.WeblogicUsername, + WeblogicPassword: p.WeblogicPassword, + Weblogicport: p.Weblogicport, + }, + }, nil +} diff --git a/weblogiccli/weblogic_model.go b/weblogiccli/weblogic_model.go new file mode 100644 index 0000000..a87a724 --- /dev/null +++ b/weblogiccli/weblogic_model.go @@ -0,0 +1,66 @@ +package main + +import ( + "github.com/Azure/discover-java-apps/weblogic" + "time" +) + +type WeblogicCliApp struct { + Server string `json:"server" csv:"Server"` + AppName string `json:"appName" csv:"AppName"` + AppType string `json:"appType" csv:"AppType"` + AppPort int `json:"appPort" csv:"AppPort"` + JvmMemory int64 `json:"jvmMemoryInMB" csv:"JvmHeapMemory(MB)"` + OsName string `json:"osName" csv:"OsName"` + OsVersion string `json:"osVersion" csv:"OsVersion"` + JarFileLocation string `json:"jarFileLocation" csv:"JarFileLocation"` + LastModifiedTime string `json:"lastModifiedTime" csv:"JarFileModifiedTime"` + WeblogicVersion string `json:"weblogicVersion" csv:"WeblogicVersion"` + WeblogicPatches string `json:"weblogicPatches" csv:"WeblogicPatches"` + DeploymentTarget string `json:"deploymentTarget" csv:"DeploymentTarget"` + RuntimeJdkVersion string `json:"runtimeJdkVersion" csv:"RuntimeJdkVersion"` + ServerType string `json:"serverType" csv:"ServerType"` + OracleHome string `json:"oracleHome" csv:"OracleHome"` + DomainHome string `json:"domainHome" csv:"DomainHome"` +} + +type weblogicAppConverter struct { +} + +type Converter[From any, To any] interface { + Convert(from From) To +} + +func NewWeblogicAppConverter() Converter[[]*weblogic.WeblogicApp, []*WeblogicCliApp] { + return &weblogicAppConverter{} +} + +func (s weblogicAppConverter) Convert(apps []*weblogic.WeblogicApp) []*WeblogicCliApp { + var results []*WeblogicCliApp + + for _, app := range apps { + + var appType = "war" + + results = append(results, &WeblogicCliApp{ + Server: app.Server, + OsName: app.OsName, + OsVersion: app.OsVersion, + JvmMemory: app.JvmMemoryInMB / weblogic.MiB, + DeploymentTarget: app.DeploymentTarget, + AppName: app.AppName, + AppType: appType, + ServerType: app.ServerType, + AppPort: app.AppPort, + WeblogicVersion: app.WeblogicVersion, + WeblogicPatches: app.WeblogicPatches, + JarFileLocation: app.AppFileLocation, + RuntimeJdkVersion: app.RuntimeJdkVersion, + OracleHome: app.OracleHome, + DomainHome: app.DomainHome, + + LastModifiedTime: app.LastModifiedTime.UTC().Format(time.RFC3339), + }) + } + return results +}