first commit

This commit is contained in:
2024-10-28 23:04:48 +01:00
commit 1ee55157f1
911 changed files with 325331 additions and 0 deletions

77
cmd/chkmqhealthy/main.go Normal file
View File

@@ -0,0 +1,77 @@
/*
© Copyright IBM Corporation 2017, 2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// chkmqhealthy checks that MQ is healthy, by checking the output of the "dspmq" command
package main
import (
"context"
"fmt"
"os"
"os/exec"
"os/signal"
"strings"
"github.com/ibm-messaging/mq-container/pkg/name"
)
func queueManagerHealthy(ctx context.Context) (bool, error) {
name, err := name.GetQueueManagerName()
if err != nil {
return false, err
}
// Specify the queue manager name, just in case someone's created a second queue manager
// #nosec G204
cmd := exec.CommandContext(ctx, "dspmq", "-n", "-m", name)
// Run the command and wait for completion
out, err := cmd.CombinedOutput()
fmt.Printf("%s", out)
if err != nil {
fmt.Println(err)
return false, err
}
readyStrings := []string{
"(RUNNING)",
"(RUNNING AS STANDBY)",
"(RECOVERY GROUP LEADER)",
"(STARTING)",
"(REPLICA)",
}
for _, checkString := range readyStrings {
if strings.Contains(string(out), checkString) {
return true, nil
}
}
return false, nil
}
func doMain() int {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer cancel()
healthy, err := queueManagerHealthy(ctx)
if err != nil {
return 2
}
if !healthy {
return 1
}
return 0
}
func main() {
os.Exit(doMain())
}

88
cmd/chkmqready/main.go Normal file
View File

@@ -0,0 +1,88 @@
/*
© Copyright IBM Corporation 2017, 2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// chkmqready checks that MQ is ready for work, by checking if the MQ listener port is available
package main
import (
"context"
"fmt"
"net"
"os"
"os/signal"
"github.com/ibm-messaging/mq-container/internal/ready"
"github.com/ibm-messaging/mq-container/pkg/name"
)
func doMain() int {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer cancel()
// Check if runmqserver has indicated that it's finished configuration
r, err := ready.Check()
if !r || err != nil {
return 1
}
name, err := name.GetQueueManagerName()
if err != nil {
fmt.Println(err)
return 1
}
// Check if the queue manager has a running listener
status, err := ready.Status(ctx, name)
if err != nil {
return 1
}
switch status {
case ready.StatusActiveQM:
portOpen, err := checkPort("127.0.0.1:1414")
if err != nil {
fmt.Println(err)
}
if !portOpen {
return 1
}
return 0
case ready.StatusRecoveryQM:
fmt.Printf("Detected queue manager running as recovery leader")
return 0
case ready.StatusStandbyQM:
fmt.Printf("Detected queue manager running in standby mode")
return 10
case ready.StatusReplicaQM:
fmt.Printf("Detected queue manager running in replica mode")
return 20
default:
return 1
}
}
func main() {
os.Exit(doMain())
}
func checkPort(address string) (portOpen bool, err error) {
var conn net.Conn
conn, err = net.Dial("tcp", address)
if err != nil {
return
}
portOpen = true
err = conn.Close()
return
}

167
cmd/chkmqstarted/main.go Normal file
View File

@@ -0,0 +1,167 @@
/*
© Copyright IBM Corporation 2021, 2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// chkmqstarted checks that MQ has successfully started, by checking the output of the "dspmq" command
package main
import (
"context"
"fmt"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"time"
"github.com/ibm-messaging/mq-container/internal/ready"
"github.com/ibm-messaging/mq-container/pkg/name"
)
func queueManagerStarted(ctx context.Context) (bool, error) {
name, err := name.GetQueueManagerName()
if err != nil {
return false, err
}
readyStrings := []string{
"(RUNNING)",
"(RUNNING AS STANDBY)",
"(RECOVERY GROUP LEADER)",
"(STARTING)",
"(REPLICA)",
}
// For Native-HA only, check if the queue manager instance is in-sync with one or more replicas
// - If not in-sync within the expected time period, revert to checking on queue manager 'ready' status
// - This ensures we do not block indefinitely for breaking changes (i.e. protocol changes)
if os.Getenv("MQ_NATIVE_HA") == "true" {
// Check if the Native-HA queue manager instance is currently in-sync
isReadyToSync, isInSync, err := isInSyncWithReplicas(ctx, name, readyStrings)
if err != nil {
return false, err
} else if isInSync {
return true, nil
}
// Check if the Native-HA queue manager instance is ready-to-sync
// - A successful queue manager 'ready' status indicates that we are ready-to-sync
if !isReadyToSync {
return false, nil
}
err = ready.SetReadyToSync()
if err != nil {
return false, err
}
// Check if the time period for checking in-sync has now expired
// - We have already confirmed a successful queue manager 'ready' status
// - Therefore the expiration of the in-sync time period will result in success
expired, err := hasInSyncTimePeriodExpired()
if err != nil {
return false, err
} else if expired {
return true, nil
}
return false, nil
}
// Specify the queue manager name, just in case someone's created a second queue manager
// #nosec G204
cmd := exec.CommandContext(ctx, "dspmq", "-n", "-m", name)
// Run the command and wait for completion
out, err := cmd.CombinedOutput()
if err != nil {
fmt.Println(err)
return false, err
}
for _, checkString := range readyStrings {
if strings.Contains(string(out), checkString) {
return true, nil
}
}
return false, nil
}
// isInSyncWithReplicas returns the in-sync status for a Native-HA queue manager instance
func isInSyncWithReplicas(ctx context.Context, name string, readyStrings []string) (bool, bool, error) {
cmd := exec.CommandContext(ctx, "dspmq", "-n", "-o", "nativeha", "-m", name)
out, err := cmd.CombinedOutput()
if err != nil {
return false, false, err
} else if strings.Contains(string(out), "INSYNC(YES)") {
return true, true, nil
}
for _, checkString := range readyStrings {
if strings.Contains(string(out), checkString) {
return true, false, nil
}
}
return false, false, nil
}
// hasInSyncTimePeriodExpired returns true if a Native-HA queue manager instance is not in-sync within the expected time period, otherwise false
func hasInSyncTimePeriodExpired() (bool, error) {
// Default timeout 5 seconds
var timeout int64 = 5
var err error
// Check if a timeout override has been set
customTimeout := os.Getenv("MQ_NATIVE_HA_IN_SYNC_TIMEOUT")
if customTimeout != "" {
timeout, err = strconv.ParseInt(customTimeout, 10, 64)
if err != nil {
return false, err
}
}
isReadyToSync, readyToSyncStartTime, err := ready.GetReadyToSyncStartTime()
if err != nil {
return false, err
}
if isReadyToSync && time.Now().Unix()-readyToSyncStartTime.Unix() >= timeout {
return true, nil
}
return false, nil
}
func doMain() int {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer cancel()
started, err := queueManagerStarted(ctx)
if err != nil {
return 2
}
if !started {
return 1
}
return 0
}
func main() {
os.Exit(doMain())
}

179
cmd/runmqdevserver/main.go Normal file
View File

@@ -0,0 +1,179 @@
/*
© Copyright IBM Corporation 2018, 2023
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"os"
"strings"
"syscall"
"github.com/ibm-messaging/mq-container/internal/copy"
"github.com/ibm-messaging/mq-container/internal/simpleauth"
"github.com/ibm-messaging/mq-container/pkg/containerruntimelogger"
"github.com/ibm-messaging/mq-container/pkg/logger"
"github.com/ibm-messaging/mq-container/pkg/name"
)
var log *logger.Logger
func getLogFormat() string {
logFormat := strings.ToLower(strings.TrimSpace(os.Getenv("MQ_LOGGING_CONSOLE_FORMAT")))
//old-style env var is used.
if logFormat == "" {
logFormat = strings.ToLower(strings.TrimSpace(os.Getenv("LOG_FORMAT")))
}
if logFormat != "" && (logFormat == "basic" || logFormat == "json") {
return logFormat
} else {
//this is the case where value is either empty string or set to something other than "basic"/"json"
logFormat = "basic"
}
return logFormat
}
func getDebug() bool {
debug := os.Getenv("DEBUG")
if debug == "true" || debug == "1" {
return true
}
return false
}
func configureLogger() error {
var err error
f := getLogFormat()
d := getDebug()
n, err := name.GetQueueManagerName()
if err != nil {
return err
}
switch f {
case "json":
log, err = logger.NewLogger(os.Stderr, d, true, n)
if err != nil {
return err
}
case "basic":
log, err = logger.NewLogger(os.Stderr, d, false, n)
if err != nil {
return err
}
default:
log, err = logger.NewLogger(os.Stdout, d, false, n)
return fmt.Errorf("invalid value for LOG_FORMAT: %v", f)
}
return nil
}
func logTerminationf(format string, args ...interface{}) {
logTermination(fmt.Sprintf(format, args...))
}
// TODO: Duplicated code
func logTermination(args ...interface{}) {
msg := fmt.Sprint(args...)
// Write the message to the termination log. This is not the default place
// that Kubernetes will look for termination information.
log.Debugf("Writing termination message: %v", msg)
// #nosec G306 - its a read by owner/s group, and pose no harm.
err := os.WriteFile("/run/termination-log", []byte(msg), 0660)
if err != nil {
log.Debug(err)
}
log.Error(msg)
}
func doMain() error {
err := configureLogger()
if err != nil {
logTermination(err)
return err
}
err = containerruntimelogger.LogContainerDetails(log)
if err != nil {
logTermination(err)
return err
}
// Initialise 10-dev.mqsc file on ephemeral volume
// #nosec G306 - its a read by owner/s group, and pose no harm.
err = os.WriteFile("/run/10-dev.mqsc", []byte(""), 0660)
if err != nil {
logTermination(err)
return err
}
// Initialise 20-dev-tls.mqsc file on ephemeral volume
// #nosec G306 - its a read by owner/s group, and pose no harm.
err = os.WriteFile("/run/20-dev-tls.mqsc", []byte(""), 0660)
if err != nil {
logTermination(err)
return err
}
// Initialise /run/qm-service-component.ini file on ephemeral volume
// #nosec G306 - its a read by owner/s group, and pose no harm.
err = os.WriteFile("/run/qm-service-component.ini", []byte(""), 0660)
if err != nil {
logTermination(err)
return err
}
// Enable mq simpleauth if MQ_CONNAUTH_USE_HTP is set true
// and either or both of MQ_APP_PASSWORD and MQ_ADMIN_PASSWORD
// environment variables specified.
enableHtPwd, set := os.LookupEnv("MQ_CONNAUTH_USE_HTP")
if set && strings.EqualFold(enableHtPwd, "true") {
err := copy.CopyFile("/etc/mqm/qm-service-component.ini.default", "/run/qm-service-component.ini")
if err != nil {
logTermination(err)
return err
}
err = simpleauth.CheckForPasswords(log)
if err != nil {
logTermination(err)
return err
}
}
err = updateMQSC(set)
if err != nil {
logTerminationf("Error updating MQSC: %v", err)
return err
}
return nil
}
var osExit = os.Exit
func main() {
err := doMain()
if err != nil {
osExit(1)
} else {
// Replace this process with runmqserver
// #nosec G204
err = syscall.Exec("/usr/local/bin/runmqserver", []string{"runmqserver", "-nologruntime", "-dev"}, os.Environ())
if err != nil {
log.Errorf("Error replacing this process with runmqserver: %v", err)
}
}
}

View File

@@ -0,0 +1,45 @@
/*
© Copyright IBM Corporation 2018, 2023
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"os"
"github.com/ibm-messaging/mq-container/internal/mqtemplate"
)
func updateMQSC(appPasswordRequired bool) error {
var checkClient string
if appPasswordRequired {
checkClient = "REQUIRED"
} else {
checkClient = "ASQMGR"
}
const mqscLink string = "/run/10-dev.mqsc"
const mqscTemplate string = "/etc/mqm/10-dev.mqsc.tpl"
if os.Getenv("MQ_DEV") == "true" {
// Re-configure channel if app password not set
err := mqtemplate.ProcessTemplateFile(mqscTemplate, mqscLink, map[string]string{"ChckClnt": checkClient}, log)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,57 @@
/*
© Copyright IBM Corporation 2017, 2023
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"os"
"path/filepath"
)
func createVolume(dataPath string) error {
_, err := os.Stat(dataPath)
if err != nil {
if os.IsNotExist(err) {
// #nosec G301
err = os.MkdirAll(dataPath, 0755)
if err != nil {
return err
}
} else {
return err
}
}
return nil
}
// Delete files/directories from specified path
func cleanVolume(cleanPath string) error {
// #nosec G304
dirContents, err := os.ReadDir(cleanPath)
if err != nil {
if !os.IsNotExist(err) {
return err
}
}
for _, name := range dirContents {
err = os.RemoveAll(filepath.Join(cleanPath, name.Name()))
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,92 @@
/*
© Copyright IBM Corporation 2017, 2023
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"errors"
"os"
"path/filepath"
"strings"
)
// resolveLicenseFile returns the file name of the MQ license file, taking into
// account the language set by the LANG environment variable
func resolveLicenseFile() string {
lang, ok := os.LookupEnv("LANG")
if !ok {
return "English.txt"
}
switch {
case strings.HasPrefix(lang, "zh_TW"):
return "Chinese_TW.txt"
case strings.HasPrefix(lang, "zh"):
return "Chinese.txt"
// Differentiate Czech (cs) and Kashubian (csb)
case strings.HasPrefix(lang, "cs") && !strings.HasPrefix(lang, "csb"):
return "Czech.txt"
case strings.HasPrefix(lang, "fr"):
return "French.txt"
case strings.HasPrefix(lang, "de"):
return "German.txt"
case strings.HasPrefix(lang, "el"):
return "Greek.txt"
case strings.HasPrefix(lang, "id"):
return "Indonesian.txt"
case strings.HasPrefix(lang, "it"):
return "Italian.txt"
case strings.HasPrefix(lang, "ja"):
return "Japanese.txt"
// Differentiate Korean (ko) from Konkani (kok)
case strings.HasPrefix(lang, "ko") && !strings.HasPrefix(lang, "kok"):
return "Korean.txt"
case strings.HasPrefix(lang, "lt"):
return "Lithuanian.txt"
case strings.HasPrefix(lang, "pl"):
return "Polish.txt"
case strings.HasPrefix(lang, "pt"):
return "Portugese.txt"
case strings.HasPrefix(lang, "ru"):
return "Russian.txt"
case strings.HasPrefix(lang, "sl"):
return "Slovenian.txt"
case strings.HasPrefix(lang, "es"):
return "Spanish.txt"
case strings.HasPrefix(lang, "tr"):
return "Turkish.txt"
}
return "English.txt"
}
func checkLicense() (bool, error) {
lic, ok := os.LookupEnv("LICENSE")
switch {
case ok && lic == "accept":
return true, nil
case ok && lic == "view":
file := filepath.Join("/opt/mqm/licenses", resolveLicenseFile())
// #nosec G304
buf, err := os.ReadFile(file)
if err != nil {
log.Println(err)
return false, err
}
log.Println(string(buf))
return false, nil
}
log.Println("Error: Set environment variable LICENSE=accept to indicate acceptance of license terms and conditions.")
log.Println("License agreements and information can be viewed by setting the environment variable LICENSE=view. You can also set the LANG environment variable to view the license in a different language.")
return false, errors.New("Set environment variable LICENSE=accept to indicate acceptance of license terms and conditions")
}

View File

@@ -0,0 +1,282 @@
/*
© Copyright IBM Corporation 2017
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE_2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"os"
"testing"
)
var licenseTests = []struct {
in string
out string
}{
{"en_US.UTF_8", "English.txt"},
{"en_US.ISO-8859-15", "English.txt"},
{"es_GB", "Spanish.txt"},
{"el_ES.UTF_8", "Greek.txt"},
// Cover a wide variety of valid values
{"af", "English.txt"},
{"af_ZA", "English.txt"},
{"ar", "English.txt"},
{"ar_AE", "English.txt"},
{"ar_BH", "English.txt"},
{"ar_DZ", "English.txt"},
{"ar_EG", "English.txt"},
{"ar_IQ", "English.txt"},
{"ar_JO", "English.txt"},
{"ar_KW", "English.txt"},
{"ar_LB", "English.txt"},
{"ar_LY", "English.txt"},
{"ar_MA", "English.txt"},
{"ar_OM", "English.txt"},
{"ar_QA", "English.txt"},
{"ar_SA", "English.txt"},
{"ar_SY", "English.txt"},
{"ar_TN", "English.txt"},
{"ar_YE", "English.txt"},
{"az", "English.txt"},
{"az_AZ", "English.txt"},
{"az_AZ", "English.txt"},
{"be", "English.txt"},
{"be_BY", "English.txt"},
{"bg", "English.txt"},
{"bg_BG", "English.txt"},
{"bs_BA", "English.txt"},
{"ca", "English.txt"},
{"ca_ES", "English.txt"},
{"cs", "Czech.txt"},
{"cs_CZ", "Czech.txt"},
{"csb_PL", "English.txt"},
{"cy", "English.txt"},
{"cy_GB", "English.txt"},
{"da", "English.txt"},
{"da_DK", "English.txt"},
{"de", "German.txt"},
{"de_AT", "German.txt"},
{"de_CH", "German.txt"},
{"de_DE", "German.txt"},
{"de_LI", "German.txt"},
{"de_LU", "German.txt"},
{"dv", "English.txt"},
{"dv_MV", "English.txt"},
{"el", "Greek.txt"},
{"el_GR", "Greek.txt"},
{"en", "English.txt"},
{"en_AU", "English.txt"},
{"en_BZ", "English.txt"},
{"en_CA", "English.txt"},
{"en_CB", "English.txt"},
{"en_GB", "English.txt"},
{"en_IE", "English.txt"},
{"en_JM", "English.txt"},
{"en_NZ", "English.txt"},
{"en_PH", "English.txt"},
{"en_TT", "English.txt"},
{"en_US", "English.txt"},
{"en_ZA", "English.txt"},
{"en_ZW", "English.txt"},
{"eo", "English.txt"},
{"es", "Spanish.txt"},
{"es_AR", "Spanish.txt"},
{"es_BO", "Spanish.txt"},
{"es_CL", "Spanish.txt"},
{"es_CO", "Spanish.txt"},
{"es_CR", "Spanish.txt"},
{"es_DO", "Spanish.txt"},
{"es_EC", "Spanish.txt"},
{"es_ES", "Spanish.txt"},
{"es_ES", "Spanish.txt"},
{"es_GT", "Spanish.txt"},
{"es_HN", "Spanish.txt"},
{"es_MX", "Spanish.txt"},
{"es_NI", "Spanish.txt"},
{"es_PA", "Spanish.txt"},
{"es_PE", "Spanish.txt"},
{"es_PR", "Spanish.txt"},
{"es_PY", "Spanish.txt"},
{"es_SV", "Spanish.txt"},
{"es_UY", "Spanish.txt"},
{"es_VE", "Spanish.txt"},
{"et", "English.txt"},
{"et_EE", "English.txt"},
{"eu", "English.txt"},
{"eu_ES", "English.txt"},
{"fa", "English.txt"},
{"fa_IR", "English.txt"},
{"fi", "English.txt"},
{"fi_FI", "English.txt"},
{"fo", "English.txt"},
{"fo_FO", "English.txt"},
{"fr", "French.txt"},
{"fr_BE", "French.txt"},
{"fr_CA", "French.txt"},
{"fr_CH", "French.txt"},
{"fr_FR", "French.txt"},
{"fr_LU", "French.txt"},
{"fr_MC", "French.txt"},
{"gl", "English.txt"},
{"gl_ES", "English.txt"},
{"gu", "English.txt"},
{"gu_IN", "English.txt"},
{"he", "English.txt"},
{"he_IL", "English.txt"},
{"hi", "English.txt"},
{"hi_IN", "English.txt"},
{"hr", "English.txt"},
{"hr_BA", "English.txt"},
{"hr_HR", "English.txt"},
{"hu", "English.txt"},
{"hu_HU", "English.txt"},
{"hy", "English.txt"},
{"hy_AM", "English.txt"},
{"id", "Indonesian.txt"},
{"id_ID", "Indonesian.txt"},
{"is", "English.txt"},
{"is_IS", "English.txt"},
{"it", "Italian.txt"},
{"it_CH", "Italian.txt"},
{"it_IT", "Italian.txt"},
{"ja", "Japanese.txt"},
{"ja_JP", "Japanese.txt"},
{"ka", "English.txt"},
{"ka_GE", "English.txt"},
{"kk", "English.txt"},
{"kk_KZ", "English.txt"},
{"kn", "English.txt"},
{"kn_IN", "English.txt"},
{"ko", "Korean.txt"},
{"ko_KR", "Korean.txt"},
{"kok", "English.txt"},
{"kok_IN", "English.txt"},
{"ky", "English.txt"},
{"ky_KG", "English.txt"},
{"lt", "Lithuanian.txt"},
{"lt_LT", "Lithuanian.txt"},
{"lv", "English.txt"},
{"lv_LV", "English.txt"},
{"mi", "English.txt"},
{"mi_NZ", "English.txt"},
{"mk", "English.txt"},
{"mk_MK", "English.txt"},
{"mn", "English.txt"},
{"mn_MN", "English.txt"},
{"mr", "English.txt"},
{"mr_IN", "English.txt"},
{"ms", "English.txt"},
{"ms_BN", "English.txt"},
{"ms_MY", "English.txt"},
{"mt", "English.txt"},
{"mt_MT", "English.txt"},
{"nb", "English.txt"},
{"nb_NO", "English.txt"},
{"nl", "English.txt"},
{"nl_BE", "English.txt"},
{"nl_NL", "English.txt"},
{"nn_NO", "English.txt"},
{"ns", "English.txt"},
{"ns_ZA", "English.txt"},
{"pa", "English.txt"},
{"pa_IN", "English.txt"},
{"pl", "Polish.txt"},
{"pl_PL", "Polish.txt"},
{"ps", "English.txt"},
{"ps_AR", "English.txt"},
{"pt", "Portugese.txt"},
{"pt_BR", "Portugese.txt"},
{"pt_PT", "Portugese.txt"},
{"qu", "English.txt"},
{"qu_BO", "English.txt"},
{"qu_EC", "English.txt"},
{"qu_PE", "English.txt"},
{"ro", "English.txt"},
{"ro_RO", "English.txt"},
{"ru", "Russian.txt"},
{"ru_RU", "Russian.txt"},
{"sa", "English.txt"},
{"sa_IN", "English.txt"},
{"se", "English.txt"},
{"se_FI", "English.txt"},
{"se_FI", "English.txt"},
{"se_FI", "English.txt"},
{"se_NO", "English.txt"},
{"se_NO", "English.txt"},
{"se_NO", "English.txt"},
{"se_SE", "English.txt"},
{"se_SE", "English.txt"},
{"se_SE", "English.txt"},
{"sk", "English.txt"},
{"sk_SK", "English.txt"},
{"sl", "Slovenian.txt"},
{"sl_SI", "Slovenian.txt"},
{"sq", "English.txt"},
{"sq_AL", "English.txt"},
{"sr_BA", "English.txt"},
{"sr_BA", "English.txt"},
{"sr_SP", "English.txt"},
{"sr_SP", "English.txt"},
{"sv", "English.txt"},
{"sv_FI", "English.txt"},
{"sv_SE", "English.txt"},
{"sw", "English.txt"},
{"sw_KE", "English.txt"},
{"syr", "English.txt"},
{"syr_SY", "English.txt"},
{"ta", "English.txt"},
{"ta_IN", "English.txt"},
{"te", "English.txt"},
{"te_IN", "English.txt"},
{"th", "English.txt"},
{"th_TH", "English.txt"},
{"tl", "English.txt"},
{"tl_PH", "English.txt"},
{"tn", "English.txt"},
{"tn_ZA", "English.txt"},
{"tr", "Turkish.txt"},
{"tr_TR", "Turkish.txt"},
{"tt", "English.txt"},
{"tt_RU", "English.txt"},
{"ts", "English.txt"},
{"uk", "English.txt"},
{"uk_UA", "English.txt"},
{"ur", "English.txt"},
{"ur_PK", "English.txt"},
{"uz", "English.txt"},
{"uz_UZ", "English.txt"},
{"uz_UZ", "English.txt"},
{"vi", "English.txt"},
{"vi_VN", "English.txt"},
{"xh", "English.txt"},
{"xh_ZA", "English.txt"},
{"zh", "Chinese.txt"},
{"zh_CN", "Chinese.txt"},
{"zh_HK", "Chinese.txt"},
{"zh_MO", "Chinese.txt"},
{"zh_SG", "Chinese.txt"},
{"zh_TW", "Chinese_TW.txt"},
{"zu", "English.txt"},
{"zu_ZA", "English.txt"},
}
func TestResolveLicenseFile(t *testing.T) {
for _, table := range licenseTests {
os.Setenv("LANG", table.in)
f := resolveLicenseFile()
if f != table.out {
t.Errorf("resolveLicenseFile() with LANG=%v - expected %v, got %v", table.in, table.out, f)
}
}
}

485
cmd/runmqserver/logging.go Normal file
View File

@@ -0,0 +1,485 @@
/*
© Copyright IBM Corporation 2017, 2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
"time"
"github.com/ibm-messaging/mq-container/internal/command"
"github.com/ibm-messaging/mq-container/pkg/logger"
"github.com/ibm-messaging/mq-container/pkg/mqini"
)
// var debug = false
var log *logger.Logger
var collectDiagOnFail = false
func logTerminationf(format string, args ...interface{}) {
logTermination(fmt.Sprintf(format, args...))
}
func logTermination(args ...interface{}) {
msg := fmt.Sprint(args...)
// Write the message to the termination log. This is not the default place
// that Kubernetes will look for termination information.
log.Debugf("Writing termination message: %v", msg)
// #nosec G306 - its a read by owner/s group, and pose no harm.
err := os.WriteFile("/run/termination-log", []byte(msg), 0660)
if err != nil {
log.Debug(err)
}
log.Error(msg)
if collectDiagOnFail {
logDiagnostics()
}
}
func getLogFormat() string {
logFormat := strings.ToLower(strings.TrimSpace(os.Getenv("MQ_LOGGING_CONSOLE_FORMAT")))
//old-style env var is used.
if logFormat == "" {
logFormat = strings.ToLower(strings.TrimSpace(os.Getenv("LOG_FORMAT")))
}
if logFormat != "" && (logFormat == "basic" || logFormat == "json") {
return logFormat
} else {
//this is the case where value is either empty string or set to something other than "basic"/"json"
logFormat = "basic"
}
return logFormat
}
// formatBasic formats a log message parsed from JSON, as "basic" text
func formatBasic(obj map[string]interface{}) string {
// Emulate the MQ "MessageDetail=Extended" option, by appending inserts to the message
// This is important for certain messages, where key details are only available in the extended message content
inserts := make([]string, 0)
for k, v := range obj {
if strings.HasPrefix(k, "ibm_commentInsert") {
inserts = append(inserts, fmt.Sprintf("%s(%v)", strings.Replace(k, "ibm_comment", "Comment", 1), obj[k]))
} else if strings.HasPrefix(k, "ibm_arithInsert") {
if v.(float64) != 0 {
inserts = append(inserts, fmt.Sprintf("%s(%v)", strings.Replace(k, "ibm_arith", "Arith", 1), obj[k]))
}
}
}
sort.Strings(inserts)
if len(inserts) > 0 {
return fmt.Sprintf("%s %s [%v]\n", obj["ibm_datetime"], obj["message"], strings.Join(inserts, ", "))
}
// Convert time zone information from some logs (e.g. Liberty) for consistency
obj["ibm_datetime"] = strings.Replace(obj["ibm_datetime"].(string), "+0000", "Z", 1)
// Escape any new-line characters, so that we don't get multi-line messages messing up the output
obj["message"] = strings.ReplaceAll(obj["message"].(string), "\n", "\\n")
if obj["type"] != nil && (obj["type"] == "liberty_trace") {
timeStamp := obj["ibm_datetime"]
threadID := ""
srtModuleName := ""
logLevel := ""
ibmClassName := ""
srtIbmClassName := ""
ibmMethodName := ""
message := ""
if obj["loglevel"] != nil {
//threadID is captured below
if obj["ibm_threadId"] != nil {
threadID = obj["ibm_threadId"].(string)
}
//logLevel character to be mirrored in console web server logging is decided below
logLevelTmp := obj["loglevel"].(string)
switch logLevelTmp {
case "AUDIT":
logLevel = "A"
case "INFO":
logLevel = "I"
case "EVENT":
logLevel = "1"
case "ENTRY":
logLevel = ">"
case "EXIT":
logLevel = "<"
case "FINE":
logLevel = "1"
case "FINER":
logLevel = "2"
case "FINEST":
logLevel = "3"
default:
logLevel = string(logLevelTmp[0])
}
//This is a 13 characters string present in extracted out of module node
if obj["module"] != nil {
srtModuleNameArr := strings.Split(obj["module"].(string), ".")
arrLen := len(srtModuleNameArr)
srtModuleName = srtModuleNameArr[arrLen-1]
if len(srtModuleName) > 13 {
srtModuleName = srtModuleName[0:13]
}
}
if obj["ibm_className"] != nil {
ibmClassName = obj["ibm_className"].(string)
//A 13 character string is extracted from class name. This is required for FINE, FINER & FINEST log lines
ibmClassNameArr := strings.Split(ibmClassName, ".")
arrLen := len(ibmClassNameArr)
srtIbmClassName = ibmClassNameArr[arrLen-1]
if len(srtModuleName) > 13 {
srtIbmClassName = srtIbmClassName[0:13]
}
}
if obj["ibm_methodName"] != nil {
ibmMethodName = obj["ibm_methodName"].(string)
}
if obj["message"] != nil {
message = obj["message"].(string)
}
//For AUDIT & INFO logging
if logLevel == "A" || logLevel == "I" {
return fmt.Sprintf("%s %s %-13s %s %s %s %s\n", timeStamp, threadID, srtModuleName, logLevel, ibmClassName, ibmMethodName, message)
}
//For EVENT logLevel
if logLevelTmp == "EVENT" {
return fmt.Sprintf("%s %s %-13s %s %s\n", timeStamp, threadID, srtModuleName, logLevel, message)
}
//For ENTRY & EXIT
if logLevel == ">" || logLevel == "<" {
return fmt.Sprintf("%s %s %-13s %s %s %s\n", timeStamp, threadID, srtModuleName, logLevel, ibmMethodName, message)
}
//For deeper log levels
if logLevelTmp == "FINE" || logLevel == "2" || logLevel == "3" {
return fmt.Sprintf("%s %s %-13s %s %s %s %s\n", timeStamp, threadID, srtIbmClassName, logLevel, ibmClassName, ibmMethodName, message)
}
}
}
return fmt.Sprintf("%s %s\n", obj["ibm_datetime"], obj["message"])
}
// mirrorSystemErrorLogs starts a goroutine to mirror the contents of the MQ system error logs
func mirrorSystemErrorLogs(ctx context.Context, wg *sync.WaitGroup, mf mirrorFunc) (chan error, error) {
// Always use the JSON log as the source
return mirrorLog(ctx, wg, "/var/mqm/errors/AMQERR01.json", false, mf, false)
}
// mirrorMQSCLogs starts a goroutine to mirror the contents of the auto-config mqsc logs
func mirrorMQSCLogs(ctx context.Context, wg *sync.WaitGroup, name string, mf mirrorFunc) (chan error, error) {
qm, err := mqini.GetQueueManager(name)
if err != nil {
log.Debug(err)
return nil, err
}
f := filepath.Join(mqini.GetErrorLogDirectory(qm), "autocfgmqsc.LOG")
return mirrorLog(ctx, wg, f, true, mf, false)
}
// mirrorQueueManagerErrorLogs starts a goroutine to mirror the contents of the MQ queue manager error logs
func mirrorQueueManagerErrorLogs(ctx context.Context, wg *sync.WaitGroup, name string, fromStart bool, mf mirrorFunc) (chan error, error) {
// Always use the JSON log as the source
qm, err := mqini.GetQueueManager(name)
if err != nil {
log.Debug(err)
return nil, err
}
f := filepath.Join(mqini.GetErrorLogDirectory(qm), "AMQERR01.json")
return mirrorLog(ctx, wg, f, fromStart, mf, true)
}
// mirrorMQSimpleAuthLogs starts a goroutine to mirror the contents of the MQ SimpleAuth authorization service's log
func mirrorMQSimpleAuthLogs(ctx context.Context, wg *sync.WaitGroup, name string, fromStart bool, mf mirrorFunc) (chan error, error) {
return mirrorLog(ctx, wg, "/var/mqm/errors/simpleauth.json", false, mf, true)
}
// mirrorWebServerLogs starts a goroutine to mirror the contents of the Liberty web server messages.log
func mirrorWebServerLogs(ctx context.Context, wg *sync.WaitGroup, name string, fromStart bool, mf mirrorFunc) (chan error, error) {
return mirrorLog(ctx, wg, "/var/mqm/web/installations/Installation1/servers/mqweb/logs/messages.log", fromStart, mf, true)
}
func getDebug() bool {
debug := os.Getenv("DEBUG")
if debug == "true" || debug == "1" {
return true
}
return false
}
func configureLogger(name string) (mirrorFunc, error) {
var err error
f := getLogFormat()
d := getDebug()
switch f {
case "json":
log, err = logger.NewLogger(os.Stderr, d, true, name)
if err != nil {
return nil, err
}
return func(msg string, isQMLog bool) bool {
arrLoggingConsoleExcludeIds := strings.Split(strings.ToUpper(os.Getenv("MQ_LOGGING_CONSOLE_EXCLUDE_ID")), ",")
if isExcludedMsgIdPresent(msg, arrLoggingConsoleExcludeIds) {
//If excluded id is present do not mirror it, return back
return false
}
// Check if the message is JSON
if len(msg) > 0 && msg[0] == '{' {
obj, err := processLogMessage(msg)
if err == nil && isQMLog && filterQMLogMessage(obj) {
return false
}
if err != nil {
log.Printf("Failed to unmarshall JSON in log message - %v", msg)
} else {
fmt.Println(msg)
}
} else {
// The log being mirrored isn't JSON. This can happen only in case of 'mqsc' logs
// Also if the logging source is from autocfgmqsc.LOG, then we have to construct the json string as per below logic
if checkLogSourceForMirroring("mqsc") && canMQSCLogBeMirroredToConsole(msg) {
logLevel := determineMQSCLogLevel(strings.TrimSpace(msg))
fmt.Printf("{\"ibm_datetime\":\"%s\",\"type\":\"mqsc_log\",\"loglevel\":\"%s\",\"message\":\"%s\"}\n",
getTimeStamp(), logLevel, strings.TrimSpace(msg))
}
}
return true
}, nil
case "basic":
log, err = logger.NewLogger(os.Stderr, d, false, name)
if err != nil {
return nil, err
}
return func(msg string, isQMLog bool) bool {
arrLoggingConsoleExcludeIds := strings.Split(strings.ToUpper(os.Getenv("MQ_LOGGING_CONSOLE_EXCLUDE_ID")), ",")
if isExcludedMsgIdPresent(msg, arrLoggingConsoleExcludeIds) {
//If excluded id is present do not mirror it, return back
return false
}
// Check if the message is JSON
if len(msg) > 0 && msg[0] == '{' {
// Parse the JSON message, and print a simplified version
obj, err := processLogMessage(msg)
if err == nil && isQMLog && filterQMLogMessage(obj) {
return false
}
if err != nil {
log.Printf("Failed to unmarshall JSON in log message - %v", err)
} else {
fmt.Print(formatBasic(obj))
}
} else {
// The log being mirrored isn't JSON, so just print it. This can happen only in case of mqsc logs
if checkLogSourceForMirroring("mqsc") && canMQSCLogBeMirroredToConsole(msg) {
log.Printf(strings.TrimSpace(msg))
}
}
return true
}, nil
default:
log, err = logger.NewLogger(os.Stdout, d, false, name)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("invalid value for LOG_FORMAT: %v", f)
}
}
func processLogMessage(msg string) (map[string]interface{}, error) {
var obj map[string]interface{}
err := json.Unmarshal([]byte(msg), &obj)
return obj, err
}
func filterQMLogMessage(obj map[string]interface{}) bool {
hostname, err := os.Hostname()
if os.Getenv("MQ_MULTI_INSTANCE") == "true" && err == nil && !strings.Contains(obj["host"].(string), hostname) {
return true
}
return false
}
// Function to check if ids provided in MQ_LOGGING_CONSOLE_EXCLUDE_ID are present in given log line or not
func isExcludedMsgIdPresent(msg string, envExcludeIds []string) bool {
for _, id := range envExcludeIds {
if id != "" && strings.Contains(msg, strings.TrimSpace(id)) {
return true
}
}
return false
}
func logDiagnostics() {
if getDebug() {
log.Debug("--- Start Diagnostics ---")
// show the directory ownership/permissions
// #nosec G104
out, _, _ := command.Run("ls", "-l", "/mnt/")
log.Debugf("/mnt/:\n%s", out)
// #nosec G104
out, _, _ = command.Run("ls", "-l", "/mnt/mqm")
log.Debugf("/mnt/mqm:\n%s", out)
// #nosec G104
out, _, _ = command.Run("ls", "-l", "/mnt/mqm/data")
log.Debugf("/mnt/mqm/data:\n%s", out)
// #nosec G104
out, _, _ = command.Run("ls", "-l", "/mnt/mqm-log/log")
log.Debugf("/mnt/mqm-log/log:\n%s", out)
// #nosec G104
out, _, _ = command.Run("ls", "-l", "/mnt/mqm-data/qmgrs")
log.Debugf("/mnt/mqm-data/qmgrs:\n%s", out)
// #nosec G104
out, _, _ = command.Run("ls", "-l", "/var/mqm")
log.Debugf("/var/mqm:\n%s", out)
// #nosec G104
out, _, _ = command.Run("ls", "-l", "/var/mqm/errors")
log.Debugf("/var/mqm/errors:\n%s", out)
// #nosec G104
out, _, _ = command.Run("ls", "-l", "/etc/mqm")
log.Debugf("/etc/mqm:\n%s", out)
// #nosec G104
out, _, _ = command.Run("ls", "-l", "/run")
log.Debugf("/run:\n%s", out)
// Print out summary of any FDCs
// #nosec G204
cmd := exec.Command("/opt/mqm/bin/ffstsummary")
cmd.Dir = "/var/mqm/errors"
// #nosec G104
outB, _ := cmd.CombinedOutput()
log.Debugf("ffstsummary:\n%s", string(outB))
log.Debug("--- End Diagnostics ---")
}
}
// Returns the value of MQ_LOGGING_CONSOLE_SOURCE environment variable
func getMQLogConsoleSource() string {
return strings.ToLower(strings.TrimSpace(os.Getenv("MQ_LOGGING_CONSOLE_SOURCE")))
}
// Function to check if valid values are provided for environment variable MQ_LOGGING_CONSOLE_SOURCE. If not valid, main program throws a warning to console
func isLogConsoleSourceValid() bool {
mqLogSource := getMQLogConsoleSource()
retValue := false
//If nothing is set, we will mirror all, so valid
if mqLogSource == "" {
return true
}
logConsoleSource := strings.Split(mqLogSource, ",")
//This will find out if the environment variable contains permitted values and is comma separated
for _, src := range logConsoleSource {
switch strings.TrimSpace(src) {
//If it is a permitted value, it is valid. Keep it as true, but dont return it. We may encounter something junk soon
case "qmgr", "web", "mqsc", "":
retValue = true
//If invalid entry arrives in-between/anywhere, just return false, there is no turning back
default:
return false
}
}
return retValue
}
// To check which all logs have to be mirrored
func checkLogSourceForMirroring(source string) bool {
logsrcs := getMQLogConsoleSource()
//Nothing set, this is when we mirror both qmgr & web
if logsrcs == "" {
if source == "qmgr" || source == "web" {
return true
} else {
return false
}
}
//Split the csv environment value so that we get an accurate comparison instead of a contains() check
logSrcArr := strings.Split(logsrcs, ",")
//Iterate through the array to decide on mirroring
for _, arr := range logSrcArr {
switch strings.TrimSpace(arr) {
case "qmgr":
//If value of source is qmgr and it exists in environment variable, mirror qmgr logs
if source == "qmgr" {
return true
}
case "web":
//If value of source is web and it exists in environment variable, and mirror web logs
if source == "web" {
//If older environment variable is set make sure to print appropriate message
if os.Getenv("MQ_ENABLE_EMBEDDED_WEB_SERVER_LOG") != "" {
log.Println("Environment variable MQ_ENABLE_EMBEDDED_WEB_SERVER_LOG has now been replaced. Use MQ_LOGGING_CONSOLE_SOURCE instead.")
}
return true
}
case "mqsc":
//If value of input parameter is mqsc and it exists in environment variable, mirror mqsc logs
if source == "mqsc" {
return true
}
}
}
return false
}
// canMQSCLogBeMirroredToConsole does the following check. The autocfgmqsc.log may contain comments, empty or unformatted lines. These will be ignored
func canMQSCLogBeMirroredToConsole(message string) bool {
if len(message) > 0 && !strings.HasPrefix(strings.TrimSpace(message), ": *") && !(strings.TrimSpace(message) == ":") {
return true
}
return false
}
// getTimeStamp fetches current time stamp
func getTimeStamp() string {
const timestampFormat string = "2008-01-02T15:04:05.000Z07:00"
t := time.Now()
return t.Format(timestampFormat)
}
// determineMQSCLogLevel finds out log level based on if the message contains 'AMQxxxxE:' string or not.
func determineMQSCLogLevel(message string) string {
//Match the below pattern
re := regexp.MustCompile(`AMQ[0-9]+E:`)
result := re.FindStringSubmatch(message)
if len(result) > 0 {
return "ERROR"
} else {
return "INFO"
}
}

View File

@@ -0,0 +1,149 @@
/*
© Copyright IBM Corporation 2020, 2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
"testing"
)
var formatBasicTests = []struct {
in []byte
outContains string
}{
{
[]byte("{\"ibm_datetime\":\"2020/06/24 00:00:00\",\"message\":\"Hello world\"}"),
"Hello",
},
{
[]byte("{\"ibm_datetime\":\"2020/06/24 00:00:00\",\"message\":\"Hello world\", \"ibm_commentInsert1\":\"foo\"}"),
"CommentInsert1(foo)",
},
{
[]byte("{\"ibm_datetime\":\"2020/06/24 00:00:00\",\"message\":\"Hello world\", \"ibm_arithInsert1\":1}"),
"ArithInsert1(1)",
},
}
func TestFormatBasic(t *testing.T) {
for i, table := range formatBasicTests {
t.Run(fmt.Sprintf("%v", i), func(t *testing.T) {
var inObj map[string]interface{}
json.Unmarshal(table.in, &inObj)
t.Logf("Unmarshalled: %+v", inObj)
out := formatBasic(inObj)
if !strings.Contains(out, table.outContains) {
t.Errorf("formatBasic() with input=%v - expected output to contain %v, got %v", string(table.in), table.outContains, out)
}
})
}
}
// This test covers for functions isLogConsoleSourceValid() & checkLogSourceForMirroring()
var mqLogSourcesTests = []struct {
testNum int
logsrc string
exptValid bool
exptQmgrSrc bool
exptWebSrc bool
exptMqscSrc bool
}{
{1, "qmgr,web", true, true, true, false},
{2, "qmgr", true, true, false, false},
{3, "web,qmgr", true, true, true, false},
{4, "web", true, false, true, false},
{5, " ", true, true, true, false},
{6, "QMGR,WEB", true, true, true, false},
{7, "qmgr, ", true, true, false, false},
{8, "qmgr , web", true, true, true, false},
{9, "qmgr,dummy", false, true, false, false},
{10, "fake,dummy", false, false, false, false},
{11, "qmgr,fake,dummy", false, true, false, false},
{12, "fake,dummy,web", false, false, true, false},
{13, "true", false, false, false, false},
{14, "false", false, false, false, false},
{15, "", true, true, true, false},
{16, "mqsc", true, false, false, true},
{17, "MQSC", true, false, false, true},
{18, "qmgr,mqsc", true, true, false, true},
{19, "web,mqsc", true, false, true, true},
{20, "qmgr,web,mqsc", true, true, true, true},
}
func TestLoggingConsoleSourceInputs(t *testing.T) {
for _, mqlogsrctest := range mqLogSourcesTests {
err := os.Setenv("MQ_LOGGING_CONSOLE_SOURCE", mqlogsrctest.logsrc)
if err != nil {
t.Error(err)
}
isValid := isLogConsoleSourceValid()
if isValid != mqlogsrctest.exptValid {
t.Errorf("Expected return value from isLogConsoleSourceValid() is %v for MQ_LOGGING_CONSOLE_SOURCE='%v', got %v\n", mqlogsrctest.exptValid, mqlogsrctest.logsrc, isValid)
}
isLogSrcQmgr := checkLogSourceForMirroring("qmgr")
if isLogSrcQmgr != mqlogsrctest.exptQmgrSrc {
t.Errorf("Expected return value from checkLogSourceForMirroring() is %v for MQ_LOGGING_CONSOLE_SOURCE='%v', got %v\n", mqlogsrctest.exptQmgrSrc, mqlogsrctest.logsrc, isLogSrcQmgr)
}
isLogSrcWeb := checkLogSourceForMirroring("web")
if isLogSrcWeb != mqlogsrctest.exptWebSrc {
t.Errorf("Expected return value from checkLogSourceForMirroring() is %v for MQ_LOGGING_CONSOLE_SOURCE='%v', got %v\n", mqlogsrctest.exptWebSrc, mqlogsrctest.logsrc, isLogSrcWeb)
}
isLogSrcMqsc := checkLogSourceForMirroring("mqsc")
if isLogSrcMqsc != mqlogsrctest.exptMqscSrc {
t.Errorf("Expected return value from checkLogSourceForMirroring() is %v for MQ_LOGGING_CONSOLE_SOURCE='%v', got %v\n", mqlogsrctest.exptMqscSrc, mqlogsrctest.logsrc, isLogSrcMqsc)
}
}
}
// This test covers for function isExcludedMsgIdPresent()
var mqExcludeIDTests = []struct {
testNum int
exculdeIDsArr []string
expectedRetVal bool
logEntry string
}{
{
1,
[]string{"AMQ5051I", "AMQ5037I", "AMQ5975I"},
true,
"{\"ibm_messageId\":\"AMQ5051I\",\"ibm_arithInsert1\":0,\"ibm_arithInsert2\":1,\"message\":\"AMQ5051I: The queue manager task 'AUTOCONFIG' has started.\"}",
},
{
2,
[]string{"AMQ5975I", "AMQ5037I"},
false,
"{\"ibm_messageId\":\"AMQ5051I\",\"ibm_arithInsert1\":0,\"ibm_arithInsert2\":1,\"message\":\"AMQ5051I: The queue manager task 'AUTOCONFIG' has started.\"}",
},
{
3,
[]string{""},
false,
"{\"ibm_messageId\":\"AMQ5051I\",\"ibm_arithInsert1\":0,\"ibm_arithInsert2\":1,\"message\":\"AMQ5051I: The queue manager task 'AUTOCONFIG' has started.\"}",
},
}
func TestIsExcludedMsgIDPresent(t *testing.T) {
for _, excludeIDTest := range mqExcludeIDTests {
retVal := isExcludedMsgIdPresent(excludeIDTest.logEntry, excludeIDTest.exculdeIDsArr)
if retVal != excludeIDTest.expectedRetVal {
t.Errorf("%v. Expected return value from isExcludedMsgIdPresent() is %v for MQ_LOGGING_CONSOLE_EXCLUDE_ID='%v', got %v\n",
excludeIDTest.testNum, excludeIDTest.expectedRetVal, excludeIDTest.exculdeIDsArr, retVal)
}
}
}

432
cmd/runmqserver/main.go Normal file
View File

@@ -0,0 +1,432 @@
/*
© Copyright IBM Corporation 2017, 2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// runmqserver initializes, creates and starts a queue manager, as PID 1 in a container
package main
import (
"context"
"errors"
"flag"
"os"
"path"
"sync"
"github.com/ibm-messaging/mq-container/internal/copy"
"github.com/ibm-messaging/mq-container/internal/fips"
"github.com/ibm-messaging/mq-container/internal/ha"
"github.com/ibm-messaging/mq-container/internal/metrics"
"github.com/ibm-messaging/mq-container/internal/ready"
"github.com/ibm-messaging/mq-container/internal/simpleauth"
"github.com/ibm-messaging/mq-container/internal/tls"
"github.com/ibm-messaging/mq-container/pkg/containerruntimelogger"
"github.com/ibm-messaging/mq-container/pkg/name"
)
func doMain() error {
var initFlag = flag.Bool("i", false, "initialize volume only, then exit")
var infoFlag = flag.Bool("info", false, "Display debug info, then exit")
var noLogRuntimeFlag = flag.Bool("nologruntime", false, "used when running this program from another program, to control log output")
var devFlag = flag.Bool("dev", false, "used when running this program from runmqdevserver to control how TLS is configured")
flag.Parse()
name, nameErr := name.GetQueueManagerName()
mf, err := configureLogger(name)
if err != nil {
logTermination(err)
return err
}
// Check whether they only want debug info
if *infoFlag {
logVersionInfo()
err = containerruntimelogger.LogContainerDetails(log)
if err != nil {
log.Printf("Error displaying container details: %v", err)
}
return nil
}
err = verifySingleProcess()
if err != nil {
// We don't do the normal termination here as it would create a termination file.
log.Error(err)
return err
}
if nameErr != nil {
logTermination(err)
return err
}
err = ready.Clear()
if err != nil {
logTermination(err)
return err
}
accepted, err := checkLicense()
if err != nil {
logTerminationf("Error checking license acceptance: %v", err)
return err
}
if !accepted {
err = errors.New("License not accepted")
logTermination(err)
return err
}
log.Printf("Using queue manager name: %v", name)
// Create a startup context to be used by the signalHandler to ensure the final reap of zombie processes only occurs after all startup processes are spawned
startupCtx, markStartupComplete := context.WithCancel(context.Background())
var startupMarkedComplete bool
// If the main thread returns before completing startup, cancel the startup context to unblock the signalHandler
defer func() {
if !startupMarkedComplete {
markStartupComplete()
}
}()
// Start signal handler
signalControl := signalHandler(name, startupCtx)
// Enable diagnostic collecting on failure
collectDiagOnFail = true
if *noLogRuntimeFlag == false {
err = containerruntimelogger.LogContainerDetails(log)
if err != nil {
logTermination(err)
return err
}
}
err = createVolume("/mnt/mqm/data")
if err != nil {
logTermination(err)
return err
}
err = createVolume("/mnt/mqm-log/log")
if err != nil {
logTermination(err)
return err
}
err = createVolume("/mnt/mqm-data/qmgrs")
if err != nil {
logTermination(err)
return err
}
// Delete contents of /run/scratch directory.
err = cleanVolume("/run/scratch")
if err != nil {
logTermination(err)
return err
}
// Delete contents of /run/mqm directory.
err = cleanVolume("/run/mqm")
if err != nil {
logTermination(err)
return err
}
// Create ephemeral volumes
err = createVolume("/run/scratch/runmqserver")
if err != nil {
logTermination(err)
return err
}
// Queue manager i.e crtmqm command creates socket and
// others files in /run/mqm directory.
err = createVolume("/run/mqm")
if err != nil {
logTermination(err)
return err
}
// Initialise 15-tls.mqsc file on ephemeral volume
// #nosec G306 - its a read by owner/s group, and pose no harm.
err = os.WriteFile("/run/15-tls.mqsc", []byte(""), 0660)
if err != nil {
logTermination(err)
return err
}
// Initialise native-ha ini files file on ephemeral volume
nativeHAINIs := []string{
"10-native-ha.ini",
"10-native-ha-instance.ini",
"10-native-ha-keystore.ini",
}
for _, iniFile := range nativeHAINIs {
// #nosec G306 - its a read by owner/s group, and pose no harm.
err = os.WriteFile(path.Join("/run", iniFile), []byte(""), 0660)
if err != nil {
logTermination(err)
return err
}
}
// Copy default mqwebcontainer.xml file to ephemeral volume
if *devFlag && os.Getenv("MQ_DEV") == "true" {
err = copy.CopyFile("/etc/mqm/web/installations/Installation1/servers/mqweb/mqwebcontainer.xml.dev", "/run/mqwebcontainer.xml")
if err != nil {
logTermination(err)
return err
}
} else {
err = copy.CopyFile("/etc/mqm/web/installations/Installation1/servers/mqweb/mqwebcontainer.xml.default", "/run/mqwebcontainer.xml")
if err != nil {
logTermination(err)
return err
}
}
// Copy default tls.xml file to ephemeral volume
err = copy.CopyFile("/etc/mqm/web/installations/Installation1/servers/mqweb/tls.xml.default", "/run/tls.xml")
if err != nil {
logTermination(err)
return err
}
// Copy default jvm.options file to ephemeral volume
err = copy.CopyFile("/etc/mqm/web/installations/Installation1/servers/mqweb/configDropins/defaults/jvm.options.default", "/run/jvm.options")
if err != nil {
logTermination(err)
return err
}
enableTraceCrtmqdir := os.Getenv("MQ_ENABLE_TRACE_CRTMQDIR")
if enableTraceCrtmqdir == "true" || enableTraceCrtmqdir == "1" {
err = startMQTrace()
if err != nil {
logTermination(err)
return err
}
}
err = createDirStructure()
if err != nil {
logTermination(err)
return err
}
if enableTraceCrtmqdir == "true" || enableTraceCrtmqdir == "1" {
err = endMQTrace()
if err != nil {
logTermination(err)
return err
}
}
// If init flag is set, exit now
if *initFlag {
return nil
}
// Print out versioning information
logVersionInfo()
// Determine FIPS compliance level
fips.ProcessFIPSType(log)
keyLabel, defaultCmsKeystore, defaultP12Truststore, err := tls.ConfigureDefaultTLSKeystores()
if err != nil {
logTermination(err)
return err
}
err = tls.ConfigureTLS(keyLabel, defaultCmsKeystore, *devFlag, log)
if err != nil {
logTermination(err)
return err
}
//Validate MQ_LOG_CONSOLE_SOURCE variable
if !isLogConsoleSourceValid() {
log.Println("One or more invalid value is provided for MQ_LOGGING_CONSOLE_SOURCE. Allowed values are 'qmgr','web' and 'mqsc' in csv format")
}
var wg sync.WaitGroup
defer func() {
log.Debug("Waiting for log mirroring to complete")
wg.Wait()
}()
ctx, cancelMirror := context.WithCancel(context.Background())
defer func() {
log.Debug("Cancel log mirroring")
cancelMirror()
}()
//For mirroring web server logs if source variable is set
if checkLogSourceForMirroring("web") {
// Always log from the end of the web server messages.log, because the log rotation should happen as soon as the web server starts
_, err = mirrorWebServerLogs(ctx, &wg, name, false, mf)
if err != nil {
logTermination(err)
return err
}
}
err = postInit(name, keyLabel, defaultP12Truststore)
if err != nil {
logTermination(err)
return err
}
if os.Getenv("MQ_NATIVE_HA") == "true" {
err = ha.ConfigureNativeHA(log)
if err != nil {
logTermination(err)
return err
}
}
// Post FIPS initialization processing
fips.PostInit(log)
enableTraceCrtmqm := os.Getenv("MQ_ENABLE_TRACE_CRTMQM")
if enableTraceCrtmqm == "true" || enableTraceCrtmqm == "1" {
err = startMQTrace()
if err != nil {
logTermination(err)
return err
}
}
newQM, err := createQueueManager(name, *devFlag)
if err != nil {
logTermination(err)
return err
}
if enableTraceCrtmqm == "true" || enableTraceCrtmqm == "1" {
err = endMQTrace()
if err != nil {
logTermination(err)
return err
}
}
//For mirroring mq system logs and qm logs, if environment variable is set
if checkLogSourceForMirroring("qmgr") {
//Mirror MQ system logs
_, err = mirrorSystemErrorLogs(ctx, &wg, mf)
if err != nil {
logTermination(err)
return err
}
//Mirror queue manager logs
_, err = mirrorQueueManagerErrorLogs(ctx, &wg, name, newQM, mf)
if err != nil {
logTermination(err)
return err
}
}
if *devFlag && simpleauth.IsEnabled() {
_, err = mirrorMQSimpleAuthLogs(ctx, &wg, name, newQM, mf)
if err != nil {
logTermination(err)
return err
}
}
err = updateCommandLevel()
if err != nil {
logTermination(err)
return err
}
enableTraceStrmqm := os.Getenv("MQ_ENABLE_TRACE_STRMQM")
if enableTraceStrmqm == "true" || enableTraceStrmqm == "1" {
err = startMQTrace()
if err != nil {
logTermination(err)
return err
}
}
// This is a developer image only change
// This workaround should be removed and handled via <crtmqm -ii>, when inimerge is ready to handle stanza ordering
if *devFlag && simpleauth.IsEnabled() {
err = updateQMini(name)
if err != nil {
logTermination(err)
return err
}
}
err = startQueueManager(name)
if err != nil {
logTermination(err)
return err
}
//If the queue manager has started successfully, reflect mqsc logs when enabled
if checkLogSourceForMirroring("mqsc") {
_, err = mirrorMQSCLogs(ctx, &wg, name, mf)
if err != nil {
logTermination(err)
return err
}
}
if enableTraceStrmqm == "true" || enableTraceStrmqm == "1" {
err = endMQTrace()
if err != nil {
logTermination(err)
return err
}
}
enableMetrics := os.Getenv("MQ_ENABLE_METRICS")
if enableMetrics == "true" || enableMetrics == "1" {
go metrics.GatherMetrics(name, log)
} else {
log.Println("Metrics are disabled")
}
// Start reaping zombies from now on.
// Start this here, so that we don't reap any sub-processes created
// by this process (e.g. for crtmqm or strmqm)
signalControl <- startReaping
// Reap zombies now, just in case we've already got some
signalControl <- reapNow
startupMarkedComplete = true
markStartupComplete()
// Write a file to indicate that chkmqready should now work as normal
err = ready.Set()
if err != nil {
logTermination(err)
return err
}
// Wait for terminate signal
<-signalControl
return nil
}
var osExit = os.Exit
func main() {
err := doMain()
if err != nil {
osExit(1)
}
}

View File

@@ -0,0 +1,60 @@
/*
© Copyright IBM Corporation 2017, 2023
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"flag"
"os"
"path/filepath"
"strconv"
"testing"
"github.com/ibm-messaging/mq-container/pkg/logger"
)
var test *bool
func init() {
test = flag.Bool("test", false, "Set to true when running tests for coverage")
log, _ = logger.NewLogger(os.Stdout, true, false, "test")
}
// Test started when the test binary is started. Only calls main.
func TestSystem(t *testing.T) {
if *test {
var oldExit = osExit
defer func() {
osExit = oldExit
}()
filename, ok := os.LookupEnv("EXIT_CODE_FILE")
if !ok {
filename = "/var/coverage/exitCode"
} else {
filename = filepath.Join("/var/coverage/", filename)
}
osExit = func(rc int) {
// Write the exit code to a file instead
log.Printf("Writing exit code %v to file %v", strconv.Itoa(rc), filename)
err := os.WriteFile(filename, []byte(strconv.Itoa(rc)), 0644)
if err != nil {
log.Print(err)
}
}
main()
}
}

200
cmd/runmqserver/mirror.go Normal file
View File

@@ -0,0 +1,200 @@
/*
© Copyright IBM Corporation 2018, 2019
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"bufio"
"context"
"fmt"
"os"
"sync"
"time"
)
// waitForFile waits until the specified file exists
func waitForFile(ctx context.Context, path string) (os.FileInfo, error) {
var fi os.FileInfo
var err error
// Wait for file to exist
for {
select {
// Check to see if cancellation has been requested
case <-ctx.Done():
return os.Stat(path)
default:
fi, err = os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
time.Sleep(500 * time.Millisecond)
continue
} else {
return nil, fmt.Errorf("mirror: unable to get info on file %v", path)
}
}
return fi, nil
}
}
}
type mirrorFunc func(msg string, isQMLog bool) bool
// mirrorAvailableMessages prints lines from the file, until no more are available
func mirrorAvailableMessages(f *os.File, mf mirrorFunc, isQMLog bool) {
scanner := bufio.NewScanner(f)
count := 0
for scanner.Scan() {
t := scanner.Text()
if mf(t, isQMLog) {
count++
}
}
if count > 0 {
log.Debugf("Mirrored %v log entries from %v", count, f.Name())
}
err := scanner.Err()
if err != nil {
log.Errorf("Error reading file %v: %v", f.Name(), err)
return
}
}
// mirrorLog tails the specified file, and logs each line to stdout.
// This is useful for usability, as the container console log can show
// messages from the MQ error logs.
func mirrorLog(ctx context.Context, wg *sync.WaitGroup, path string, fromStart bool, mf mirrorFunc, isQMLog bool) (chan error, error) {
errorChannel := make(chan error, 1)
var offset int64 = -1
var f *os.File
var err error
var fi os.FileInfo
// Need to check if the file exists before returning, otherwise we have a
// race to see if the new file get created before we can test for it
fi, err = os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
// File doesn't exist, so ensure we start at the beginning
offset = 0
} else {
return nil, err
}
} else {
// If the file exists, open it now, before we return. This makes sure
// the file is open before the queue manager is created or started.
// Otherwise, there would be the potential for a nearly-full file to
// rotate before the goroutine had a chance to open it.
// #nosec G304 - no harm, we open readonly and check error.
f, err = os.OpenFile(path, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
// File already exists, so start reading at the end
offset = fi.Size()
}
// Increment wait group counter, only if the goroutine gets started
wg.Add(1)
go func() {
// Notify the wait group when this goroutine ends
defer func() {
log.Debugf("Finished monitoring %v", path)
wg.Done()
}()
if f == nil {
// File didn't exist, so need to wait for it
fi, err = waitForFile(ctx, path)
if err != nil {
log.Error(err)
errorChannel <- err
return
}
if fi == nil {
return
}
log.Debugf("File exists: %v, %v", path, fi.Size())
// #nosec G304 - no harm, we open readonly and check error.
f, err = os.OpenFile(path, os.O_RDONLY, 0)
if err != nil {
log.Error(err)
errorChannel <- err
return
}
}
fi, err = f.Stat()
if err != nil {
log.Error(err)
errorChannel <- err
return
}
// The file now exists. If it didn't exist before we started, offset=0
// Always start at the beginning if we've been told to go from the start
if offset != 0 && !fromStart {
log.Debugf("Seeking offset %v in file %v", offset, path)
_, err = f.Seek(offset, 0)
if err != nil {
log.Errorf("Unable to return to offset %v: %v", offset, err)
}
}
closing := false
for {
// If there's already data there, mirror it now.
mirrorAvailableMessages(f, mf, isQMLog)
// Wait for the new log file (after rotation)
newFI, err := waitForFile(ctx, path)
if err != nil {
log.Error(err)
errorChannel <- err
return
}
if !os.SameFile(fi, newFI) {
log.Debugf("Detected log rotation in file %v", path)
// WARNING: There is a possible race condition here. If *another*
// log rotation happens before we can open the new file, then we
// could skip all those messages. This could happen with a very small
// MQ error log size.
mirrorAvailableMessages(f, mf, isQMLog)
err = f.Close()
if err != nil {
log.Errorf("Unable to close mirror file handle: %v", err)
}
// Re-open file
log.Debugf("Re-opening error log file %v", path)
// #nosec G304 - no harm, we open readonly and check error.
f, err = os.OpenFile(path, os.O_RDONLY, 0)
if err != nil {
log.Error(err)
errorChannel <- err
return
}
fi = newFI
// Don't seek this time, because we know it's a new file
mirrorAvailableMessages(f, mf, isQMLog)
}
select {
case <-ctx.Done():
log.Debugf("Context cancelled for mirroring %v", path)
if closing {
log.Debugf("Shutting down mirror for %v", path)
return
}
// Set a flag, to allow one more time through the loop
closing = true
default:
time.Sleep(500 * time.Millisecond)
}
}
}()
return errorChannel, nil
}

View File

@@ -0,0 +1,194 @@
/*
© Copyright IBM Corporation 2018, 2023
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"fmt"
"os"
"strconv"
"strings"
"sync"
"testing"
"time"
)
func TestMirrorLogWithoutRotation(t *testing.T) {
// Repeat the test multiple times, to help identify timing problems
for i := 0; i < 10; i++ {
t.Run(t.Name()+strconv.Itoa(i), func(t *testing.T) {
// Use just the sub-test name in the file name
tmp, err := os.CreateTemp("", strings.Split(t.Name(), "/")[1])
if err != nil {
t.Fatal(err)
}
t.Log(tmp.Name())
defer os.Remove(tmp.Name())
count := 0
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
_, err = mirrorLog(ctx, &wg, tmp.Name(), true, func(msg string, isQMLog bool) bool {
count++
return true
}, false)
if err != nil {
t.Fatal(err)
}
f, err := os.OpenFile(tmp.Name(), os.O_WRONLY, 0700)
if err != nil {
t.Fatal(err)
}
log.Println("Logging 3 JSON messages")
fmt.Fprintln(f, "{\"message\"=\"A\"}")
fmt.Fprintln(f, "{\"message\"=\"B\"}")
fmt.Fprintln(f, "{\"message\"=\"C\"}")
f.Close()
cancel()
wg.Wait()
if count != 3 {
t.Fatalf("Expected 3 log entries; got %v", count)
}
})
}
}
func TestMirrorLogWithRotation(t *testing.T) {
// Repeat the test multiple times, to help identify timing problems
for i := 0; i < 5; i++ {
t.Run(t.Name()+strconv.Itoa(i), func(t *testing.T) {
// Use just the sub-test name in the file name
tmp, err := os.CreateTemp("", strings.Split(t.Name(), "/")[1])
if err != nil {
t.Fatal(err)
}
t.Log(tmp.Name())
defer func() {
t.Log("Removing file")
os.Remove(tmp.Name())
}()
count := 0
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
_, err = mirrorLog(ctx, &wg, tmp.Name(), true, func(msg string, isQMLog bool) bool {
count++
return true
}, false)
if err != nil {
t.Fatal(err)
}
f, err := os.OpenFile(tmp.Name(), os.O_WRONLY, 0700)
if err != nil {
t.Fatal(err)
}
t.Log("Logging 3 JSON messages")
fmt.Fprintln(f, "{\"message\"=\"A\"}")
fmt.Fprintln(f, "{\"message\"=\"B\"}")
fmt.Fprintln(f, "{\"message\"=\"C\"}")
f.Close()
// Rotate the file, by renaming it
rotated := tmp.Name() + ".1"
os.Rename(tmp.Name(), rotated)
defer os.Remove(rotated)
// Open a new file, with the same name as before
f, err = os.OpenFile(tmp.Name(), os.O_WRONLY|os.O_CREATE, 0700)
if err != nil {
t.Fatal(err)
}
t.Log("Logging 2 more JSON messages")
fmt.Fprintln(f, "{\"message\"=\"D\"}")
fmt.Fprintln(f, "{\"message\"=\"E\"}")
f.Close()
// Shut the mirroring down
cancel()
wg.Wait()
if count != 5 {
t.Fatalf("Expected 5 log entries; got %v", count)
}
})
}
}
func testMirrorLogExistingFile(t *testing.T, newQM bool) int {
tmp, err := os.CreateTemp("", t.Name())
if err != nil {
t.Fatal(err)
}
t.Log(tmp.Name())
log.Println("Logging 1 message before we start")
os.WriteFile(tmp.Name(), []byte("{\"message\"=\"A\"}\n"), 0600)
defer os.Remove(tmp.Name())
count := 0
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
_, err = mirrorLog(ctx, &wg, tmp.Name(), newQM, func(msg string, isQMLog bool) bool {
count++
return true
}, false)
if err != nil {
t.Fatal(err)
}
f, err := os.OpenFile(tmp.Name(), os.O_APPEND|os.O_WRONLY, 0700)
if err != nil {
t.Fatal(err)
}
log.Println("Logging 2 new JSON messages")
fmt.Fprintln(f, "{\"message\"=\"B\"}")
fmt.Fprintln(f, "{\"message\"=\"C\"}")
f.Close()
cancel()
wg.Wait()
return count
}
// TestMirrorLogExistingFile tests that we only get new log messages, if the
// log file already exists
func TestMirrorLogExistingFile(t *testing.T) {
count := testMirrorLogExistingFile(t, false)
if count != 2 {
t.Fatalf("Expected 2 log entries; got %v", count)
}
}
// TestMirrorLogExistingFileButNewQueueManager tests that we only get all log
// messages, even if the file exists, if we tell it we want all messages
func TestMirrorLogExistingFileButNewQueueManager(t *testing.T) {
count := testMirrorLogExistingFile(t, true)
if count != 3 {
t.Fatalf("Expected 3 log entries; got %v", count)
}
}
func TestMirrorLogCancelWhileWaiting(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
defer func() {
cancel()
wg.Wait()
}()
_, err := mirrorLog(ctx, &wg, "fake.log", true, func(msg string, isQMLog bool) bool {
return true
}, false)
if err != nil {
t.Error(err)
}
time.Sleep(time.Second * 3)
cancel()
wg.Wait()
// No need to assert anything. If it didn't work, the code would have hung (TODO: not ideal)
}

View File

@@ -0,0 +1,59 @@
/*
© Copyright IBM Corporation 2018, 2023
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"os"
"github.com/ibm-messaging/mq-container/internal/fips"
"github.com/ibm-messaging/mq-container/internal/tls"
)
// postInit is run after /var/mqm is set up
func postInit(name, keyLabel string, p12Truststore tls.KeyStoreData) error {
enableWebServer := os.Getenv("MQ_ENABLE_EMBEDDED_WEB_SERVER")
if enableWebServer == "true" || enableWebServer == "1" {
// Enable FIPS for MQ Web Server if asked for.
if fips.IsFIPSEnabled() {
err := configureFIPSWebServer(p12Truststore)
if err != nil {
return err
}
}
// Configure the web server (if enabled)
webKeystore, err := configureWebServer(keyLabel, p12Truststore)
if err != nil {
return err
}
// If trust-store is empty, set reference to point to the keystore
webTruststoreRef := "MQWebTrustStore"
if len(p12Truststore.TrustedCerts) == 0 {
webTruststoreRef = "MQWebKeyStore"
}
// Start the web server, in the background (if installed)
// WARNING: No error handling or health checking available for the web server
go func() {
err = startWebServer(webKeystore, p12Truststore.Password, webTruststoreRef)
if err != nil {
log.Printf("Error starting web server: %v", err)
}
}()
}
return nil
}

View File

@@ -0,0 +1,64 @@
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/ibm-messaging/mq-container/internal/command"
)
// Verifies that we are the main or only instance of this program
func verifySingleProcess() error {
programName, err := determineExecutable()
if err != nil {
return fmt.Errorf("Failed to determine name of this program - %v", err)
}
// Verify that there is only one runmqserver
_, err = verifyOnlyOne(programName)
if err != nil {
return fmt.Errorf("You cannot run more than one instance of this program")
}
return nil
}
// Verifies that there is only one instance running of the given program name.
func verifyOnlyOne(programName string) (int, error) {
// #nosec G104
out, _, _ := command.Run("ps", "-e", "--format", "cmd")
//if this goes wrong then assume we are the only one
numOfProg := strings.Count(out, programName)
if numOfProg != 1 {
return numOfProg, fmt.Errorf("Expected there to be only 1 instance of %s but found %d", programName, numOfProg)
}
return numOfProg, nil
}
// Determines the name of the currently running executable.
func determineExecutable() (string, error) {
file, err := os.Executable()
if err != nil {
return "", err
}
_, exec := filepath.Split(file)
return exec, nil
}

360
cmd/runmqserver/qmgr.go Normal file
View File

@@ -0,0 +1,360 @@
/*
© Copyright IBM Corporation 2017, 2023
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"github.com/ibm-messaging/mq-container/internal/command"
containerruntime "github.com/ibm-messaging/mq-container/internal/containerruntime"
"github.com/ibm-messaging/mq-container/internal/mqscredact"
"github.com/ibm-messaging/mq-container/internal/mqversion"
"github.com/ibm-messaging/mq-container/internal/pathutils"
"github.com/ibm-messaging/mq-container/internal/ready"
)
// createDirStructure creates the default MQ directory structure under /var/mqm
func createDirStructure() error {
// log file diagnostics before and after crtmqdir if DEBUG=true
logDiagnostics()
out, rc, err := command.Run("/opt/mqm/bin/crtmqdir", "-f", "-a")
if err != nil {
if rc == 10 {
// Ignore warnings about 'mqwebuser.xml' being a symlink
if !(strings.Join(strings.Fields(string(out)), " ") == "The filesystem object '/mnt/mqm/data/web/installations/Installation1/servers/mqweb/mqwebuser.xml' is a symbolic link.") {
log.Printf("Warning creating directory structure: %v\n", string(out))
}
} else {
log.Printf("Error creating directory structure: the 'crtmqdir' command returned with code: %v. Reason: %v\n", rc, string(out))
return err
}
}
log.Println("Created directory structure under /var/mqm")
logDiagnostics()
return nil
}
// createQueueManager creates a queue manager, if it doesn't already exist.
// It returns true if one was created (or a standby was created), or false if one already existed
func createQueueManager(name string, devMode bool) (bool, error) {
log.Printf("Creating queue manager %v", name)
mounts, err := containerruntime.GetMounts()
if err != nil {
log.Printf("Error getting mounts for queue manager")
return false, err
}
dataDir := getQueueManagerDataDir(mounts, replaceCharsInQMName(name))
// Run 'dspmqinf' to check if 'mqs.ini' configuration file exists
// If command succeeds, the queue manager (or standby queue manager) has already been created
_, _, err = command.Run("dspmqinf", name)
if err == nil {
log.Printf("Detected existing queue manager %v", name)
// Check if MQ_QMGR_LOG_FILE_PAGES matches the value set in qm.ini
lfp := os.Getenv("MQ_QMGR_LOG_FILE_PAGES")
if lfp != "" {
qmIniBytes, err := readQMIni(dataDir)
if err != nil {
log.Printf("Error reading qm.ini : %v", err)
return false, err
}
if !validateLogFilePageSetting(qmIniBytes, lfp) {
log.Println("Warning: the value of MQ_QMGR_LOG_FILE_PAGES does not match the value of 'LogFilePages' in the qm.ini. This setting cannot be altered after Queue Manager creation.")
}
}
return false, nil
}
// Check if 'qm.ini' configuration file exists for the queue manager
// TODO : handle possible race condition - use a file lock?
_, err = os.Stat(pathutils.CleanPath(dataDir, "qm.ini"))
if err != nil {
// If 'qm.ini' is not found - run 'crtmqm' to create a new queue manager
args := getCreateQueueManagerArgs(mounts, name, devMode)
out, rc, err := command.Run("crtmqm", args...)
if err != nil {
log.Printf("Error creating queue manager: the 'crtmqm' command returned with code: %v. Reason: %v", rc, string(out))
return false, err
}
} else {
// If 'qm.ini' is found - run 'addmqinf' to create a standby queue manager with existing configuration
args := getCreateStandbyQueueManagerArgs(name)
out, rc, err := command.Run("addmqinf", args...)
if err != nil {
log.Printf("Error creating standby queue manager: the 'addmqinf' command returned with code: %v. Reason: %v",
rc, string(out))
return false, err
}
log.Println("Created standby queue manager")
return true, nil
}
log.Println("Created queue manager")
return true, nil
}
// readQMIni reads the qm.ini file and returns it as a byte array
// This function is specific to comply with the nosec.
func readQMIni(dataDir string) ([]byte, error) {
qmgrDir := pathutils.CleanPath(dataDir, "qm.ini")
// #nosec G304 - qmgrDir filepath is derived from dspmqinf
iniFileBytes, err := os.ReadFile(qmgrDir)
if err != nil {
return nil, err
}
return iniFileBytes, err
}
// validateLogFilePageSetting validates if the specified logFilePage number is equal to the existing value in the qm.ini
func validateLogFilePageSetting(iniFileBytes []byte, logFilePages string) bool {
lfpString := "LogFilePages=" + logFilePages
qminiConfigStr := string(iniFileBytes)
return strings.Contains(qminiConfigStr, lfpString)
}
func updateCommandLevel() error {
level, ok := os.LookupEnv("MQ_CMDLEVEL")
if ok && level != "" {
log.Printf("Setting CMDLEVEL to %v", level)
out, rc, err := command.Run("strmqm", "-e", "CMDLEVEL="+level)
if err != nil {
log.Printf("Error setting CMDLEVEL for queue manager: the 'strmqm' command returned with code: %v. Reason: %v",
rc, string(out))
return err
}
}
return nil
}
func startQueueManager(name string) error {
log.Println("Starting queue manager")
out, rc, err := command.Run("strmqm", "-x", name)
if err != nil {
// 30=standby queue manager started, which is fine
// 94=native HA replica started, which is fine
if rc == 30 {
log.Printf("Started standby queue manager")
return nil
} else if rc == 94 {
log.Printf("Started replica queue manager")
return nil
}
log.Printf("Error starting queue manager: the 'strmqm' command returned with code: %v. Reason: %v", rc, string(out))
return err
}
log.Println("Started queue manager")
return nil
}
func stopQueueManager(name string) error {
log.Println("Stopping queue manager")
qmGracePeriod := os.Getenv("MQ_GRACE_PERIOD")
status, err := ready.Status(context.Background(), name)
if err != nil {
log.Printf("Error getting status for queue manager %v. The 'dspmq' command returned reason: %v",
name, err.Error())
return err
}
isStandby := status.StandbyQM()
args := []string{"-w", "-r", "-tp", qmGracePeriod, name}
if os.Getenv("MQ_MULTI_INSTANCE") == "true" {
if isStandby {
args = []string{"-x", name}
} else {
args = []string{"-s", "-w", "-tp", qmGracePeriod, name}
}
}
out, rc, err := command.Run("endmqm", args...)
if err != nil {
log.Printf("Error stopping queue manager: the 'endmqm' command returned with code: %v. Reason: %v", rc, string(out))
return err
}
if isStandby {
log.Printf("Stopped standby queue manager")
} else {
log.Println("Stopped queue manager")
}
return nil
}
func startMQTrace() error {
log.Println("Starting MQ trace")
out, rc, err := command.Run("strmqtrc")
if err != nil {
log.Printf("Error starting MQ trace: the 'strmqtrc' command returned with code: %v. Reason: %v", rc, string(out))
return err
}
log.Println("Started MQ trace")
return nil
}
func endMQTrace() error {
log.Println("Ending MQ Trace")
out, rc, err := command.Run("endmqtrc")
if err != nil {
log.Printf("Error ending MQ trace: the 'endmqtrc' command returned with code: %v. Reason: %v", rc, string(out))
return err
}
log.Println("Ended MQ trace")
return nil
}
func formatMQSCOutput(out string) string {
// redact sensitive information
out, _ = mqscredact.Redact(out)
// add tab characters to make it more readable as part of the log
return strings.Replace(string(out), "\n", "\n\t", -1)
}
func isStandbyQueueManager(name string) (bool, error) {
out, rc, err := command.Run("dspmq", "-n", "-m", name)
if err != nil {
log.Printf("Error while getting status for queue manager %v: the 'dspmq' command returned with code: %v. Reason: %v",
name, rc, string(out))
return false, err
}
if strings.Contains(string(out), "(RUNNING AS STANDBY)") {
return true, nil
}
return false, nil
}
func getQueueManagerDataDir(mounts map[string]string, name string) string {
dataDir := pathutils.CleanPath("/var/mqm/qmgrs", name)
if _, ok := mounts["/mnt/mqm-data"]; ok {
dataDir = pathutils.CleanPath("/mnt/mqm-data/qmgrs", name)
}
return dataDir
}
func getCreateQueueManagerArgs(mounts map[string]string, name string, devMode bool) []string {
mqversionBase := "9.2.1.0"
// use "UserExternal" only if we are 9.2.1.0 or above.
oaVal := "user"
mqVersionCheck, err := mqversion.Compare(mqversionBase)
if err != nil {
log.Printf("Error comparing MQ versions for oa,rc: %v", mqVersionCheck)
}
if mqVersionCheck >= 0 {
oaVal = "UserExternal"
}
//build args
args := []string{"-ii", "/etc/mqm/", "-ic", "/etc/mqm/", "-q", "-p", "1414"}
if os.Getenv("MQ_NATIVE_HA") == "true" {
args = append(args, "-lr", os.Getenv("HOSTNAME"))
}
if devMode {
args = append(args, "-oa", oaVal)
}
if _, ok := mounts["/mnt/mqm-log"]; ok {
args = append(args, "-ld", "/mnt/mqm-log/log")
}
if _, ok := mounts["/mnt/mqm-data"]; ok {
args = append(args, "-md", "/mnt/mqm-data/qmgrs")
}
if os.Getenv("MQ_QMGR_LOG_FILE_PAGES") != "" {
_, err = strconv.Atoi(os.Getenv("MQ_QMGR_LOG_FILE_PAGES"))
if err != nil {
log.Printf("Error processing MQ_QMGR_LOG_FILE_PAGES, the default value for LogFilePages will be used. Err: %v", err)
} else {
args = append(args, "-lf", os.Getenv("MQ_QMGR_LOG_FILE_PAGES"))
}
}
args = append(args, name)
return args
}
func getCreateStandbyQueueManagerArgs(name string) []string {
args := []string{"-s", "QueueManager"}
args = append(args, "-v", fmt.Sprintf("Name=%v", name))
args = append(args, "-v", fmt.Sprintf("Directory=%v", name))
args = append(args, "-v", "Prefix=/var/mqm")
args = append(args, "-v", fmt.Sprintf("DataPath=/mnt/mqm-data/qmgrs/%v", name))
return args
}
// updateQMini removes the original ServicecCmponent stanza so we can add a new one
func updateQMini(qmname string) error {
val, set := os.LookupEnv("MQ_CONNAUTH_USE_HTP")
if !set {
//simpleauth mode not enabled.
return nil
}
bval, err := strconv.ParseBool(strings.ToLower(val))
if err != nil {
return err
}
if bval == false {
//simpleauth mode not enabled.
return nil
}
log.Printf("Removing existing ServiceComponent configuration")
mounts, err := containerruntime.GetMounts()
if err != nil {
log.Printf("Error getting mounts for queue manager")
return err
}
dataDir := getQueueManagerDataDir(mounts, replaceCharsInQMName(qmname))
qmgrDir := pathutils.CleanPath(dataDir, "qm.ini")
//read the initial version.
// #nosec G304 - qmgrDir filepath is derived from dspmqinf
iniFileBytes, err := os.ReadFile(qmgrDir)
if err != nil {
return err
}
qminiConfigStr := string(iniFileBytes)
if strings.Contains(qminiConfigStr, "ServiceComponent:") {
var re = regexp.MustCompile(`(?m)^.*ServiceComponent.*$\s^.*Service.*$\s^.*Name.*$\s^.*Module.*$\s^.*ComponentDataSize.*$`)
curFile := re.ReplaceAllString(qminiConfigStr, "")
// #nosec G304 G306 - qmgrDir filepath is derived from dspmqinf and
// its a read by owner/s group, and pose no harm.
err := os.WriteFile(qmgrDir, []byte(curFile), 0660)
if err != nil {
return err
}
}
return nil
}
// If queue manager name contains a '.', then the '.' will be replaced with '!'
// in the name of the data directory created by the queue manager. Similarly
// '/' will be replaced with '&'.
func replaceCharsInQMName(qmname string) string {
replacedName := qmname
if strings.Contains(replacedName, ".") {
replacedName = strings.ReplaceAll(replacedName, ".", "!")
}
if strings.Contains(replacedName, "/") {
replacedName = strings.ReplaceAll(replacedName, "/", "&")
}
return replacedName
}

View File

@@ -0,0 +1,122 @@
/*
© Copyright IBM Corporation 2023
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"os"
"strings"
"testing"
)
func Test_validateLogFilePageSetting(t *testing.T) {
type args struct {
iniFilePath string
isValid bool
logFilePagesValue string
}
tests := []struct {
name string
args args
}{
{
name: "TestLogFilePages1",
args: args{
iniFilePath: "./test-files/testvalidateLogFilePages_1.ini",
isValid: true,
logFilePagesValue: "1235",
},
},
{
name: "TestLogFilePages2",
args: args{
iniFilePath: "./test-files/testvalidateLogFilePages_2.ini",
isValid: true,
logFilePagesValue: "2224",
},
},
{
name: "TestLogFilePages3",
args: args{
iniFilePath: "./test-files/testvalidateLogFilePages_3.ini",
isValid: false,
logFilePagesValue: "1235",
},
},
{
name: "TestLogFilePages4",
args: args{
iniFilePath: "./test-files/testvalidateLogFilePages_4.ini",
isValid: false,
logFilePagesValue: "1235",
},
},
{
name: "TestLogFilePages5",
args: args{
iniFilePath: "./test-files/testvalidateLogFilePages_5.ini",
isValid: false,
logFilePagesValue: "1235",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
iniFileBytes, err := os.ReadFile(tt.args.iniFilePath)
if err != nil {
t.Fatal(err)
}
validate := validateLogFilePageSetting(iniFileBytes, tt.args.logFilePagesValue)
if validate != tt.args.isValid {
t.Fatalf("Expected ini file validation output to be %v got %v", tt.args.isValid, validate)
}
})
}
}
// Unit test for special character in queue manager names
func Test_SpecialCharInQMNameReplacements(t *testing.T) {
type qmNames struct {
qmName string
replacedQMName string
}
tests := []qmNames{
{
qmName: "QM.",
replacedQMName: "QM!",
}, {
qmName: "QM/",
replacedQMName: "QM&",
},
{
qmName: "QM.GR.NAME",
replacedQMName: "QM!GR!NAME",
}, {
qmName: "QM/GR/NAME",
replacedQMName: "QM&GR&NAME",
}, {
qmName: "QMGRNAME",
replacedQMName: "QMGRNAME",
},
}
for _, test := range tests {
replacedQMName := replaceCharsInQMName(test.qmName)
if !strings.EqualFold(replacedQMName, test.replacedQMName) {
t.Fatalf("QMName replacement failed. Expected %s but got %s\n", test.replacedQMName, replacedQMName)
}
}
}

124
cmd/runmqserver/signals.go Normal file
View File

@@ -0,0 +1,124 @@
/*
© Copyright IBM Corporation 2017, 2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"os"
"os/signal"
"syscall"
"github.com/ibm-messaging/mq-container/internal/metrics"
"golang.org/x/sys/unix"
)
const (
startReaping = iota
reapNow = iota
)
func signalHandler(qmgr string, startupCtx context.Context) chan int {
control := make(chan int)
// Use separate channels for the signals, to avoid SIGCHLD signals swamping
// the buffer, and preventing other signals.
stopSignals := make(chan os.Signal, 1)
reapSignals := make(chan os.Signal, 1)
signal.Notify(stopSignals, syscall.SIGTERM, syscall.SIGINT)
// Pulling out as function as reused for shutdown and standard control flow
processControlSignal := func(job int) {
switch {
case job == startReaping:
// Add SIGCHLD to the list of signals we're listening to
log.Debug("Listening for SIGCHLD signals")
signal.Notify(reapSignals, syscall.SIGCHLD)
case job == reapNow:
reapZombies()
}
}
// Start handling signals
go func() {
shutdownCtx, shutdownComplete := context.WithCancel(context.Background())
defer func() {
shutdownComplete()
}()
stopTriggered := false
for {
select {
case sig := <-stopSignals:
if stopTriggered {
continue
}
log.Printf("Signal received: %v", sig)
signal.Stop(stopSignals)
stopTriggered = true
// If a stop signal is received during the startup process continue processing control signals until the main thread marks startup as complete
// Don't close the control channel until the main thread has been allowed to finish spawning processes and marks startup as complete
// Continue to process job control signals to avoid a deadlock
done := false
for !done {
select {
// When the main thread has cancelled the startup context due to completion or an error stop processing control signals
case <-startupCtx.Done():
done = true
// Keep processing control signals until the main thread has finished its startup
case job := <-control:
processControlSignal(job)
}
}
metrics.StopMetricsGathering(log)
// Shutdown queue manager in separate goroutine to allow reaping to continue in parallel
go func() {
_ = stopQueueManager(qmgr)
shutdownComplete()
}()
case <-shutdownCtx.Done():
signal.Stop(reapSignals)
// One final reap
// This occurs after all startup processes have been spawned
reapZombies()
close(control)
// End the goroutine
return
case <-reapSignals:
log.Debug("Received SIGCHLD signal")
reapZombies()
case job := <-control:
processControlSignal(job)
}
}
}()
return control
}
// reapZombies reaps any zombie (terminated) processes now.
// This function should be called before exiting.
func reapZombies() {
for {
var ws unix.WaitStatus
pid, err := unix.Wait4(-1, &ws, unix.WNOHANG, nil)
// If err or pid indicate "no child processes"
if pid == 0 || err == unix.ECHILD {
return
}
log.Debugf("Reaped PID %v", pid)
}
}

View File

@@ -0,0 +1,9 @@
ExitPath:
ExitsDefaultPath=/mnt/mqm/data/exits
ExitsDefaultPath64=/mnt/mqm/data/exits64
Log:
LogPrimaryFiles=3
LogSecondaryFiles=2
LogFilePages=1235
LogBufferPages=0
LogWriteIntegrity=TripleWrite

View File

@@ -0,0 +1,9 @@
ExitPath:
ExitsDefaultPath=/mnt/mqm/data/exits
ExitsDefaultPath64=/mnt/mqm/data/exits64
Log:
LogPrimaryFiles=3
LogSecondaryFiles=2
LogFilePages=2224
LogBufferPages=0
LogWriteIntegrity=TripleWrite

View File

@@ -0,0 +1,9 @@
ExitPath:
ExitsDefaultPath=/mnt/mqm/data/exits
ExitsDefaultPath64=/mnt/mqm/data/exits64
Log:
LogPrimaryFiles=3
LogSecondaryFiles=2
LogFilePages=6002
LogBufferPages=0
LogWriteIntegrity=TripleWrite

View File

@@ -0,0 +1,8 @@
ExitPath:
ExitsDefaultPath=/mnt/mqm/data/exits
ExitsDefaultPath64=/mnt/mqm/data/exits64
Log:
LogPrimaryFiles=3
LogSecondaryFiles=2
LogBufferPages=0
LogWriteIntegrity=TripleWrite

View File

@@ -0,0 +1,8 @@
ExitPath:
ExitsDefaultPath=/mnt/mqm/data/exits
ExitsDefaultPath64=/mnt/mqm/data/exits64
Log:
LogPrimaryFiles=3
LogSecondaryFiles=2
LogBufferPages=1235
LogWriteIntegrity=TripleWrite

View File

@@ -0,0 +1,79 @@
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"strings"
"github.com/ibm-messaging/mq-container/internal/command"
"github.com/ibm-messaging/mq-container/internal/mqversion"
)
var (
// ImageCreated is the date the image was built
ImageCreated = "Not specified"
// ImageRevision is the source control revision identifier
ImageRevision = "Not specified"
// ImageSource is the URL to get source code for building the image
ImageSource = "Not specified"
// ImageTag is the tag of the image
ImageTag = "Not specified"
)
func logDateStamp() {
log.Printf("Image created: %v", ImageCreated)
}
func logGitRepo() {
// log.Printf("Image revision: %v", ImageRevision)
}
func logGitCommit() {
// log.Printf("Image source: %v", ImageSource)
}
func logImageTag() {
log.Printf("Image tag: %v", ImageTag)
}
func logMQVersion() {
mqVersion, err := mqversion.Get()
if err != nil {
log.Printf("Error Getting MQ version: %v", strings.TrimSuffix(string(mqVersion), "\n"))
}
mqBuild, _, err := command.Run("dspmqver", "-b", "-f", "4")
if err != nil {
log.Printf("Error Getting MQ build: %v", strings.TrimSuffix(string(mqBuild), "\n"))
}
mqLicense, _, err := command.Run("dspmqver", "-b", "-f", "8192")
if err != nil {
log.Printf("Error Getting MQ license: %v", strings.TrimSuffix(string(mqLicense), "\n"))
}
log.Printf("MQ version: %v", strings.TrimSuffix(mqVersion, "\n"))
log.Printf("MQ level: %v", strings.TrimSuffix(mqBuild, "\n"))
log.Printf("MQ license: %v", strings.TrimSuffix(mqLicense, "\n"))
}
func logVersionInfo() {
logDateStamp()
logGitRepo()
logGitCommit()
logImageTag()
logMQVersion()
}

View File

@@ -0,0 +1,172 @@
/*
© Copyright IBM Corporation 2018, 2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/ibm-messaging/mq-container/internal/copy"
"github.com/ibm-messaging/mq-container/internal/mqtemplate"
"github.com/ibm-messaging/mq-container/internal/tls"
)
func startWebServer(webKeystore, webkeystorePW, webTruststoreRef string) error {
_, err := os.Stat("/opt/mqm/bin/strmqweb")
if err != nil && os.IsNotExist(err) {
log.Debug("Skipping web server, because it's not installed")
return nil
}
log.Println("Starting web server")
// #nosec G204 - command is fixed, no injection vector
cmd := exec.Command("strmqweb")
// Pass all the environment to MQ Web Server JVM
cmd.Env = os.Environ()
// TLS enabled
if webKeystore != "" {
cmd.Env = append(cmd.Env, "AMQ_WEBKEYSTORE="+webKeystore)
cmd.Env = append(cmd.Env, "AMQ_WEBKEYSTOREPW="+webkeystorePW)
cmd.Env = append(cmd.Env, "AMQ_WEBTRUSTSTOREREF="+webTruststoreRef)
}
out, err := cmd.CombinedOutput()
rc := cmd.ProcessState.ExitCode()
if err != nil {
log.Printf("Error %v starting web server: %v", rc, string(out))
return err
}
log.Println("Started web server")
return nil
}
func configureWebServer(keyLabel string, p12Truststore tls.KeyStoreData) (string, error) {
webKeystore := ""
// Copy server.xml file to ensure that we have the latest expected contents - this file is only populated on QM creation
err := copy.CopyFile("/opt/mqm/samp/web/server.xml", "/var/mqm/web/installations/Installation1/servers/mqweb/server.xml")
if err != nil {
log.Error(err)
return "", err
}
// Configure TLS for the Web Console
err = tls.ConfigureWebTLS(keyLabel, log, p12Truststore.Password)
if err != nil {
return "", err
}
// Configure the Web Keystore
if keyLabel != "" || os.Getenv("MQ_GENERATE_CERTIFICATE_HOSTNAME") != "" {
webKeystore, err = tls.ConfigureWebKeystore(p12Truststore, keyLabel)
if err != nil {
return "", err
}
}
_, err = os.Stat("/opt/mqm/bin/strmqweb")
if err != nil {
if os.IsNotExist(err) {
return "", nil
}
return "", err
}
const webConfigDir string = "/etc/mqm/web"
_, err = os.Stat(webConfigDir)
if err != nil {
if os.IsNotExist(err) {
return "", nil
}
return "", err
}
const prefix string = "/etc/mqm/web"
err = filepath.Walk(prefix, func(from string, info os.FileInfo, err error) error {
if err != nil {
return err
}
to := fmt.Sprintf("/var/mqm/web%v", from[len(prefix):])
exists := true
_, err = os.Stat(to)
if err != nil {
if os.IsNotExist(err) {
exists = false
} else {
return err
}
}
if info.IsDir() {
if !exists {
// #nosec G301 - write group permissions are required
err := os.MkdirAll(to, 0770)
if err != nil {
return err
}
}
} else {
if exists {
err := os.Remove(to)
if err != nil {
return err
}
}
// Use a symlink for file 'mqwebuser.xml', so that if it contains a secret, it doesn't get persisted to a volume
if strings.HasSuffix(from, "/mqwebuser.xml") {
err = os.Symlink(from, to)
if err != nil {
log.Error(err)
return err
}
} else {
err := copy.CopyFile(from, to)
if err != nil {
log.Error(err)
return err
}
}
}
return nil
})
return webKeystore, err
}
// Configure FIPS mode for MQ Web Server
func configureFIPSWebServer(p12TrustStore tls.KeyStoreData) error {
// Need to update jvm.options file of MQ Web Server. We don't update the jvm.options file
// in /etc/mqm/web/installations/Installation1/servers/mqweb directory. Instead we update
// the one in /etc/mqm/web/installations/Installation1/servers/mqweb/configDropins/defaults.
// During runtime MQ Web Server merges the data from two files.
const jvmOptsLink string = "/run/jvm.options"
const jvmOptsTemplate string = "/etc/mqm/web/installations/Installation1/servers/mqweb/configDropins/defaults/jvm.options.tpl"
// Update the jvm.options file using the data from template file. Tell the MQ Web Server
// use a FIPS provider by setting "-Dcom.ibm.jsse2.usefipsprovider=true" and then tell it
// use a specific FIPS provider by setting "Dcom.ibm.jsse2.usefipsProviderName=IBMJCEPlusFIPS".
err := mqtemplate.ProcessTemplateFile(jvmOptsTemplate, jvmOptsLink, map[string]string{
"FipsProvider": "true",
"FipsProviderName": "IBMJCEPlusFIPS",
}, log)
return err
}