diff --git a/image/cli/app-root/src/.bashrc b/image/cli/app-root/src/.bashrc
index 3bacaf4273..6a0f7ac43d 100644
--- a/image/cli/app-root/src/.bashrc
+++ b/image/cli/app-root/src/.bashrc
@@ -32,6 +32,7 @@ echo " - ${TEXT_BOLD}${COLOR_GREEN}mas must-gather${TEXT_RESET} to perform must
echo " - ${TEXT_BOLD}${COLOR_GREEN}mas uninstall${TEXT_RESET} to uninstall a MAS instance"
echo " - ${TEXT_BOLD}${COLOR_GREEN}mas backup${TEXT_RESET} to backup a MAS instance"
echo " - ${TEXT_BOLD}${COLOR_GREEN}mas restore${TEXT_RESET} to restore a MAS instance"
+echo " - ${TEXT_BOLD}${COLOR_GREEN}mas db2ucluster-migration${TEXT_RESET} to migrate Db2uCluster to Db2uInstance"
# None of these functions are tested/supported on s390x /ppc64le yet
if [ $arch != "s390x" ] && [ $arch != "ppc64le" ]; then
diff --git a/image/cli/mascli/mas b/image/cli/mascli/mas
index 9bbe495e9c..d353827734 100755
--- a/image/cli/mascli/mas
+++ b/image/cli/mascli/mas
@@ -372,6 +372,16 @@ case $1 in
mas-cli restore "$@"
;;
+ db2ucluster-migration)
+ echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >> $LOGFILE
+ echo "!! db2ucluster-migration !!" >> $LOGFILE
+ echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >> $LOGFILE
+ echo "${TEXT_UNDERLINE}IBM Maximo Application Suite DB2 Migration Manager (v${VERSION})${TEXT_RESET}"
+ echo "Powered by ${COLOR_CYAN}${TEXT_UNDERLINE}https://github.com/ibm-mas/ansible-devops/${TEXT_RESET} and ${COLOR_CYAN}${TEXT_UNDERLINE}https://tekton.dev/${TEXT_RESET}"
+ shift
+ mas-cli db2ucluster-migration "$@"
+ ;;
+
gitops-bootstrap)
echo "${TEXT_UNDERLINE}IBM Maximo Application Suite GitOps Manager (v${VERSION})${TEXT_RESET}"
echo "Powered by ${COLOR_CYAN}${TEXT_UNDERLINE}https://github.com/ibm-mas/gitops/${TEXT_RESET}"
diff --git a/python/src/mas/cli/__main__.py b/python/src/mas/cli/__main__.py
index 5e495c2aef..65746ec8bf 100644
--- a/python/src/mas/cli/__main__.py
+++ b/python/src/mas/cli/__main__.py
@@ -50,6 +50,7 @@ def usage() -> None:
+ " - mas-cli must-gather Collect diagnostic information\n"
+ " - mas-cli setup-rbac Set up RBAC resources for MAS installation\n"
+ " - mas-cli pre-install Set up pre-install RBAC for MAS\n"
+ + " - mas-cli db2ucluster-migration Migrate Db2uCluster to Db2uInstance\n"
)
)
print_formatted_text(HTML("For usage information run mas-cli [action] --help\n"))
@@ -131,6 +132,12 @@ def main() -> None:
app = MustGatherApp()
app.mustGather(argv[2:])
return
+ if function == "db2ucluster-migration":
+ from mas.cli.db2_migration.app import Db2MigrationApp
+
+ app = Db2MigrationApp()
+ app.migrate(argv[2:])
+ return
if function in ["-h", "--help"]:
usage()
raise SystemExit(0)
diff --git a/python/src/mas/cli/db2_migration/__init__.py b/python/src/mas/cli/db2_migration/__init__.py
new file mode 100644
index 0000000000..a8e4860b7e
--- /dev/null
+++ b/python/src/mas/cli/db2_migration/__init__.py
@@ -0,0 +1,9 @@
+# -----------------------------------------------------------
+# Licensed Materials - Property of IBM
+# 5737-M66
+# (C) Copyright IBM Corp. 2026 All Rights Reserved.
+# US Government Users Restricted Rights - Use, duplication, or disclosure
+# restricted by GSA ADP Schedule Contract with IBM Corp.
+# -----------------------------------------------------------
+
+from ..cli import BaseApp # noqa: F401
diff --git a/python/src/mas/cli/db2_migration/app.py b/python/src/mas/cli/db2_migration/app.py
new file mode 100644
index 0000000000..7d270e33d9
--- /dev/null
+++ b/python/src/mas/cli/db2_migration/app.py
@@ -0,0 +1,220 @@
+# -----------------------------------------------------------
+# Licensed Materials - Property of IBM
+# 5737-M66
+# (C) Copyright IBM Corp. 2026 All Rights Reserved.
+# US Government Users Restricted Rights - Use, duplication, or disclosure
+# restricted by GSA ADP Schedule Contract with IBM Corp.
+# -----------------------------------------------------------
+
+import logging
+
+from typing import List, Dict, Any
+from halo import Halo
+from prompt_toolkit import print_formatted_text, HTML
+from openshift.dynamic.exceptions import NotFoundError
+
+from ..cli import BaseApp
+from .argParser import db2MigrationArgParser
+from mas.devops.ocp import createNamespace
+from mas.devops.tekton import preparePipelinesNamespace, installOpenShiftPipelines, updateTektonDefinitions, launchDb2MigrationPipeline
+
+logger = logging.getLogger(__name__)
+
+
+class Db2MigrationApp(BaseApp):
+ """Application class for Db2uCluster to Db2uInstance migration"""
+
+ def detectDb2uClusters(self, namespace: str) -> List[Dict[str, Any]]:
+ """Detect all Db2uCluster instances in the specified namespace.
+
+ Args:
+ namespace (str): Kubernetes namespace to search
+
+ Returns:
+ List[Dict[str, Any]]: List of Db2uCluster resources found
+ """
+ try:
+ db2ClusterAPI = self.dynamicClient.resources.get(api_version="db2u.databases.ibm.com/v1", kind="Db2uCluster")
+ db2_clusters = db2ClusterAPI.get(namespace=namespace)
+ return db2_clusters.items if db2_clusters else []
+ except NotFoundError:
+ return []
+
+ def promptForDb2Cluster(self, db2_clusters: List[Dict[str, Any]]) -> Dict[str, Any]:
+ """Prompt user to select a Db2uCluster from the detected list.
+
+ Args:
+ db2_clusters (List[Dict[str, Any]]): List of available Db2uCluster resources
+
+ Returns:
+ Dict[str, Any]: Selected Db2uCluster resource
+ """
+ if len(db2_clusters) == 1:
+ db2_cluster = db2_clusters[0]
+ db2ClusterName = db2_cluster.metadata.name
+ self.printHighlight(f"Found 1 Db2uCluster: {db2ClusterName}")
+ return db2_cluster
+
+ # Multiple Db2uClusters - Prompt for selection
+ # self.printH2("Available Db2uClusters")
+ # options = []
+ # for i, db2_cluster in enumerate(db2_clusters):
+ # name = db2_cluster.metadata.name
+ # version = db2_cluster.spec.version if hasattr(db2_cluster.spec, "version") else "unknown"
+ # status = db2_cluster.status.state if hasattr(db2_cluster, "status") and hasattr(db2_cluster.status, "state") else "unknown"
+ # options.append(f"{name} (version: {version}, status: {status})")
+
+ # selectedIndex = self.promptForListSelect("Select Db2uCluster to migrate", options)
+ # return db2_clusters[selectedIndex]
+
+ self.printH2("Available Db2uClusters")
+ for i, db2_cluster in enumerate(db2_clusters):
+ name = db2_cluster.metadata.name
+ version = db2_cluster.spec.version if hasattr(db2_cluster.spec, "version") else "unknown"
+ status = db2_cluster.status.state if hasattr(db2_cluster, "status") and hasattr(db2_cluster.status, "state") else "unknown"
+ print(f" {i+1}. {name} (version: {version}, status: {status})")
+
+ selectedIndex = self.promptForInt("Select Db2uCluster to migrate", min=1, max=len(db2_clusters))
+ return db2_clusters[selectedIndex - 1]
+
+ def promptForBackup(self) -> bool:
+ """Prompt user whether to perform backup before migration.
+
+ Returns:
+ bool: True if backup should be performed, False otherwise
+ """
+ self.printH2("Backup Configuration")
+ print_formatted_text(
+ HTML(
+ "It is strongly recommended to backup before migration.\n"
+ "This will create a full database backup that can be used for rollback.\n"
+ )
+ )
+ return self.yesOrNo("Perform backup before migration")
+
+ def migrate(self, argv: List[str]) -> None:
+ """Main entry point for DB2 migration command.
+
+ Args:
+ argv (List[str]): Command line arguments
+ """
+ args = db2MigrationArgParser.parse_args(argv)
+ self.noConfirm = args.no_confirm
+
+ # Connect to cluster
+ self.connect()
+
+ # Determine mode: interactive vs non-interactive
+ isInteractive = args.namespace is None
+
+ if isInteractive:
+ # Interactive mode
+ self.printH1("Db2uCluster to Db2uInstance Migration")
+
+ # List db2u namespaces
+ with Halo(text="Detecting db2u namespaces", spinner=self.spinner) as h:
+ try:
+ namespaceAPI = self.dynamicClient.resources.get(api_version="v1", kind="Namespace")
+ allNamespaces = namespaceAPI.get()
+ db2uNamespaces = [ns.metadata.name for ns in allNamespaces.items if ns.metadata.name.startswith("db2u")]
+ # v1 = client.CoreV1Api()
+ # allNamespaces = v1.list_namespace()
+ # db2uNamespaces = [ns.metadata.name for ns in allNamespaces.items if ns.metadata.name.startswith("db2u")]
+
+ if db2uNamespaces:
+ h.succeed(f"Found {len(db2uNamespaces)} db2u namespace(s)")
+ print_formatted_text(HTML("Available db2u namespaces:"))
+ for ns in sorted(db2uNamespaces):
+ print(f" - {ns}")
+ print()
+ else:
+ h.info("No db2u namespaces found")
+ except Exception as e:
+ h.fail(f"Failed to list namespaces: {e}")
+
+ # Prompt for namespace with default
+ namespace = self.promptForString("Enter namespace containing Db2uClusters", default="db2u")
+
+ # Detect Db2uClusters
+ with Halo(text=f"Detecting Db2uClusters in namespace {namespace}", spinner=self.spinner) as h:
+ db2_clusters = self.detectDb2uClusters(namespace)
+ if not db2_clusters:
+ h.fail(f"No Db2uClusters found in namespace {namespace}")
+ self.fatalError(f"No Db2uClusters found in namespace {namespace}")
+ h.succeed(f"Found {len(db2_clusters)} Db2uCluster(s)")
+
+ # Select Db2uCluster
+ selectedDb2Cluster = self.promptForDb2Cluster(db2_clusters)
+ db2ClusterName = selectedDb2Cluster.metadata.name
+
+ # Prompt for backup
+ enableBackup = self.promptForBackup()
+
+ else:
+ # Non-interactive mode
+ namespace = args.namespace
+ db2ClusterName = args.db2_cluster_name
+ enableBackup = args.backup == "true" if args.backup else True
+
+ # Validate Db2uCluster exists if name provided
+ if db2ClusterName:
+ db2_clusters = self.detectDb2uClusters(namespace)
+ db2ClusterNames = [c.metadata.name for c in db2_clusters]
+ if db2ClusterName not in db2ClusterNames:
+ self.fatalError(f"Db2uCluster {db2ClusterName} not found in namespace {namespace}")
+ else:
+ # Auto-select if only one Db2uCluster
+ db2_clusters = self.detectDb2uClusters(namespace)
+ if len(db2_clusters) == 0:
+ self.fatalError(f"No Db2uClusters found in namespace {namespace}")
+ elif len(db2_clusters) == 1:
+ db2ClusterName = db2_clusters[0].metadata.name
+ else:
+ self.fatalError("Multiple Db2uClusters found. Please specify --db2-cluster-name")
+
+ # Confirmation
+ if not self.noConfirm:
+ self.printH2("Migration Summary")
+ print_formatted_text(
+ HTML(
+ f"Namespace: {namespace}\n"
+ f"Db2uCluster: {db2ClusterName}\n"
+ f"Backup: {'Enabled' if enableBackup else 'Disabled'}\n"
+ )
+ )
+ if not self.yesOrNo("Proceed with migration"):
+ print_formatted_text(HTML("Migration cancelled"))
+ return
+
+ # Set parameters
+ self.setParam("db2_migration_namespace", namespace)
+ self.setParam("db2_migration_db2_cluster_name", db2ClusterName)
+ self.setParam("db2_migration_backup_enabled", str(enableBackup).lower())
+
+ # Prepare pipeline namespace
+ pipelinesNamespace = "mas-pipelines"
+
+ with Halo(text="Validating OpenShift Pipelines installation", spinner=self.spinner) as h:
+ if installOpenShiftPipelines(self.dynamicClient):
+ h.succeed("OpenShift Pipelines Operator is installed and ready")
+ else:
+ h.fail("OpenShift Pipelines Operator installation failed")
+ self.fatalError("Installation failed")
+
+ with Halo(text=f"Preparing namespace ({pipelinesNamespace})", spinner=self.spinner) as h:
+ createNamespace(self.dynamicClient, pipelinesNamespace)
+ preparePipelinesNamespace(dynClient=self.dynamicClient)
+ h.succeed(f"Namespace {pipelinesNamespace} is ready")
+
+ with Halo(text=f"Installing latest Tekton definitions (v{self.version})", spinner=self.spinner) as h:
+ updateTektonDefinitions(self.dynamicClient, pipelinesNamespace, self.tektonDefsPath)
+ h.succeed(f"Latest Tekton definitions are installed (v{self.version})")
+
+ # Launch pipeline
+ with Halo(text="Submitting PipelineRun for DB2 migration", spinner=self.spinner) as h:
+ pipelineURL = launchDb2MigrationPipeline(dynClient=self.dynamicClient, params=self.params)
+ if pipelineURL:
+ h.succeed("PipelineRun for DB2 migration submitted")
+ print_formatted_text(HTML(f"\nView progress:\n {pipelineURL}\n"))
+ else:
+ h.fail("Failed to submit PipelineRun, see log file for details")
diff --git a/python/src/mas/cli/db2_migration/argParser.py b/python/src/mas/cli/db2_migration/argParser.py
new file mode 100644
index 0000000000..19360ee136
--- /dev/null
+++ b/python/src/mas/cli/db2_migration/argParser.py
@@ -0,0 +1,66 @@
+# -----------------------------------------------------------
+# Licensed Materials - Property of IBM
+# 5737-M66
+# (C) Copyright IBM Corp. 2026 All Rights Reserved.
+# US Government Users Restricted Rights - Use, duplication, or disclosure
+# restricted by GSA ADP Schedule Contract with IBM Corp.
+# -----------------------------------------------------------
+
+import argparse
+import sys
+
+from ..cli import getHelpFormatter
+
+
+class Db2MigrationArgumentParser(argparse.ArgumentParser):
+ """Custom argument parser for DB2 migration command"""
+
+ def format_usage(self):
+ prog = self.prog
+ return (
+ f"Usage (non-interactive mode):\n"
+ f" {prog} --namespace NAMESPACE [--db2-cluster-name DB2_CLUSTER_NAME]\n"
+ f" [--backup {{true,false}}] [--no-confirm]\n"
+ f"\n"
+ f"Usage (interactive mode):\n"
+ f" {prog}\n"
+ f"\n"
+ f"Usage (help):\n"
+ f" {prog} -h\n"
+ )
+
+ def parse_args(self, args=None, namespace=None):
+ parsedArgs = super().parse_args(args, namespace)
+
+ providedArgs = []
+ if args is not None:
+ providedArgs = [arg for arg in args if arg.startswith("-")]
+ else:
+ providedArgs = [arg for arg in sys.argv[1:] if arg.startswith("-")]
+
+ hasAnyArgs = len(providedArgs) > 0
+ hasNamespace = parsedArgs.namespace is not None
+ helpOnly = "--help" in providedArgs or "-h" in providedArgs
+
+ if hasAnyArgs and not hasNamespace and not helpOnly:
+ self.error("non-interactive mode requires --namespace parameter")
+
+ return parsedArgs
+
+
+db2MigrationArgParser = Db2MigrationArgumentParser(
+ prog="mas db2ucluster-migration",
+ description="Migrate Db2uCluster to Db2uInstance by launching the DB2 Migration Tekton Pipeline.",
+ epilog="Refer to the online documentation for more information: https://ibm-mas.github.io/cli/",
+ formatter_class=getHelpFormatter(),
+ add_help=False,
+)
+
+migrationArgGroup = db2MigrationArgParser.add_argument_group("Migration Configuration", "Configure the DB2 migration parameters.")
+migrationArgGroup.add_argument("--namespace", required=False, help="Namespace containing Db2uCluster instances")
+migrationArgGroup.add_argument("--db2-cluster-name", required=False, help="Specific Db2uCluster name to migrate")
+migrationArgGroup.add_argument("--backup", required=False, choices=["true", "false"], help="Enable or disable backup before migration")
+
+otherArgGroup = db2MigrationArgParser.add_argument_group("More", "Additional options.")
+otherArgGroup.add_argument("--no-confirm", required=False, action="store_true", default=False, help="Launch without prompting for confirmation")
+otherArgGroup.add_argument("-h", "--help", action="help", default=False, help="Show this help message and exit")
diff --git a/python/src/mas/cli/must_gather/common/crd_processor.py b/python/src/mas/cli/must_gather/common/crd_processor.py
index 546c7392f9..d8e2061e95 100644
--- a/python/src/mas/cli/must_gather/common/crd_processor.py
+++ b/python/src/mas/cli/must_gather/common/crd_processor.py
@@ -21,7 +21,7 @@
from dataclasses import dataclass
from typing import Dict, List, Tuple, Optional
from kubernetes.dynamic import DynamicClient
-from jsonpath_ng.ext import parse
+from jsonpath_ng.ext.parser import parse
logger = logging.getLogger(__name__)
diff --git a/tekton/generate-tekton-pipelines.yml b/tekton/generate-tekton-pipelines.yml
index ceaa590a00..c597f245d3 100644
--- a/tekton/generate-tekton-pipelines.yml
+++ b/tekton/generate-tekton-pipelines.yml
@@ -22,6 +22,7 @@
- mas-update
- mas-upgrade
- mas-uninstall
+ - mas-devops-db2-migration
# MAS FVT Pipelines
- mas-fvt-assist
- mas-fvt-core
diff --git a/tekton/src/pipelines/mas-devops-db2-migration.yml.j2 b/tekton/src/pipelines/mas-devops-db2-migration.yml.j2
new file mode 100644
index 0000000000..060f149542
--- /dev/null
+++ b/tekton/src/pipelines/mas-devops-db2-migration.yml.j2
@@ -0,0 +1,72 @@
+---
+apiVersion: tekton.dev/v1
+kind: Pipeline
+metadata:
+ name: mas-devops-db2-migration
+spec:
+ workspaces:
+ - name: shared-configs
+ optional: true
+
+ params:
+ # Tekton Pipeline Configuration
+ # -------------------------------------------------------------------------
+ - name: image_pull_policy
+ type: string
+ default: IfNotPresent
+
+ # DB2 Migration Parameters
+ # -------------------------------------------------------------------------
+ - name: db2_action
+ type: string
+ default: "migrate"
+ description: Action for db2 role (must be 'migrate')
+
+ - name: db2_migration_namespace
+ type: string
+ description: Namespace containing Db2uCluster to migrate
+
+ - name: db2_migration_db2_cluster_name
+ type: string
+ description: Name of Db2uCluster to migrate
+
+ - name: db2_migration_backup_enabled
+ type: string
+ default: "true"
+ description: Whether to backup before migration
+
+ - name: db2_migration_cleanup_old_db2_cluster
+ type: string
+ default: "false"
+ description: Whether to delete old Db2uCluster CR after migration
+
+ - name: ibm_entitlement_key
+ type: string
+ default: ""
+ description: IBM Entitlement Key
+
+ tasks:
+ # Run DB2 Migration
+ # -------------------------------------------------------------------------
+ - name: db2-migration
+ timeout: "0"
+ taskRef:
+ kind: Task
+ name: mas-devops-db2
+ params:
+ {{ lookup('template', pipeline_src_dir ~ '/taskdefs/common/cli-params.yml.j2') | indent(8) }}
+ - name: devops_suite_name
+ value: db2-migration
+ - name: db2_action
+ value: $(params.db2_action)
+ - name: db2_migration_namespace
+ value: $(params.db2_migration_namespace)
+ - name: db2_migration_db2_cluster_name
+ value: $(params.db2_migration_db2_cluster_name)
+ - name: db2_migration_backup_enabled
+ value: $(params.db2_migration_backup_enabled)
+ - name: db2_migration_cleanup_old_db2_cluster
+ value: $(params.db2_migration_cleanup_old_db2_cluster)
+ workspaces:
+ - name: configs
+ workspace: shared-configs
diff --git a/tekton/src/tasks/dependencies/db2.yml.j2 b/tekton/src/tasks/dependencies/db2.yml.j2
index 47a033a466..c6a5e78ec0 100644
--- a/tekton/src/tasks/dependencies/db2.yml.j2
+++ b/tekton/src/tasks/dependencies/db2.yml.j2
@@ -219,6 +219,24 @@ spec:
description: S3 secret key for backups
default: ""
+ # DB2 Migration Parameters
+ - name: db2_migration_namespace
+ type: string
+ description: Namespace containing Db2uCluster to migrate
+ default: ""
+ - name: db2_migration_db2_cluster_name
+ type: string
+ description: Name of Db2uCluster to migrate
+ default: ""
+ - name: db2_migration_backup_enabled
+ type: string
+ description: Whether to backup before migration
+ default: "true"
+ - name: db2_migration_cleanup_old_db2_cluster
+ type: string
+ description: Whether to delete old Db2uCluster CR after migration
+ default: "false"
+
stepTemplate:
env:
{{ lookup('template', task_src_dir ~ '/common/cli-env.yml.j2') | indent(6) }}
@@ -362,6 +380,16 @@ spec:
value: $(params.backup_s3_secret_key)
- name: OVERRIDE_STORAGECLASS
value: $(params.override_storageclass)
+
+ # DB2 Migration specific
+ - name: DB2_MIGRATION_NAMESPACE
+ value: $(params.db2_migration_namespace)
+ - name: DB2_MIGRATION_DB2_CLUSTER_NAME
+ value: $(params.db2_migration_db2_cluster_name)
+ - name: DB2_MIGRATION_BACKUP_ENABLED
+ value: $(params.db2_migration_backup_enabled)
+ - name: DB2_MIGRATION_CLEANUP_OLD_DB2_CLUSTER
+ value: $(params.db2_migration_cleanup_old_db2_cluster)
steps:
- name: db2
command: