From 0d9b57e95c0142477a10ace83b12c7d7d4f50b75 Mon Sep 17 00:00:00 2001 From: Thierno-Bah1 Date: Thu, 27 Aug 2026 12:31:59 -0400 Subject: [PATCH 01/13] feat(mcpi-cli-install): add mcpi-install CLI command and Tekton pipeline --- .bob/plans/2025-01-31-mcpi-install.md | 572 ++++++++++++++++++ pytest.ini | 2 + python/src/mas/cli/__main__.py | 6 + python/src/mas/cli/mcpi/__init__.py | 9 + python/src/mas/cli/mcpi/install/__init__.py | 11 + python/src/mas/cli/mcpi/install/app.py | 238 ++++++++ python/src/mas/cli/mcpi/install/argBuilder.py | 90 +++ python/src/mas/cli/mcpi/install/argParser.py | 138 +++++ python/src/mas/cli/mcpi/install/params.py | 47 ++ python/src/mas/cli/mcpi/install/summarizer.py | 87 +++ .../integration/mcpi_install/__init__.py | 9 + .../integration/mcpi_install/conftest.py | 26 + .../mcpi_install/test_mcpi_install_app.py | 117 ++++ tekton/generate-tekton-pipelines.yml | 2 + tekton/generate-tekton-tasks.yml | 11 +- tekton/src/params/install-mcpi.yml.j2 | 14 + tekton/src/params/install.yml.j2 | 4 + tekton/src/pipelines/mas-install.yml.j2 | 10 +- tekton/src/pipelines/mcpi-install.yml.j2 | 126 ++++ .../src/pipelines/taskdefs/mcpi/mcpi.yml.j2 | 37 ++ tekton/src/tasks/mcpi/mcpi.yml.j2 | 105 ++++ 21 files changed, 1659 insertions(+), 2 deletions(-) create mode 100644 .bob/plans/2025-01-31-mcpi-install.md create mode 100644 python/src/mas/cli/mcpi/__init__.py create mode 100644 python/src/mas/cli/mcpi/install/__init__.py create mode 100644 python/src/mas/cli/mcpi/install/app.py create mode 100644 python/src/mas/cli/mcpi/install/argBuilder.py create mode 100644 python/src/mas/cli/mcpi/install/argParser.py create mode 100644 python/src/mas/cli/mcpi/install/params.py create mode 100644 python/src/mas/cli/mcpi/install/summarizer.py create mode 100644 python/tests/integration/mcpi_install/__init__.py create mode 100644 python/tests/integration/mcpi_install/conftest.py create mode 100644 python/tests/integration/mcpi_install/test_mcpi_install_app.py create mode 100644 tekton/src/params/install-mcpi.yml.j2 create mode 100644 tekton/src/pipelines/mcpi-install.yml.j2 create mode 100644 tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2 create mode 100644 tekton/src/tasks/mcpi/mcpi.yml.j2 diff --git a/.bob/plans/2025-01-31-mcpi-install.md b/.bob/plans/2025-01-31-mcpi-install.md new file mode 100644 index 00000000000..98dad8c9e5b --- /dev/null +++ b/.bob/plans/2025-01-31-mcpi-install.md @@ -0,0 +1,572 @@ +# Plan: Add `mcpi-install` Command to MAS CLI + +## Objective + +Add a new `mcpi-install` CLI command that installs the **Maximo Cluster Performance Insights (MCPI)** add-on operator (`ibm-mas-cfg-mcpi`) via a Tekton pipeline. The implementation follows the exact same 4-layer architecture used by `aiservice-install`. + +--- + +## Repos Involved + +| Repo | What changes | +|------|-------------| +| **`cli`** (this repo) | Python CLI module + Tekton templates — Phases 1 & 2 of this plan | +| **`ansible-devops`** | Add `mcpicfg` role to the `ibm.mas_devops` collection so `run-role.sh mcpicfg` works in the CLI image | +| **`python-devops`** (local path: `/Users/tahamadou/Source/MAS/python-devops/`) | Add `launchMcpiInstallPipeline()` + `prepareMcpiPipelinesNamespace()` + 3 new templates (see Design Decisions) | +| **`ibm-mas`** | `mcpicfg` role source already exists at `operator/ibm-mas-cfg-mcpi/roles/mcpicfg/` — no changes needed here, role is copied to `ansible-devops` | + +--- + +## Design Decisions + +### Architecture Layers + +| Layer | Responsibility | +|-------|---------------| +| **Layer 1 — Dispatcher** | `__main__.py` routes `mcpi-install` → `McpiInstallApp().install()` | +| **Layer 2 — Python App** | `python/src/mas/cli/mcpi/install/` — interactive/non-interactive CLI | +| **Layer 3 — Tekton** | Task CRD + pipeline fragment + standalone pipeline + param files | +| **Layer 4 — Ansible** | `mcpicfg` role already exists in `ibm-mas`; invoked via `run-role.sh mcpicfg` | + +### CLI Parameters + +MCPI has no database, no S3, no ODH/RHOAI, no workspace sync. Its parameter set is minimal compared to AI Service: + +| CLI Flag | `dest` / param name | Required | Notes | +|----------|---------------------|----------|-------| +| `-c, --mas-catalog-version` | `mas_catalog_version` | Yes | Catalog selection | +| `--mas-catalog-digest` | `mas_catalog_digest` | No | Dev/airgap catalog | +| `--ibm-entitlement-key` | `ibm_entitlement_key` | Yes | Image pull secret | +| `--mas-instance-id` | `mas_instance_id` | Yes | MAS instance ID (triggers non-interactive) | +| `--mcpi-channel` | `mcpi_channel` | Yes (in non-interactive) | Subscription channel; acts as sentinel guard in pipeline | +| `--routing-mode` | `routing_mode` | No | `subdomain` or `path`; default `subdomain` | +| `--manual-route-mgmt` | `manual_route_mgmt` | No | Default `false` | +| `--license-file` | `license_file` | Yes (non-interactive) | SLS license file path | +| `--sls-namespace` | `sls_namespace` | No | Default `ibm-sls` | +| `--dedicated-sls` | `dedicated_sls` | No | Flag — set SLS ns to `mas--sls` | +| `--contact-email` | `dro_contact_email` | Yes (non-interactive) | DRO contact | +| `--contact-firstname` | `dro_contact_firstname` | Yes (non-interactive) | DRO contact | +| `--contact-lastname` | `dro_contact_lastname` | Yes (non-interactive) | DRO contact | +| `--dro-namespace` | `dro_namespace` | No | DRO namespace | +| `--mongodb-namespace` | `mongodb_namespace` | No | BYO MongoDB namespace | +| `--storage-class-rwo` | `storage_class_rwo` | Yes | Pipeline storage | +| `--storage-class-rwx` | `storage_class_rwx` | Yes | Pipeline storage | +| `--storage-pipeline` | `storage_pipeline` | Yes (non-interactive) | Pipeline PVC class | +| `--storage-accessmode` | `storage_accessmode` | Yes (non-interactive) | ReadWriteMany / ReadWriteOnce | +| `--additional-configs` | `additional_configs` | No | Extra config dir | +| `--artifactory-username` | `artifactory_username` | No | Dev mode only | +| `--artifactory-token` | `artifactory_token` | No | Dev mode only | +| `--image-pull-policy` | `image_pull_policy` | No | Tekton image pull policy | +| `--service-account` | `service_account_name` | No | Custom SA | +| `--slack-token` | `slack_token` | No | Slack integration | +| `--slack-channel` | `slack_channel` | No | Slack integration | +| `--advanced` | `advanced` | No | Show advanced prompts in interactive mode | +| `--simplified` | `simplified` | No | Hide advanced prompts in interactive mode | +| `--accept-license` | `accept_license` | No | Pre-accept license | +| `--dev-mode` | `dev_mode` | No | Dev mode | +| `--skip-pre-check` | `skip_pre_check` | No | Skip pre-install healthcheck | +| `--no-confirm` | `no_confirm` | No | Skip confirmation prompt | +| `-h, --help` | help | No | | + +> **Reason for SLS/DRO params**: Even though MCPI has no per-app entitlement, the pipeline still requires SLS and DRO to be installed as MAS core dependencies. This follows the same pattern as `aiservice-install`. + +> **`routing_mode` and `manual_route_mgmt`**: MCPI-specific params beyond the standard MAS set. These map directly to `ROUTING_MODE` and `MANUAL_ROUTE_MGMT` env vars consumed by the `mcpicfg` Ansible role. + +### requiredParams and optionalParams + +**`requiredParams`:** +```python +[ + "mas_catalog_version", + "mas_instance_id", + "storage_class_rwo", + "storage_class_rwx", + "ibm_entitlement_key", + "mcpi_channel", + "dro_contact_email", + "dro_contact_firstname", + "dro_contact_lastname", +] +``` + +**`optionalParams`:** +```python +[ + "image_pull_policy", + "service_account_name", + "mas_catalog_digest", + "sls_namespace", + "dro_namespace", + "mongodb_namespace", + "routing_mode", + "manual_route_mgmt", + "artifactory_username", + "artifactory_token", + "slack_token", + "slack_channel", +] +``` + +### Pipelines Namespace + +Follows the AI Service pattern exactly: dedicated namespace `mcpi-{mas_instance_id}-pipelines`. + +**A new `prepareMcpiPipelinesNamespace()` function is needed in `python-devops`** (analogous to `prepareAiServicePipelinesNamespace()` at line 731). It requires: +- New template: `mcpi-pipelines-rbac.yml.j2` (clone of `aiservice-pipelines-rbac.yml.j2`, replace `aiservice` with `mcpi`) +- New template: `mcpi-pipelines-pvc.yml.j2` (clone of `aiservice-pipelines-pvc.yml.j2`, replace `aiservice` with `mcpi`) +- Same signature: `prepareMcpiPipelinesNamespace(dynClient, instanceId, storageClass, accessMode, waitForBind=True, configureRBAC=True)` + +### `launchInstallPipeline()` — Confirmed Blocker + +`launchInstallPipeline()` in `python-devops/src/mas/devops/tekton.py` (line 1285) has two hardcoded branches: +- If `mas_instance_id` in params → launches `mas-install` pipeline, namespace `mas-{id}-pipelines` +- Otherwise → launches AI Service install, namespace `aiservice-{id}-pipelines` + +**MCPI needs a new function `launchMcpiInstallPipeline()` in `python-devops`** plus a new Jinja2 template `pipelinerun-mcpi-install.yml.j2`. This is a **Phase 0 prerequisite** that must be done in the `python-devops` repo before Phase 1 of this CLI plan. + +The new function uses `launchPipelineRun()` (generic, line 1247) with: +- `namespace = f"mcpi-{params['mas_instance_id']}-pipelines"` +- `templateName = "pipelinerun-mcpi-install"` +- PipelineRun URL: `f"{getConsoleURL(dynClient)}/k8s/ns/mcpi-{instanceId}-pipelines/tekton.dev~v1beta1~PipelineRun/{instanceId}-install-{timestamp}"` + +The `pipelinerun-mcpi-install.yml.j2` template (in `src/mas/devops/templates/`) includes only MCPI-relevant params: `ibm_entitlement_key`, `mas_instance_id`, `mas_catalog_version`, `mcpi_channel`, `routing_mode`, `manual_route_mgmt`, plus SLS/DRO params and storage class params (trim `pipelinerun-install.yml.j2` to remove Db2/Kafka/CP4D/MAS app sections). + +### Class Hierarchy + +```python +class McpiInstallApp( + BaseApp, + McpiInstallArgBuilderMixin, # from argBuilder.py + McpiInstallSummarizerMixin, # from summarizer.py + InstallSettingsMixin, # reused from install/settings.py + ConfigGeneratorMixin # reused from gencfg.py +) +``` + +### `mcpicfg` Role in `ansible-devops` — Confirmed Blocker + +`run-role.sh mcpicfg` inside the Tekton task calls `ansible-playbook ibm.mas_devops.run_role` which dynamically loads the `ibm.mas_devops.mcpicfg` role. The `mcpicfg` role currently exists **only in the `ibm-mas` operator repo** at `operator/ibm-mas-cfg-mcpi/roles/mcpicfg/`. It is **not present** in `ansible-devops/ibm/mas_devops/roles/`. + +**The `mcpicfg` role must be added to `ansible-devops`** (Phase 0 prerequisite) so it gets bundled into the CLI Docker image. The role must be placed at `ansible-devops/ibm/mas_devops/roles/mcpicfg/` with the same structure as `operator/ibm-mas-cfg-mcpi/roles/mcpicfg/` (tasks/, defaults/, vars/, templates/). + +### Interactive Mode — Trigger + +Presence of `--mas-instance-id` triggers **non-interactive** mode (omitting it triggers interactive mode prompts). This follows the same sentinel pattern as `aiservice-install` where the presence of `--aiservice-instance-id` (mapped to `args.aiservice_instance_id`) determines mode. + +### Tekton Task Env Vars + +The `mcpicfg` role reads these env vars: + +| Env Var | Role Variable | Default | +|---------|--------------|---------| +| `IBM_ENTITLEMENT_KEY` | image pull secret | — | +| `MAS_INSTANCE_ID` | pipeline context | — | +| `MCPI_CHANNEL` | subscription channel | — | +| `ROUTING_MODE` | `routingMode` | `subdomain` | +| `MANUAL_ROUTE_MGMT` | `manualRouteMgmt` | `false` | + +### No gencfg Step + +MCPI has no post-install config generation. The McpiCfg CR is created and managed by the operator itself. No `gencfg-mcpi` task is needed. + +### Standalone vs. `mas-install` Pipeline + +MCPI gets both: +1. A **standalone `mcpi-install` pipeline** for independent MCPI installs +2. The **`mas-install` pipeline** is modified to include MCPI as an optional task guarded by `when: mcpi_channel != ""` + +--- + +## Critical Rules + +- Track progress in this plan file only, not in chat todo lists +- Follow existing code style exactly — look at `aiservice/install/` files as the canonical reference for every structural decision +- Copyright header `# Copyright (c) 2026 IBM Corporation` required in all new Python files +- All new Python files need Google-style docstrings +- Preserve all existing tests — add new tests, do not modify existing ones +- Validate at end of each phase: run `black`, `flake8`, and `pytest` before marking a phase done +- **Phase 0 (`ansible-devops` + `python-devops`) must be complete before Phase 1 begins** — the CLI cannot be completed without `launchMcpiInstallPipeline()` and the `mcpicfg` role in the collection + +--- + +## Execution Plan + +### Phase 0 — Cross-Repo Prerequisites +[x] **Status:** complete + +**Intent:** Two changes in external repos are required before the CLI code can be completed. These must be opened as PRs in their respective repos and merged (or at least available as dev branches) before Phase 1 can be validated end-to-end. + +**Expected Outcomes:** +- `ansible-devops` has a `mcpicfg` role under `ibm/mas_devops/roles/mcpicfg/` +- `python-devops` exposes `launchMcpiInstallPipeline()`, `prepareMcpiPipelinesNamespace()`, `pipelinerun-mcpi-install.yml.j2`, `mcpi-pipelines-rbac.yml.j2`, `mcpi-pipelines-pvc.yml.j2` + +**Todo:** + +- [x] **0.1** In the **`ansible-devops`** repo: Copy the `mcpicfg` role from `ibm-mas`: + - Source: `/Users/tahamadou/Source/MAS/ibm-mas/operator/ibm-mas-cfg-mcpi/roles/mcpicfg/` + - Destination: `/Users/tahamadou/Source/MAS/ansible-devops/ibm/mas_devops/roles/mcpicfg/` + - Copy all subdirectories: `tasks/`, `defaults/`, `vars/`, `templates/` + - Verify the role loads: `ansible-galaxy collection build` should succeed + +- [x] **0.2** In **`python-devops`** (`/Users/tahamadou/Source/MAS/python-devops/`): Add MCPI namespace templates: + - `src/mas/devops/templates/mcpi-pipelines-rbac.yml.j2` — clone of `aiservice-pipelines-rbac.yml.j2` with `aiservice` → `mcpi` + - `src/mas/devops/templates/mcpi-pipelines-pvc.yml.j2` — clone of `aiservice-pipelines-pvc.yml.j2` with `aiservice` → `mcpi` + +- [x] **0.3** In **`python-devops`**: Add `prepareMcpiPipelinesNamespace()` to `src/mas/devops/tekton.py`: + - After `prepareAiServicePipelinesNamespace()` (line 731) + - Same signature: `prepareMcpiPipelinesNamespace(dynClient, instanceId, storageClass, accessMode, waitForBind=True, configureRBAC=True)` + - Uses `namespace = f"mcpi-{instanceId}-pipelines"` + - Uses templates `mcpi-pipelines-rbac.yml.j2` and `mcpi-pipelines-pvc.yml.j2` + - Renders with `mcpi_instance_id=instanceId` (not `aiservice_instance_id`) + +- [x] **0.4** In **`python-devops`**: Add `pipelinerun-mcpi-install.yml.j2` to `src/mas/devops/templates/`: + - Based on `pipelinerun-install.yml.j2` trimmed to MCPI scope: + - `metadata.name: "{{mas_instance_id}}-install-{{ timestamp }}"` + - `labels.tekton.dev/pipeline: mcpi-install` + - `spec.pipelineRef.name: mcpi-install` + - Params: `ibm_entitlement_key`, `storage_class_rwx`, `storage_class_rwo`, SLS params, DRO params, `mas_catalog_version` (+conditional `mas_catalog_digest`), `mas_instance_id`, `mcpi_channel`, conditional `routing_mode`, conditional `manual_route_mgmt`, conditional `artifactory_username/token`, conditional `image_pull_policy`, conditional `skip_pre_check` + - Workspaces: `shared-configs`, `shared-entitlement`, `shared-pod-templates`, `shared-additional-configs`, `shared-certificates` (same as `pipelinerun-install.yml.j2`, no `shared-db2` or `shared-aiservice-config`) + +- [x] **0.5** In **`python-devops`**: Add `launchMcpiInstallPipeline()` to `src/mas/devops/tekton.py`: + - After `launchInstallPipeline()` (line 1285) + - Signature: `launchMcpiInstallPipeline(dynClient: DynamicClient, params: dict) -> str` + - Body: + ```python + instanceId = params["mas_instance_id"] + namespace = f"mcpi-{instanceId}-pipelines" + timestamp = launchPipelineRun(dynClient, namespace, "pipelinerun-mcpi-install", params) + return f"{getConsoleURL(dynClient)}/k8s/ns/mcpi-{instanceId}-pipelines/tekton.dev~v1beta1~PipelineRun/{instanceId}-install-{timestamp}" + ``` + +**Relevant Context:** +- `launchInstallPipeline` source: `python-devops/src/mas/devops/tekton.py:1285` +- `prepareAiServicePipelinesNamespace` source: `python-devops/src/mas/devops/tekton.py:731` +- `launchPipelineRun` generic: `python-devops/src/mas/devops/tekton.py:1247` +- `pipelinerun-install.yml.j2` (trim for mcpi): `python-devops/src/mas/devops/templates/pipelinerun-install.yml.j2` +- `aiservice-pipelines-rbac.yml.j2` (clone): `python-devops/src/mas/devops/templates/aiservice-pipelines-rbac.yml.j2` +- `aiservice-pipelines-pvc.yml.j2` (clone): `python-devops/src/mas/devops/templates/aiservice-pipelines-pvc.yml.j2` +- `mcpicfg` role source: `/Users/tahamadou/Source/MAS/ibm-mas/operator/ibm-mas-cfg-mcpi/roles/mcpicfg/` +- `ansible-devops` roles dir: `/Users/tahamadou/Source/MAS/ansible-devops/ibm/mas_devops/roles/` + +--- + +### Phase 1 — Python CLI Module +[x] **Status:** complete + +**Intent:** Create the `python/src/mas/cli/mcpi/install/` module with all 6 files and wire it into the dispatcher. This is the UI layer that collects user input, validates it, builds the pipeline command, and launches the Tekton pipeline. + +**Expected Outcomes:** +- `mas-cli mcpi-install --help` works and shows all flags +- Non-interactive mode runs end-to-end with mocked Tekton calls +- All files are lint-clean and tested + +**Todo:** + +- [ ] **1.1** Create `python/src/mas/cli/mcpi/__init__.py`: + ```python + # Copyright header (2026) + # Empty — package marker + ``` + +- [ ] **1.2** Create `python/src/mas/cli/mcpi/install/__init__.py`: + - Mirror `python/src/mas/cli/aiservice/install/__init__.py` exactly: + ```python + from ...cli import BaseApp # noqa: F401 + ``` + +- [ ] **1.4** Create `python/src/mas/cli/mcpi/install/params.py`: + - `requiredParams` and `optionalParams` as defined in Design Decisions above + +- [ ] **1.5** Create `python/src/mas/cli/mcpi/install/argParser.py`: + - Module-level `argparse.ArgumentParser` instance named `mcpiInstallArgParser` + - Program: `"mas mcpi-install"` + - Argument groups (in order): + 1. `MAS Catalog Selection & Entitlement` — `--mas-catalog-version`, `--mas-catalog-digest`, `--ibm-entitlement-key` + 2. `MCPI Basic Configuration` — `-i, --mas-instance-id` + 3. `MCPI Advanced Configuration` — `--routing-mode`, `--manual-route-mgmt` + 4. `MAS Advanced Configuration` — `--additional-configs` + 5. `Storage` — `--storage-class-rwo`, `--storage-class-rwx`, `--storage-pipeline`, `--storage-accessmode` + 6. `IBM Suite License Service` — `--license-file`, `--sls-namespace`, `--dedicated-sls` + 7. `IBM Data Reporting Operator (DRO)` — `--contact-email`, `--contact-firstname`, `--contact-lastname`, `--dro-namespace` + 8. `MongoDb Community Operator` — `--mongodb-namespace` + 9. `MAS Applications` — `--mcpi-channel` + 10. `Development Mode` — `--artifactory-username`, `--artifactory-token` + 11. `More` — `--advanced`, `--simplified`, `--accept-license`, `--dev-mode`, `--skip-pre-check`, `--no-confirm`, `--image-pull-policy`, `--service-account`, `--slack-token`, `--slack-channel`, `-h/--help` + - `isValidFile()` helper for `--license-file` validation (mirror aiservice pattern) + +- [ ] **1.6** Create `python/src/mas/cli/mcpi/install/argBuilder.py`: + - Class `McpiInstallArgBuilderMixin` with `buildCommand(self) -> str` + - Build a re-runnable shell command starting with `export IBM_ENTITLEMENT_KEY=x` + - Command: `mas mcpi-install --mas-catalog-version ... --ibm-entitlement-key $IBM_ENTITLEMENT_KEY ...` + - Include all set params in the same style as `aiServiceInstallArgBuilderMixin.buildCommand()` + - End with `--accept-license --no-confirm` + +- [ ] **1.7** Create `python/src/mas/cli/mcpi/install/summarizer.py`: + - Class `McpiInstallSummarizerMixin` + - Methods: + - `ocpSummary()` — pipeline config, OCP architecture, storage classes + - `mcpiSummary()` — catalog version, MAS instance ID, MCPI channel, routing mode, manual route mgmt + - `droSummary()` — DRO email, first/last name, namespace + - `slsSummary()` — SLS namespace, license file + - `mongoSummary()` — MongoDB namespace (BYO vs. install) + - `slackSummary()` — Slack channel + - `displayInstallSummary()` — orchestrates all summaries; logs `yaml.dump(self.params)` at DEBUG; calls `getConsoleURL(self.dynamicClient)` for connected cluster line + +- [ ] **1.8** Create `python/src/mas/cli/mcpi/install/app.py`: + - Class `McpiInstallApp(BaseApp, McpiInstallArgBuilderMixin, McpiInstallSummarizerMixin, InstallSettingsMixin, ConfigGeneratorMixin)` + - Import `mcpiInstallArgParser` from `.argParser` + - Import `requiredParams, optionalParams` from `.params` + - Import `McpiInstallArgBuilderMixin` from `.argBuilder` + - Import `McpiInstallSummarizerMixin` from `.summarizer` + - Import from `mas.devops.tekton`: `installOpenShiftPipelines`, `updateTektonDefinitions`, `prepareMcpiPipelinesNamespace`, `prepareInstallSecrets`, `testCLI`, `launchMcpiInstallPipeline` (added in Phase 0) + - `logMethodCall` decorator (mirror aiservice pattern) + - Key methods: + - `configMcpi(self)` — prompts for MAS instance ID (using `InstanceIDFormatValidator`), MCPI channel, routing mode; mirrors `configAibroker()` from aiservice + - `interactiveMode(self, simplified: bool, advanced: bool) -> None` — catalog, storage, SLS, DRO, ICR credentials, cert manager, MCPI config + - `nonInteractiveMode(self) -> None` — iterates over `vars(self.args)`, handles `requiredParams`, `optionalParams`, `license_file`, `dedicated_sls`, `storage_pipeline`, `storage_accessmode`, `mongodb_namespace`, `additional_configs`, `mcpi_channel`, `approval_*`, and ignore-list keys (`accept_license`, `dev_mode`, etc.) + - `install(self, argv: list) -> int` — main entry: parses args, detects interactive vs non-interactive via `args.mas_instance_id`, connects, runs mode, calls `slsLicenseFile()`, calls `buildCommand()` + `displayInstallSummary()`, confirms, prepares namespace and launches pipeline + - Pipelines namespace: `mcpi-{self.getParam('mas_instance_id')}-pipelines` + - Namespace prep: `prepareMcpiPipelinesNamespace(dynClient=..., instanceId=self.getParam('mas_instance_id'), ...)` + +- [ ] **1.9** Modify `python/src/mas/cli/__main__.py`: + - After line 76 (the `aiservice-install` block), add: + ```python + if function == "mcpi-install": + from mas.cli.mcpi.install.app import McpiInstallApp + + app = McpiInstallApp() + raise SystemExit(app.install(argv[2:])) + ``` + - In `usage()`, add to the MAS Management Actions string (after the `aiservice-install` line if it exists, otherwise after the last install-like command): + ```python + + " - mas-cli mcpi-install Install Maximo Cluster Performance Insights\n" + ``` + +- [ ] **1.10** Write integration tests in `python/tests/integration/mcpi_install/`: + - Create `__init__.py` (empty with copyright header) + - Create `conftest.py` — mirrors `aiservice_install/conftest.py`, auto-applies `mcpi-install` marker + - Create `test_mcpi_install_app.py`: + - `test_install_noninteractive(tmpdir)` — full non-interactive happy path: + - Mocks: `DynamicClient`, `getNodes`, `isAirgapInstall`, `getCurrentCatalog`, `installOpenShiftPipelines`, `updateTektonDefinitions`, `prepareAiServicePipelinesNamespace`, `launchMcpiInstallPipeline`, `prepareInstallSecrets`, `testCLI` + - Passes all required flags: `--mas-catalog-version`, `--ibm-entitlement-key`, `--mas-instance-id`, `--storage-class-rwo`, `--storage-class-rwx`, `--storage-pipeline`, `--storage-accessmode`, `--license-file`, `--contact-email`, `--contact-firstname`, `--contact-lastname`, `--mcpi-channel`, `--accept-license`, `--no-confirm` + - Asserts `launchInstallPipeline` (or equivalent) was called once + +- [ ] **1.11** Validate Phase 1: + ``` + black python/src/mas/cli/mcpi/ python/tests/integration/mcpi_install/ --line-length 160 --check + flake8 python/src/mas/cli/mcpi/ python/tests/integration/mcpi_install/ --max-line-length 160 + .venv/bin/pytest python/tests/integration/mcpi_install/ -v + ``` + +**Relevant Context:** +- Reference app: [`python/src/mas/cli/aiservice/install/app.py`](python/src/mas/cli/aiservice/install/app.py:70) +- Reference argParser: [`python/src/mas/cli/aiservice/install/argParser.py`](python/src/mas/cli/aiservice/install/argParser.py:25) +- Reference argBuilder: [`python/src/mas/cli/aiservice/install/argBuilder.py`](python/src/mas/cli/aiservice/install/argBuilder.py:16) +- Reference summarizer: [`python/src/mas/cli/aiservice/install/summarizer.py`](python/src/mas/cli/aiservice/install/summarizer.py:18) +- Reference params: [`python/src/mas/cli/aiservice/install/params.py`](python/src/mas/cli/aiservice/install/params.py) +- Reference __init__: [`python/src/mas/cli/aiservice/install/__init__.py`](python/src/mas/cli/aiservice/install/__init__.py) +- Dispatcher: [`python/src/mas/cli/__main__.py`](python/src/mas/cli/__main__.py:72) +- Test pattern: [`python/tests/integration/aiservice_install/test_app.py`](python/tests/integration/aiservice_install/test_app.py:26) +- Test conftest: [`python/tests/integration/aiservice_install/conftest.py`](python/tests/integration/aiservice_install/conftest.py) +- `prepareAiServicePipelinesNamespace` call: [`python/src/mas/cli/aiservice/install/app.py`](python/src/mas/cli/aiservice/install/app.py:579) + +--- + +### Phase 2 — Tekton Definitions +[x] **Status:** complete + +**Intent:** Create all Tekton Jinja2 template sources needed to deploy and run the MCPI install pipeline. + +**Expected Outcomes:** +- `ansible-playbook tekton/generate-tekton-tasks.yml` produces `target/tasks/mcpi.yaml` +- `ansible-playbook tekton/generate-tekton-pipelines.yml` produces `target/pipelines/mcpi-install.yaml` +- `target/pipelines/mas-install.yaml` includes the MCPI task, guarded by `when: mcpi_channel != ""` +- All generated YAML is valid Tekton syntax + +**Todo:** + +- [ ] **2.1** Create `tekton/src/tasks/mcpi/mcpi.yml.j2`: + - `apiVersion: tekton.dev/v1`, `kind: Task`, `metadata.name: mas-devops-mcpi` + - `spec.params`: + - `{{ lookup('template', task_src_dir ~ '/common/cli-params.yml.j2') | indent(4) }}` + - `artifactory_username`, `artifactory_token` (pre-release, empty defaults) + - `ibm_entitlement_key` (type: string) + - `mas_instance_id` (type: string) + - `mcpi_channel` (type: string, description: "Catalog channel for the MCPI operator subscription") + - `routing_mode` (type: string, default: "", description: "Routing mode for MCPI: subdomain or path") + - `manual_route_mgmt` (type: string, default: "", description: "Disable automatic route management") + - `mas_icr_cp` (type: string, default: "") + - `mas_icr_cpopen` (type: string, default: "") + - `custom_labels` (type: string, default: "", description: "Optional MAS custom labels, comma separated list of key=value pairs") + - `spec.stepTemplate.env`: + - `{{ lookup('template', task_src_dir ~ '/common/cli-env.yml.j2') | indent(4) }}` + - `IBM_ENTITLEMENT_KEY: $(params.ibm_entitlement_key)` + - `MAS_INSTANCE_ID: $(params.mas_instance_id)` + - `MCPI_CHANNEL: $(params.mcpi_channel)` + - `ROUTING_MODE: $(params.routing_mode)` + - `MANUAL_ROUTE_MGMT: $(params.manual_route_mgmt)` + - `MAS_ICR_CP: $(params.mas_icr_cp)` + - `MAS_ICR_CPOPEN: $(params.mas_icr_cpopen)` + - `CUSTOM_LABELS: $(params.custom_labels)` + - `ARTIFACTORY_USERNAME: $(params.artifactory_username)` + - `ARTIFACTORY_TOKEN: $(params.artifactory_token)` + - `spec.steps` — single step named `mcpi`: + - `image: quay.io/ibmmas/cli:latest` + - `command: ["/opt/app-root/src/run-role.sh", "mcpicfg"]` + - `workingDir: /workspace/configs` + - `spec.workspaces`: `- name: configs` (optional: true) + +- [ ] **2.2** Create `tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2`: + - Task fragment used in both `mas-install.yml.j2` and `mcpi-install.yml.j2` + - `- name: mcpi`, `timeout: "0"` + - `params`: + - `{{ lookup('template', 'taskdefs/common/cli-params.yml.j2') | indent(4) }}` + - `- name: devops_suite_name`, `value: mcpi` + - `- name: artifactory_username`, `value: $(params.artifactory_username)` + - `- name: artifactory_token`, `value: $(params.artifactory_token)` + - `- name: ibm_entitlement_key`, `value: $(params.ibm_entitlement_key)` + - `- name: custom_labels`, `value: $(params.custom_labels)` + - `- name: mas_instance_id`, `value: $(params.mas_instance_id)` + - `- name: mcpi_channel`, `value: "$(params.mcpi_channel)"` + - `- name: routing_mode`, `value: $(params.routing_mode)` + - `- name: manual_route_mgmt`, `value: $(params.manual_route_mgmt)` + - `taskRef`: `name: mas-devops-mcpi`, `kind: Task` + - `when`: `input: "$(params.mcpi_channel)"`, `operator: notin`, `values: [""]` + - `workspaces`: `- name: configs`, `workspace: shared-configs` + +- [ ] **2.3** Create `tekton/src/params/install-mcpi.yml.j2`: + ```yaml + # MAS Add-on Configuration - Maximo Cluster Performance Insights (MCPI) + # ----------------------------------------------------------------------------- + - name: mcpi_channel + type: string + description: Default channel for IBM Maximo Cluster Performance Insights operator subscription + default: "" + - name: routing_mode + type: string + description: "Routing mode for MCPI: subdomain or path" + default: "" + - name: manual_route_mgmt + type: string + description: Disable automatic route management for MCPI + default: "" + ``` + +- [ ] **2.4** Modify `tekton/src/params/install.yml.j2`: + - After line 844 (the end of the AI Service section: `{{ lookup('template', params_src_dir ~ '/install-aiservice.yml.j2') }}`), add: + ```yaml + + # MCPI Configuration + # ----------------------------------------------------------------------------- + {{ lookup('template', params_src_dir ~ '/install-mcpi.yml.j2') }} + ``` + +- [ ] **2.5** Create `tekton/src/pipelines/mcpi-install.yml.j2`: + - Standalone install pipeline for MCPI-only installs + - Mirror the structure of `aiservice-upgrade.yml.j2` adapted for a fresh install + - `metadata.name: mcpi-install` + - Workspaces: `shared-configs`, `shared-pod-templates` + - Params: + - `{{ lookup('template', params_src_dir ~ '/common.yml.j2') | indent(4) }}` + - `- name: mas_instance_id` (type: string) + - `- name: ibm_entitlement_key` (type: string) + - `{{ lookup('template', params_src_dir ~ '/install-mcpi.yml.j2') | indent(4) }}` + - Tasks: + 1. `pipeline-start` — `mas-devops-update-pipeline-status` (started) + 2. `ibm-catalogs` — runs after `pipeline-start` + 3. `cert-manager` — runs after `ibm-catalogs` + 4. `sls` — runs after `cert-manager` + 5. `dro` — runs after `sls` + 6. `mcpi` (via `{{ lookup('template', pipeline_src_dir ~ '/taskdefs/mcpi/mcpi.yml.j2') | indent(4) }}`) — runs after `sls`, `dro` + 7. `post-install-verify` (via `ocp-verify.yml.j2` taskdef) — runs after `mcpi` + - `finally`: `sync-install` (`mas-devops-update-configmap`) + `pipeline-finish` (`mas-devops-update-pipeline-status`) + +- [ ] **2.6** Modify `tekton/src/pipelines/mas-install.yml.j2`: + - After the aiservice section (after the `gencfg-aiservice` block, which ends around line 464), add a numbered section: + ``` + # 15. Install and configure MCPI (Maximo Cluster Performance Insights) + # ------------------------------------------------------------------------- + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/mcpi/mcpi.yml.j2') | indent(4) }} + runAfter: + - suite-config + ``` + - Add `- mcpi` to the `post-install-verify` task's `runAfter` list (currently ends at line 479 with `- aiservice`) + +- [ ] **2.7** Modify `tekton/generate-tekton-tasks.yml`: + - Add a new block after the AI Service tasks section (after line 57): + ```yaml + # 3b. Generate Tasks (MCPI) + # ------------------------------------------------------------------------- + - name: Generate Tasks (MCPI) + ansible.builtin.template: + src: "{{ task_src_dir }}/mcpi/{{ item }}.yml.j2" + dest: "{{ task_target_dir }}/{{ item }}.yaml" + with_items: + - mcpi + ``` + +- [ ] **2.8** Modify `tekton/generate-tekton-pipelines.yml`: + - Add after `# AI Service Pipelines` block (after line 47): + ```yaml + # MCPI Pipelines + - mcpi-install + ``` + +- [ ] **2.9** Validate Phase 2: + ```bash + cd tekton + ansible-playbook generate-tekton-tasks.yml + ansible-playbook generate-tekton-pipelines.yml + ``` + - Confirm `target/tasks/mcpi.yaml` generated and syntactically valid YAML + - Confirm `target/pipelines/mcpi-install.yaml` generated + - Confirm `target/pipelines/mas-install.yaml` contains the `mcpi` task block + +**Relevant Context:** +- Task template reference: [`tekton/src/tasks/aiservice/aiservice.yml.j2`](tekton/src/tasks/aiservice/aiservice.yml.j2) +- Pipeline fragment reference: [`tekton/src/pipelines/taskdefs/aiservice/aiservice.yml.j2`](tekton/src/pipelines/taskdefs/aiservice/aiservice.yml.j2) +- Params reference: [`tekton/src/params/install-aiservice.yml.j2`](tekton/src/params/install-aiservice.yml.j2) +- Standalone pipeline reference: [`tekton/src/pipelines/aiservice-upgrade.yml.j2`](tekton/src/pipelines/aiservice-upgrade.yml.j2) +- Install pipeline: [`tekton/src/pipelines/mas-install.yml.j2`](tekton/src/pipelines/mas-install.yml.j2:436) +- Params file: [`tekton/src/params/install.yml.j2`](tekton/src/params/install.yml.j2:842) +- Generate tasks: [`tekton/generate-tekton-tasks.yml`](tekton/generate-tekton-tasks.yml:45) +- Generate pipelines: [`tekton/generate-tekton-pipelines.yml`](tekton/generate-tekton-pipelines.yml:46) + +--- + +## Final Validation + +After both phases are complete: + +1. **Lint & format:** + ``` + black python/src/mas/cli/mcpi/ python/tests/integration/mcpi_install/ --line-length 160 --check + flake8 python/src/mas/cli/mcpi/ python/tests/integration/mcpi_install/ --max-line-length 160 + ``` + +2. **Tests (no regressions):** + ``` + .venv/bin/pytest python/tests/integration/mcpi_install/ -v + .venv/bin/pytest python/tests/ -v + ``` + +3. **CLI smoke test:** + ``` + mas-cli mcpi-install --help + ``` + Confirm all flags display correctly. + +4. **Tekton generation:** + ``` + cd tekton + ansible-playbook generate-tekton-tasks.yml + ansible-playbook generate-tekton-pipelines.yml + ``` + Confirm `target/tasks/mcpi.yaml`, `target/pipelines/mcpi-install.yaml` are generated and `target/pipelines/mas-install.yaml` includes the MCPI task. + +**Success Criteria:** +- `mas-cli mcpi-install` appears in usage output +- All integration tests pass +- Tekton YAML contains `mas-devops-mcpi` task and `mcpi-install` pipeline +- MCPI task appears in `mas-install` pipeline guarded by `when: mcpi_channel != ""` +- No regressions in existing test suite diff --git a/pytest.ini b/pytest.ini index 1a713740de5..93dcd8d2138 100644 --- a/pytest.ini +++ b/pytest.ini @@ -9,6 +9,8 @@ markers = install: Installation tests (mas-cli install) aiservice-install: AI Service installation tests (mas-cli aiservice-install) aiservice_install: AI Service installation tests (mas-cli aiservice-install) + mcpi-install: MCPI installation tests (mas-cli mcpi-install) + mcpi_install: MCPI installation tests (mas-cli mcpi-install) mirror: Mirror/catalog tests (mas-cli mirror-*) update: Update tests (mas-cli update) upgrade: Upgrade tests (mas-cli upgrade) diff --git a/python/src/mas/cli/__main__.py b/python/src/mas/cli/__main__.py index 5e495c2aef0..e7b068a3f95 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 mcpi-install Install Maximo Cluster Performance Insights\n" ) ) print_formatted_text(HTML("For usage information run mas-cli [action] --help\n")) @@ -74,6 +75,11 @@ def main() -> None: app = AiServiceInstallApp() raise SystemExit(app.install(argv[2:])) + if function == "mcpi-install": + from mas.cli.mcpi.install.app import McpiInstallApp + + app = McpiInstallApp() + raise SystemExit(app.install(argv[2:])) if function == "aiservice-upgrade": from mas.cli.aiservice.upgrade.app import AiServiceUpgradeApp diff --git a/python/src/mas/cli/mcpi/__init__.py b/python/src/mas/cli/mcpi/__init__.py new file mode 100644 index 00000000000..10b45d523a5 --- /dev/null +++ b/python/src/mas/cli/mcpi/__init__.py @@ -0,0 +1,9 @@ +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** diff --git a/python/src/mas/cli/mcpi/install/__init__.py b/python/src/mas/cli/mcpi/install/__init__.py new file mode 100644 index 00000000000..7166ef936fe --- /dev/null +++ b/python/src/mas/cli/mcpi/install/__init__.py @@ -0,0 +1,11 @@ +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** + +from ...cli import BaseApp # noqa: F401 diff --git a/python/src/mas/cli/mcpi/install/app.py b/python/src/mas/cli/mcpi/install/app.py new file mode 100644 index 00000000000..9518647d7ff --- /dev/null +++ b/python/src/mas/cli/mcpi/install/app.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** + +import logging + +from sys import exit +from prompt_toolkit import print_formatted_text, HTML +from halo import Halo + +from ...cli import BaseApp +from ...gencfg import ConfigGeneratorMixin +from .argBuilder import McpiInstallArgBuilderMixin +from .argParser import mcpiInstallArgParser +from .summarizer import McpiInstallSummarizerMixin +from .params import requiredParams, optionalParams + +from ...install.settings import InstallSettingsMixin + +from mas.devops.ocp import createNamespace +from mas.devops.tekton import ( + installOpenShiftPipelines, + updateTektonDefinitions, + prepareMcpiPipelinesNamespace, + prepareInstallSecrets, + testCLI, + launchMcpiInstallPipeline, +) + +logger = logging.getLogger(__name__) + + +def logMethodCall(func): + def wrapper(self, *args, **kwargs): + logger.debug(f">>> McpiInstallApp.{func.__name__}") + result = func(self, *args, **kwargs) + logger.debug(f"<<< McpiInstallApp.{func.__name__}") + return result + + return wrapper + + +class McpiInstallApp(BaseApp, McpiInstallArgBuilderMixin, McpiInstallSummarizerMixin, InstallSettingsMixin, ConfigGeneratorMixin): + """CLI App for installing Maximo Cluster Performance Insights (MCPI).""" + + @logMethodCall + def configMcpi(self) -> None: + """Prompt for MCPI-specific settings in interactive mode.""" + self.printH1("MCPI Configuration") + self.promptForString("MAS instance ID", "mas_instance_id") + self.promptForString("MCPI subscription channel", "mcpi_channel") + + @logMethodCall + def nonInteractiveMode(self) -> None: + """Process all CLI arguments for non-interactive install. + + Raises: + SystemExit: If a required parameter is missing or invalid. + """ + self.isInteractiveMode = False + self.storageClassProvider = "custom" + self.slsLicenseFileLocal = None + + for key, value in vars(self.args).items(): + if key in requiredParams: + if value is None: + self.fatalError(f"{key} must be set") + self.setParam(key, value) + + elif key in optionalParams: + if value is not None: + self.setParam(key, value) + + elif key == "license_file": + if value is not None and value != "": + self.slsLicenseFileLocal = value + self.setParam("sls_action", "install") + + elif key == "dedicated_sls": + if value: + self.setParam("sls_namespace", f"mas-{self.args.mas_instance_id}-sls") + + elif key == "storage_accessmode": + if value is None: + self.fatalError(f"{key} must be set") + self.pipelineStorageAccessMode = value + + elif key == "storage_pipeline": + if value is None: + self.fatalError(f"{key} must be set") + self.pipelineStorageClass = value + + elif key == "additional_configs": + self.localConfigDir = value + + elif key == "mongodb_namespace": + if value is not None and value != "": + self.setParam(key, value) + self.setParam("sls_mongodb_cfg_file", f"/workspace/configs/mongo-{value}.yml") + + elif key in [ + "accept_license", + "dev_mode", + "skip_pre_check", + "no_confirm", + "help", + "advanced", + "simplified", + ]: + pass + + else: + self.fatalError(f"Unknown option: {key} {value}") + + if self.slsLicenseFileLocal is None: + self.fatalError("--license-file must be set for new SLS install") + + @logMethodCall + def install(self, argv) -> int: + """Install Maximo Cluster Performance Insights. + + Args: + argv (list): Command-line arguments to parse. + + Returns: + int: Exit code (0 on success). + """ + args = mcpiInstallArgParser.parse_args(args=argv) + + instanceId = args.mas_instance_id + self.noConfirm = args.no_confirm + self.licenseAccepted = args.accept_license + self.devMode = args.dev_mode + + if args.image_pull_policy and args.image_pull_policy != "": + self.setParam("image_pull_policy", args.image_pull_policy) + + if args.skip_pre_check: + self.setParam("skip_pre_check", "true") + + self.args = args + + if instanceId is None: + self.printH1("Set Target OpenShift Cluster") + self.connect() + else: + logger.debug("MAS instance ID is set, assuming already connected to the desired OCP cluster") + self.lookupTargetArchitecture() + + if self.dynamicClient is None: + print_formatted_text(HTML("Error: Not successfully connected to a Kubernetes cluster. See log file for details")) + exit(1) + + self.isAirgap() + + self.setParam("dro_action", "install") + + if instanceId is None: + self.interactiveMode(simplified=args.simplified, advanced=args.advanced) + else: + self.nonInteractiveMode() + + self.slsLicenseFile() + + self.printH1("Non-Interactive Install Command") + self.printDescription( + ["Save and re-use the following script to re-run this install without needing to answer the interactive prompts again", "", self.buildCommand()] + ) + + self.displayInstallSummary() + + if not self.noConfirm: + print() + self.printDescription(["Please carefully review your choices above, correcting mistakes now is much easier than after the install has begun"]) + continueWithInstall = self.yesOrNo("Proceed with these settings") + + if self.noConfirm or continueWithInstall: + self.createTektonFileWithDigest() + + self.printH1("Launch Install") + pipelinesNamespace = f"mcpi-{self.getParam('mas_instance_id')}-pipelines" + + with Halo(text="Validating OpenShift Pipelines installation", spinner=self.spinner) as h: + if installOpenShiftPipelines(self.dynamicClient, self.getParam("storage_class_rwx")): + h.stop_and_persist(symbol=self.successIcon, text="OpenShift Pipelines Operator is installed and ready to use") + else: + h.stop_and_persist(symbol=self.successIcon, text="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) + prepareMcpiPipelinesNamespace( + dynClient=self.dynamicClient, + instanceId=self.getParam("mas_instance_id"), + storageClass=self.pipelineStorageClass, + accessMode=self.pipelineStorageAccessMode, + configureRBAC=(self.getParam("service_account_name") == ""), + ) + prepareInstallSecrets( + dynClient=self.dynamicClient, + namespace=pipelinesNamespace, + slsLicenseFile=self.slsLicenseFileSecret, + additionalConfigs=self.additionalConfigsSecret, + podTemplates=self.podTemplatesSecret, + certs=self.certsSecret, + slack_token=self.getParam("slack_token"), + slack_channel=self.getParam("slack_channel"), + ) + h.stop_and_persist(symbol=self.successIcon, text=f"Namespace is ready ({pipelinesNamespace})") + + with Halo(text="Testing availability of MAS CLI image in cluster", spinner=self.spinner) as h: + testCLI() + h.stop_and_persist(symbol=self.successIcon, text="MAS CLI image deployment test completed") + + with Halo(text=f"Installing latest Tekton definitions (v{self.version})", spinner=self.spinner) as h: + updateTektonDefinitions(self.dynamicClient, pipelinesNamespace, self.tektonDefsPath) + h.stop_and_persist(symbol=self.successIcon, text=f"Latest Tekton definitions are installed (v{self.version})") + + with Halo(text=f"Submitting PipelineRun for {self.getParam('mas_instance_id')} MCPI install", spinner=self.spinner) as h: + pipelineURL = launchMcpiInstallPipeline(dynClient=self.dynamicClient, params=self.params) + if pipelineURL is not None: + h.stop_and_persist(symbol=self.successIcon, text=f"PipelineRun for {self.getParam('mas_instance_id')} install submitted") + print_formatted_text(HTML(f"\nView progress:\n {pipelineURL}\n")) + else: + h.stop_and_persist( + symbol=self.failureIcon, + text=f"Failed to submit PipelineRun for {self.getParam('mas_instance_id')} install, see log file for details", + ) + print() + + return 0 diff --git a/python/src/mas/cli/mcpi/install/argBuilder.py b/python/src/mas/cli/mcpi/install/argBuilder.py new file mode 100644 index 00000000000..c15aad5eefb --- /dev/null +++ b/python/src/mas/cli/mcpi/install/argBuilder.py @@ -0,0 +1,90 @@ +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** + +import logging + +logger = logging.getLogger(__name__) + + +class McpiInstallArgBuilderMixin: + """Mixin that builds the equivalent non-interactive CLI command string.""" + + def buildCommand(self) -> str: + """Build a re-runnable mas mcpi-install shell command from current params. + + Returns: + str: Shell command string that reproduces the current configuration. + """ + newline = " \\\n" + command = "export IBM_ENTITLEMENT_KEY=x\n" + if self.getParam("artifactory_username") != "": + command += "export ARTIFACTORY_USERNAME=x\nexport ARTIFACTORY_TOKEN=x\n" + + command += f"mas mcpi-install --mas-catalog-version {self.getParam('mas_catalog_version')}" + + if self.getParam("mas_catalog_digest") != "": + command += f" --mas-catalog-digest {self.getParam('mas_catalog_digest')}" + + command += f" --ibm-entitlement-key $IBM_ENTITLEMENT_KEY{newline}" + + # MAS Instance ID + command += f" --mas-instance-id \"{self.getParam('mas_instance_id')}\"{newline}" + + # MCPI channel & routing + command += f" --mcpi-channel \"{self.getParam('mcpi_channel')}\"{newline}" + if self.getParam("routing_mode") != "": + command += f" --routing-mode \"{self.getParam('routing_mode')}\"{newline}" + if self.getParam("manual_route_mgmt") != "": + command += f" --manual-route-mgmt \"{self.getParam('manual_route_mgmt')}\"{newline}" + + # MAS Advanced Configuration + if self.localConfigDir is not None: + command += f' --additional-configs "{self.localConfigDir}"{newline}' + + # Storage + command += f" --storage-class-rwo \"{self.getParam('storage_class_rwo')}\"" + command += f" --storage-class-rwx \"{self.getParam('storage_class_rwx')}\"{newline}" + command += f' --storage-pipeline "{self.pipelineStorageClass}"' + command += f' --storage-accessmode "{self.pipelineStorageAccessMode}"{newline}' + + # IBM Suite License Service + if self.getParam("sls_namespace") and self.getParam("sls_namespace") != "ibm-sls": + if self.getParam("sls_namespace") == f"mas-{self.getParam('mas_instance_id')}-sls": + command += " --dedicated-sls" + else: + command += f" --sls-namespace \"{self.getParam('sls_namespace')}\"" + if self.slsLicenseFileLocal: + command += f' --license-file "{self.slsLicenseFileLocal}"' + if (self.getParam("sls_namespace") and self.getParam("sls_namespace") != "ibm-sls") or self.slsLicenseFileLocal: + command += newline + + # IBM Data Reporting Operator (DRO) + command += f" --contact-email \"{self.getParam('dro_contact_email')}\"" + command += f" --contact-firstname \"{self.getParam('dro_contact_firstname')}\"" + command += f" --contact-lastname \"{self.getParam('dro_contact_lastname')}\"{newline}" + if self.getParam("dro_namespace") != "": + command += f" --dro-namespace \"{self.getParam('dro_namespace')}\"{newline}" + + # Development Mode + if self.getParam("artifactory_username") != "": + command += f" --artifactory-username $ARTIFACTORY_USERNAME --artifactory-token $ARTIFACTORY_TOKEN{newline}" + + # More Options + if self.devMode: + command += f" --dev-mode{newline}" + if self.getParam("skip_pre_check") is True: + command += f" --skip-pre-check{newline}" + if self.getParam("image_pull_policy") != "": + command += f" --image-pull-policy {self.getParam('image_pull_policy')}{newline}" + if self.getParam("service_account_name") != "": + command += f" --service-account {self.getParam('service_account_name')}{newline}" + + command += " --accept-license --no-confirm" + return command diff --git a/python/src/mas/cli/mcpi/install/argParser.py b/python/src/mas/cli/mcpi/install/argParser.py new file mode 100644 index 00000000000..1071d943e89 --- /dev/null +++ b/python/src/mas/cli/mcpi/install/argParser.py @@ -0,0 +1,138 @@ +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** + +import argparse +from os import path + +from ... import __version__ as packageVersion +from ...cli import getHelpFormatter + + +def isValidFile(parser, arg) -> str: + """Validate that a file path exists. + + Args: + parser (argparse.ArgumentParser): The argument parser instance + arg (str): File path to validate + + Returns: + str: The validated file path + """ + if not path.exists(arg): + parser.error(f"Error: The file {arg} does not exist") + else: + return arg + + +mcpiInstallArgParser = argparse.ArgumentParser( + prog="mas mcpi-install", + description="\n".join( + [ + f"IBM Maximo Application Suite Admin CLI v{packageVersion}", + "Install Maximo Cluster Performance Insights (MCPI) by configuring and launching the Tekton Pipeline.\n", + "Interactive Mode:", + "Omitting the --mas-instance-id option will trigger an interactive prompt", + ] + ), + epilog="Refer to the online documentation for more information: https://ibm-mas.github.io/cli/", + formatter_class=getHelpFormatter(), + add_help=False, +) + +# MAS Catalog Selection & Entitlement +# ----------------------------------------------------------------------------- +catArgGroup = mcpiInstallArgParser.add_argument_group("MAS Catalog Selection & Entitlement") +catArgGroup.add_argument("-c", "--mas-catalog-version", required=False, help="IBM Maximo Operator Catalog to install") +catArgGroup.add_argument( + "--mas-catalog-digest", required=False, help="IBM Maximo Operator Catalog Digest, only required when installing development catalog sources" +) +catArgGroup.add_argument("--ibm-entitlement-key", required=False, help="IBM entitlement key") + +# MCPI Basic Configuration +# ----------------------------------------------------------------------------- +mcpiArgGroup = mcpiInstallArgParser.add_argument_group("MCPI Basic Configuration") +mcpiArgGroup.add_argument("-i", "--mas-instance-id", required=False, dest="mas_instance_id", help="MAS Instance ID") + +# MCPI Advanced Configuration +# ----------------------------------------------------------------------------- +mcpiAdvancedArgGroup = mcpiInstallArgParser.add_argument_group("MCPI Advanced Configuration") +mcpiAdvancedArgGroup.add_argument("--mcpi-channel", required=False, dest="mcpi_channel", help="Subscription channel for Maximo Cluster Performance Insights") +mcpiAdvancedArgGroup.add_argument( + "--routing-mode", required=False, dest="routing_mode", choices=["subdomain", "path"], help="Routing mode for MCPI (subdomain or path)" +) +mcpiAdvancedArgGroup.add_argument("--manual-route-mgmt", required=False, dest="manual_route_mgmt", help="Disable automatic route management for MCPI") + +# MAS Advanced Configuration +# ----------------------------------------------------------------------------- +masAdvancedArgGroup = mcpiInstallArgParser.add_argument_group("MAS Advanced Configuration") +masAdvancedArgGroup.add_argument("--additional-configs", required=False, help="Path to a directory containing additional configuration files to be applied") + +# Storage +# ----------------------------------------------------------------------------- +storageArgGroup = mcpiInstallArgParser.add_argument_group("Storage") +storageArgGroup.add_argument("--storage-class-rwo", required=False, help="ReadWriteOnce (RWO) storage class (e.g. ibmc-block-gold)") +storageArgGroup.add_argument("--storage-class-rwx", required=False, help="ReadWriteMany (RWX) storage class (e.g. ibmc-file-gold-gid)") +storageArgGroup.add_argument("--storage-pipeline", required=False, help="Install pipeline storage class (e.g. ibmc-file-gold-gid)") +storageArgGroup.add_argument( + "--storage-accessmode", + required=False, + help="Install pipeline storage class access mode (ReadWriteMany or ReadWriteOnce)", + choices=["ReadWriteMany", "ReadWriteOnce"], +) + +# IBM Suite License Service +# ----------------------------------------------------------------------------- +slsArgGroup = mcpiInstallArgParser.add_argument_group("IBM Suite License Service") +slsArgGroup.add_argument("--license-file", required=False, help="Path to MAS license file", type=lambda x: isValidFile(mcpiInstallArgParser, x)) +slsArgGroup.add_argument("--sls-namespace", required=False, help="Customize the SLS install namespace", default="ibm-sls") +slsArgGroup.add_argument("--dedicated-sls", action="store_true", default=False, help="Set the SLS namespace to mas--sls") + +# IBM Data Reporting Operator (DRO) +# ----------------------------------------------------------------------------- +droArgGroup = mcpiInstallArgParser.add_argument_group("IBM Data Reporting Operator (DRO)") +droArgGroup.add_argument("--contact-email", "--uds-email", dest="dro_contact_email", required=False, help="Contact e-mail address") +droArgGroup.add_argument("--contact-firstname", "--uds-firstname", dest="dro_contact_firstname", required=False, help="Contact first name") +droArgGroup.add_argument("--contact-lastname", "--uds-lastname", dest="dro_contact_lastname", required=False, help="Contact last name") +droArgGroup.add_argument("--dro-namespace", required=False, help="Namespace for the Data Reporting Operator") + +# MongoDb Community Operator +# ----------------------------------------------------------------------------- +mongoArgGroup = mcpiInstallArgParser.add_argument_group("MongoDb Community Operator") +mongoArgGroup.add_argument("--mongodb-namespace", required=False, help="Namespace for the MongoDB Community Operator") + +# Development Mode +# ----------------------------------------------------------------------------- +devArgGroup = mcpiInstallArgParser.add_argument_group("Development Mode") +devArgGroup.add_argument("--artifactory-username", required=False, help="Username for access to development builds on Artifactory") +devArgGroup.add_argument("--artifactory-token", required=False, help="API Token for access to development builds on Artifactory") + +# More Options +# ----------------------------------------------------------------------------- +otherArgGroup = mcpiInstallArgParser.add_argument_group("More") +otherArgGroup.add_argument("--advanced", action="store_true", default=False, help="Show advanced install options (in interactive mode)") +otherArgGroup.add_argument("--simplified", action="store_true", default=False, help="Don't show advanced install options (in interactive mode)") +otherArgGroup.add_argument("--accept-license", action="store_true", default=False, help="Accept all license terms without prompting") +otherArgGroup.add_argument("--dev-mode", required=False, action="store_true", default=False, help="Configure installation for development mode") +otherArgGroup.add_argument("--skip-pre-check", required=False, action="store_true", help="Disable the 'pre-install-check' at the start of the install pipeline") +otherArgGroup.add_argument("--no-confirm", required=False, action="store_true", default=False, help="Launch the install without prompting for confirmation") +otherArgGroup.add_argument( + "--image-pull-policy", dest="image_pull_policy", required=False, help="Manually set the image pull policy used in the Tekton Pipeline" +) +otherArgGroup.add_argument( + "--service-account", + dest="service_account_name", + required=False, + help="Run the install pipeline under a custom service account", +) +otherArgGroup.add_argument("--slack-token", dest="slack_token", required=False, help="Slack bot token for sending pipeline notifications") +otherArgGroup.add_argument( + "--slack-channel", dest="slack_channel", required=False, help="Slack channel(s) for notifications (comma-separated for multiple channels)" +) +otherArgGroup.add_argument("-h", "--help", action="help", default=False, help="Show this help message and exit") diff --git a/python/src/mas/cli/mcpi/install/params.py b/python/src/mas/cli/mcpi/install/params.py new file mode 100644 index 00000000000..ee1f2804f01 --- /dev/null +++ b/python/src/mas/cli/mcpi/install/params.py @@ -0,0 +1,47 @@ +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** + +requiredParams = [ + # MAS + "mas_catalog_version", + "mas_instance_id", + # Storage classes + "storage_class_rwo", + "storage_class_rwx", + # Entitlement + "ibm_entitlement_key", + # MCPI channel + "mcpi_channel", + # DRO + "dro_contact_email", + "dro_contact_firstname", + "dro_contact_lastname", +] + +optionalParams = [ + # Pipeline + "image_pull_policy", + "service_account_name", + # Catalogue + "mas_catalog_digest", + # SLS + "sls_namespace", + # DRO + "dro_namespace", + # MCPI routing + "routing_mode", + "manual_route_mgmt", + # Dev Mode + "artifactory_username", + "artifactory_token", + # Notifications + "slack_token", + "slack_channel", +] diff --git a/python/src/mas/cli/mcpi/install/summarizer.py b/python/src/mas/cli/mcpi/install/summarizer.py new file mode 100644 index 00000000000..c7a58560ef3 --- /dev/null +++ b/python/src/mas/cli/mcpi/install/summarizer.py @@ -0,0 +1,87 @@ +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** + +import logging +import yaml +from mas.devops.ocp import getConsoleURL + +logger = logging.getLogger(__name__) + + +class McpiInstallSummarizerMixin: + """Mixin that renders the pre-launch settings review.""" + + def ocpSummary(self) -> None: + """Print OpenShift platform and pipeline configuration summary.""" + self.printH2("Pipeline Configuration") + self.printParamSummary("Service Account", "service_account_name") + self.printParamSummary("Image Pull Policy", "image_pull_policy") + self.printSummary("Skip Pre-Install Healthcheck", "Yes" if self.getParam("skip_pre_check") == "true" else "No") + + self.printH2("OpenShift Container Platform") + self.printSummary("Worker Node Architecture", self.architecture) + self.printSummary("Storage Class Provider", self.storageClassProvider) + self.printParamSummary("ReadWriteOnce Storage Class", "storage_class_rwo") + self.printParamSummary("ReadWriteMany Storage Class", "storage_class_rwx") + self.printParamSummary("Certificate Manager", "cert_manager_provider") + + def mcpiSummary(self) -> None: + """Print MCPI operator configuration summary.""" + self.printH2("Maximo Operator Catalog") + self.printParamSummary("Catalog Version", "mas_catalog_version") + if self.getParam("mas_catalog_digest") != "": + self.printParamSummary("Catalog Digest", "mas_catalog_digest") + + self.printH2("Maximo Cluster Performance Insights (MCPI)") + self.printParamSummary("MAS Instance ID", "mas_instance_id") + self.printParamSummary("Subscription Channel", "mcpi_channel") + if self.getParam("routing_mode") != "": + self.printParamSummary("Routing Mode", "routing_mode") + if self.getParam("manual_route_mgmt") != "": + self.printParamSummary("Manual Route Management", "manual_route_mgmt") + + def droSummary(self) -> None: + """Print DRO configuration summary.""" + self.printH2("IBM Data Reporter Operator (DRO) Configuration") + self.printParamSummary("Contact e-mail", "dro_contact_email") + self.printParamSummary("First name", "dro_contact_firstname") + self.printParamSummary("Last name", "dro_contact_lastname") + self.printParamSummary("Install Namespace", "dro_namespace") + + def slsSummary(self) -> None: + """Print SLS configuration summary.""" + self.printH2("IBM Suite License Service") + self.printParamSummary("Namespace", "sls_namespace") + if self.getParam("sls_action") == "install": + self.printSummary("Subscription Channel", "3.x") + if self.slsLicenseFileLocal: + self.printSummary("License File", self.slsLicenseFileLocal) + + def slackSummary(self) -> None: + """Print Slack integration summary.""" + self.printH2("Slack Integration") + if self.getParam("slack_channel") != "": + self.printParamSummary("Slack Channel", "slack_channel") + else: + self.printSummary("Slack Channel", "Not Configured") + + def displayInstallSummary(self) -> None: + """Render the full pre-launch settings review.""" + self.printH1("Review Settings") + self.printDescription(["Connected to:", f" - {getConsoleURL(self.dynamicClient)}"]) + + logger.debug("PipelineRun parameters:") + logger.debug(yaml.dump(self.params, default_flow_style=False)) + + self.ocpSummary() + self.mcpiSummary() + self.droSummary() + self.slsSummary() + self.slackSummary() diff --git a/python/tests/integration/mcpi_install/__init__.py b/python/tests/integration/mcpi_install/__init__.py new file mode 100644 index 00000000000..10b45d523a5 --- /dev/null +++ b/python/tests/integration/mcpi_install/__init__.py @@ -0,0 +1,9 @@ +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** diff --git a/python/tests/integration/mcpi_install/conftest.py b/python/tests/integration/mcpi_install/conftest.py new file mode 100644 index 00000000000..aef0b08cd98 --- /dev/null +++ b/python/tests/integration/mcpi_install/conftest.py @@ -0,0 +1,26 @@ +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** + +"""Conftest for mcpi_install tests. + +Automatically applies the 'mcpi-install' marker to all tests in this directory and subdirectories. +""" + +import pytest +from pathlib import Path + + +def pytest_collection_modifyitems(config, items): + """Auto-apply mcpi-install marker to all tests in this directory.""" + mcpi_install_dir = Path(__file__).parent + for item in items: + item_path = Path(item.fspath) + if mcpi_install_dir in item_path.parents or item_path.parent == mcpi_install_dir: + item.add_marker(pytest.mark.mcpi_install) diff --git a/python/tests/integration/mcpi_install/test_mcpi_install_app.py b/python/tests/integration/mcpi_install/test_mcpi_install_app.py new file mode 100644 index 00000000000..a25f51f2472 --- /dev/null +++ b/python/tests/integration/mcpi_install/test_mcpi_install_app.py @@ -0,0 +1,117 @@ +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** + +"""Integration tests for McpiInstallApp non-interactive mode. + +Tests verify that the CLI correctly collects parameters and launches the +Tekton MCPI install pipeline. +""" + +from unittest import mock +from unittest.mock import MagicMock + +from kubernetes.dynamic import DynamicClient +from kubernetes.dynamic.exceptions import NotFoundError +from kubernetes.client.exceptions import ApiException + +from mas.cli.mcpi.install.app import McpiInstallApp + + +def test_install_noninteractive(tmpdir): + """Test full non-interactive MCPI install completes without error. + + GIVEN all required CLI flags are provided (mas-catalog-version, ibm-entitlement-key, + mas-instance-id, storage classes, license-file, contact details, mcpi-channel) + WHEN McpiInstallApp.install() is called with those flags and --no-confirm + THEN launchMcpiInstallPipeline is called exactly once and returns without error. + """ + tmpdir.join("authorized_entitlement.lic").write("testLicense") + + with mock.patch("mas.cli.cli.config"): + dynamicClient = MagicMock(DynamicClient) + resources = MagicMock() + dynamicClient.resources = resources + + routesApi = MagicMock() + catalogApi = MagicMock() + crdApi = MagicMock() + namespaceApi = MagicMock() + clusterRoleBindingApi = MagicMock() + pvcApi = MagicMock() + secretApi = MagicMock() + + resourceApis = { + "CatalogSource": catalogApi, + "Route": routesApi, + "CustomResourceDefinition": crdApi, + "Namespace": namespaceApi, + "ClusterRoleBinding": clusterRoleBindingApi, + "PersistentVolumeClaim": pvcApi, + "Secret": secretApi, + } + resources.get.side_effect = lambda **kwargs: resourceApis.get(kwargs["kind"], None) + + route = MagicMock() + route.spec = MagicMock() + route.spec.host = "maximo.ibm.com" + routesApi.get.return_value = route + catalogApi.get.side_effect = NotFoundError(ApiException(status="404")) + + with ( + mock.patch("mas.cli.cli.DynamicClient") as mockDynamicClientClass, + mock.patch("mas.cli.cli.getNodes") as mockGetNodes, + mock.patch("mas.cli.cli.isAirgapInstall") as mockIsAirgapInstall, + mock.patch("mas.cli.mcpi.install.app.installOpenShiftPipelines"), + mock.patch("mas.cli.mcpi.install.app.updateTektonDefinitions"), + mock.patch("mas.cli.mcpi.install.app.prepareMcpiPipelinesNamespace"), + mock.patch("mas.cli.mcpi.install.app.prepareInstallSecrets"), + mock.patch("mas.cli.mcpi.install.app.testCLI"), + mock.patch("mas.cli.mcpi.install.app.launchMcpiInstallPipeline") as mockLaunchPipeline, + ): + mockDynamicClientClass.return_value = dynamicClient + mockGetNodes.return_value = [{"status": {"nodeInfo": {"architecture": "amd64"}}}] + mockIsAirgapInstall.return_value = False + mockLaunchPipeline.return_value = "https://pipeline.test.maximo.ibm.com" + + with mock.patch("mas.cli.cli.isSNO") as mockIsSno: + mockIsSno.return_value = False + app = McpiInstallApp() + app.install( + [ + "--mas-catalog-version", + "v9-250828-amd64", + "--ibm-entitlement-key", + "testEntitlementKey", + "--mas-instance-id", + "test1", + "--storage-class-rwo", + "nfs-client", + "--storage-class-rwx", + "nfs-client", + "--storage-pipeline", + "nfs-client", + "--storage-accessmode", + "ReadWriteMany", + "--license-file", + f"{tmpdir}/authorized_entitlement.lic", + "--contact-email", + "maximo@ibm.com", + "--contact-firstname", + "Test", + "--contact-lastname", + "User", + "--mcpi-channel", + "v9.2", + "--accept-license", + "--no-confirm", + ] + ) + + mockLaunchPipeline.assert_called_once() diff --git a/tekton/generate-tekton-pipelines.yml b/tekton/generate-tekton-pipelines.yml index ceaa590a00d..e52c371aebf 100644 --- a/tekton/generate-tekton-pipelines.yml +++ b/tekton/generate-tekton-pipelines.yml @@ -43,6 +43,8 @@ - mas-fvt-launcher - mas-ivt-manage - mas-fvt-facilities + # MCPI Pipelines + - mcpi-install # AI Service Pipelines - aiservice-upgrade # AI Service FVT Pipelines diff --git a/tekton/generate-tekton-tasks.yml b/tekton/generate-tekton-tasks.yml index df80d8d9a95..54416006c4a 100644 --- a/tekton/generate-tekton-tasks.yml +++ b/tekton/generate-tekton-tasks.yml @@ -42,7 +42,16 @@ - sls - turbonomic - # 3. Generate Tasks (AI Service) + # 3. Generate Tasks (MCPI) + # ------------------------------------------------------------------------- + - name: Generate Tasks (MCPI) + ansible.builtin.template: + src: "{{ task_src_dir }}/mcpi/{{ item }}.yml.j2" + dest: "{{ task_target_dir }}/{{ item }}.yaml" + with_items: + - mcpi + + # 4. Generate Tasks (AI Service) # ------------------------------------------------------------------------- - name: Generate Tasks (AI Service) ansible.builtin.template: diff --git a/tekton/src/params/install-mcpi.yml.j2 b/tekton/src/params/install-mcpi.yml.j2 new file mode 100644 index 00000000000..a1f02ccb7f3 --- /dev/null +++ b/tekton/src/params/install-mcpi.yml.j2 @@ -0,0 +1,14 @@ +# MAS Add-on Configuration - Maximo Cluster Performance Insights (MCPI) +# ----------------------------------------------------------------------------- +- name: mcpi_channel + type: string + description: Default channel for IBM Maximo Cluster Performance Insights operator subscription + default: "" +- name: routing_mode + type: string + description: "Routing mode for MCPI: subdomain or path" + default: "" +- name: manual_route_mgmt + type: string + description: Disable automatic route management for MCPI + default: "" diff --git a/tekton/src/params/install.yml.j2 b/tekton/src/params/install.yml.j2 index caf2ef6cd6d..77bd966bb7f 100644 --- a/tekton/src/params/install.yml.j2 +++ b/tekton/src/params/install.yml.j2 @@ -843,3 +843,7 @@ # ----------------------------------------------------------------------------- {{ lookup('template', params_src_dir ~ '/install-aiservice.yml.j2') }} +# MCPI Configuration +# ----------------------------------------------------------------------------- +{{ lookup('template', params_src_dir ~ '/install-mcpi.yml.j2') }} + diff --git a/tekton/src/pipelines/mas-install.yml.j2 b/tekton/src/pipelines/mas-install.yml.j2 index 7b90335b556..f0a7803f7ef 100644 --- a/tekton/src/pipelines/mas-install.yml.j2 +++ b/tekton/src/pipelines/mas-install.yml.j2 @@ -50,6 +50,7 @@ spec: # 12. Install & Configure Visual Inspection # 13. Install & Configure Facilities # 14. Install & Configure AI Service with Default Tenant + # 15. Install & Configure MCPI - name: pipeline-start timeout: "0" @@ -463,7 +464,13 @@ spec: operator: notin values: ["", "9.0.*", "9.1.*"] - # 15. Verify health of the cluster before we consider the install complete + # 15. Install and configure MCPI (Maximo Cluster Performance Insights) + # ------------------------------------------------------------------------- + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/mcpi/mcpi.yml.j2') | indent(4) }} + runAfter: + - suite-config + + # 16. Verify health of the cluster before we consider the install complete # ------------------------------------------------------------------------- {{ lookup('template', pipeline_src_dir ~ '/taskdefs/cluster-setup/ocp-verify.yml.j2', template_vars={'name': 'post-install-verify', 'devops_suite_name': 'post-install-verify'}) | indent(4) }} runAfter: @@ -477,6 +484,7 @@ spec: - eck - cognos - aiservice + - mcpi finally: # Update synchronization configmap diff --git a/tekton/src/pipelines/mcpi-install.yml.j2 b/tekton/src/pipelines/mcpi-install.yml.j2 new file mode 100644 index 00000000000..2134cc1a142 --- /dev/null +++ b/tekton/src/pipelines/mcpi-install.yml.j2 @@ -0,0 +1,126 @@ +--- +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: mcpi-install +spec: + workspaces: + # The generated configuration files + - name: shared-configs + # Any pre-generated configs that will be copied into the shared-configs workspace during suite-install + - name: shared-additional-configs + # The SLS entitlement key file that will be installed during install-sls + - name: shared-entitlement + # Pre-generated certificates + - name: shared-certificates + # PodTemplates configurations + - name: shared-pod-templates + + params: + # 1. Common Parameters + # ------------------------------------------------------------------------- + {{ lookup('template', params_src_dir ~ '/common.yml.j2') | indent(4) }} + + # 2. Installation + # ------------------------------------------------------------------------- + {{ lookup('template', params_src_dir ~ '/install-common.yml.j2') | indent(4) }} + + - name: mas_instance_id + type: string + description: MAS Instance ID + + - name: ibm_entitlement_key + type: string + description: IBM entitlement key + + # MCPI Add-on Parameters + # ------------------------------------------------------------------------- + {{ lookup('template', params_src_dir ~ '/install-mcpi.yml.j2') | indent(4) }} + + tasks: + - name: pipeline-start + timeout: "0" + taskRef: + kind: Task + name: mas-devops-update-pipeline-status + params: + - name: image_pull_policy + value: $(params.image_pull_policy) + - name: pipeline_status + value: Started + - name: pipeline_name + value: $(context.pipeline.name) + - name: pipelinerun_name + value: $(context.pipelineRun.name) + - name: pipelinerun_namespace + value: $(context.pipelineRun.namespace) + + # 1. IBM Operator Catalog + # ------------------------------------------------------------------------- + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/cluster-setup/ibm-catalogs.yml.j2') | indent(4) }} + runAfter: + - pipeline-start + + # 2. Certificate Manager + # ------------------------------------------------------------------------- + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/cluster-setup/cert-manager.yml.j2') | indent(4) }} + runAfter: + - ibm-catalogs + + # 3. IBM SLS + # ------------------------------------------------------------------------- + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/sls.yml.j2') | indent(4) }} + runAfter: + - cert-manager + + # 4. DRO + # ------------------------------------------------------------------------- + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/dro.yml.j2') | indent(4) }} + runAfter: + - ibm-catalogs + + # 5. Install and configure MCPI + # ------------------------------------------------------------------------- + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/mcpi/mcpi.yml.j2') | indent(4) }} + runAfter: + - sls + - dro + + # 6. Verify health of the cluster + # ------------------------------------------------------------------------- + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/cluster-setup/ocp-verify.yml.j2', template_vars={'name': 'post-install-verify', 'devops_suite_name': 'post-install-verify'}) | indent(4) }} + runAfter: + - mcpi + + finally: + # Update synchronization configmap + # ------------------------------------------------------------------------- + - name: sync-install + timeout: "0" + taskRef: + kind: Task + name: mas-devops-update-configmap + params: + - name: image_pull_policy + value: $(params.image_pull_policy) + - name: configmap_name + value: sync-install + - name: configmap_value + value: $(tasks.status) + + - name: pipeline-finish + timeout: "0" + taskRef: + kind: Task + name: mas-devops-update-pipeline-status + params: + - name: image_pull_policy + value: $(params.image_pull_policy) + - name: pipeline_status + value: $(tasks.status) + - name: pipeline_name + value: $(context.pipeline.name) + - name: pipelinerun_name + value: $(context.pipelineRun.name) + - name: pipelinerun_namespace + value: $(context.pipelineRun.namespace) diff --git a/tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2 b/tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2 new file mode 100644 index 00000000000..1be10a16c10 --- /dev/null +++ b/tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2 @@ -0,0 +1,37 @@ +- name: mcpi + timeout: "0" + params: + {{ lookup('template', 'taskdefs/common/cli-params.yml.j2') | indent(4) }} + - name: devops_suite_name + value: mcpi + + - name: artifactory_username + value: $(params.artifactory_username) + - name: artifactory_token + value: $(params.artifactory_token) + + - name: ibm_entitlement_key + value: $(params.ibm_entitlement_key) + + - name: custom_labels + value: $(params.custom_labels) + + - name: mas_instance_id + value: $(params.mas_instance_id) + - name: mcpi_channel + value: "$(params.mcpi_channel)" + - name: routing_mode + value: $(params.routing_mode) + - name: manual_route_mgmt + value: $(params.manual_route_mgmt) + + taskRef: + name: mas-devops-mcpi + kind: Task + when: + - input: "$(params.mcpi_channel)" + operator: notin + values: [""] + workspaces: + - name: configs + workspace: shared-configs diff --git a/tekton/src/tasks/mcpi/mcpi.yml.j2 b/tekton/src/tasks/mcpi/mcpi.yml.j2 new file mode 100644 index 00000000000..0339894c328 --- /dev/null +++ b/tekton/src/tasks/mcpi/mcpi.yml.j2 @@ -0,0 +1,105 @@ +--- +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: mas-devops-mcpi +spec: + params: + {{ lookup('template', task_src_dir ~ '/common/cli-params.yml.j2') | indent(4) }} + + # Pre-Release Support + - name: artifactory_username + default: '' + type: string + description: Required to use development MAS builds + - name: artifactory_token + default: '' + type: string + description: Required to use development MAS builds + + # Entitlement + - name: ibm_entitlement_key + type: string + + # MAS instance + - name: mas_instance_id + type: string + description: MAS Instance ID + + # MCPI Operator + - name: mcpi_channel + type: string + description: Catalog channel for the MCPI operator subscription + + # MCPI Routing + - name: routing_mode + type: string + default: "" + description: "Routing mode for MCPI: subdomain or path" + - name: manual_route_mgmt + type: string + default: "" + description: Disable automatic route management for MCPI + + # Custom Label Support + - name: custom_labels + type: string + description: Optional MAS custom labels, comma separated list of key=value pairs + default: "" + + # ICR + - name: mas_icr_cp + type: string + default: "" + - name: mas_icr_cpopen + type: string + default: "" + + stepTemplate: + env: + {{ lookup('template', task_src_dir ~ '/common/cli-env.yml.j2') | indent(6) }} + + # Pre-Release Support + - name: ARTIFACTORY_USERNAME + value: $(params.artifactory_username) + - name: ARTIFACTORY_TOKEN + value: $(params.artifactory_token) + + # Entitlement + - name: IBM_ENTITLEMENT_KEY + value: $(params.ibm_entitlement_key) + + # MAS instance + - name: MAS_INSTANCE_ID + value: $(params.mas_instance_id) + + # MCPI + - name: MCPI_CHANNEL + value: $(params.mcpi_channel) + - name: ROUTING_MODE + value: $(params.routing_mode) + - name: MANUAL_ROUTE_MGMT + value: $(params.manual_route_mgmt) + + # Labels + - name: CUSTOM_LABELS + value: $(params.custom_labels) + + # ICR + - name: MAS_ICR_CP + value: $(params.mas_icr_cp) + - name: MAS_ICR_CPOPEN + value: $(params.mas_icr_cpopen) + + steps: + - name: mcpicfg + image: quay.io/ibmmas/cli:latest + imagePullPolicy: $(params.image_pull_policy) + command: + - /opt/app-root/src/run-role.sh + - mcpicfg + workingDir: /workspace/configs + + workspaces: + - name: configs + optional: true From 892a577ffd2820df4c8fd66b281bcfd5276bca2a Mon Sep 17 00:00:00 2001 From: Thierno-Bah1 Date: Thu, 27 Aug 2026 13:25:47 -0400 Subject: [PATCH 02/13] style(mcpi-cli-install): replace quoted descriptions with unquoted format --- tekton/src/params/install-mcpi.yml.j2 | 2 +- tekton/src/tasks/mcpi/mcpi.yml.j2 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tekton/src/params/install-mcpi.yml.j2 b/tekton/src/params/install-mcpi.yml.j2 index a1f02ccb7f3..228ad7e4b88 100644 --- a/tekton/src/params/install-mcpi.yml.j2 +++ b/tekton/src/params/install-mcpi.yml.j2 @@ -6,7 +6,7 @@ default: "" - name: routing_mode type: string - description: "Routing mode for MCPI: subdomain or path" + description: Routing mode for MCPI (subdomain or path) default: "" - name: manual_route_mgmt type: string diff --git a/tekton/src/tasks/mcpi/mcpi.yml.j2 b/tekton/src/tasks/mcpi/mcpi.yml.j2 index 0339894c328..1ec7f8f559b 100644 --- a/tekton/src/tasks/mcpi/mcpi.yml.j2 +++ b/tekton/src/tasks/mcpi/mcpi.yml.j2 @@ -35,7 +35,7 @@ spec: - name: routing_mode type: string default: "" - description: "Routing mode for MCPI: subdomain or path" + description: Routing mode for MCPI (subdomain or path) - name: manual_route_mgmt type: string default: "" From e59d113ebf915416a3a8f6469eda9e63c05e1f2e Mon Sep 17 00:00:00 2001 From: Thierno-Bah1 Date: Thu, 3 Sep 2026 13:09:44 -0400 Subject: [PATCH 03/13] style(mcpi-cli-install): quote mcpi CLI image reference --- tekton/src/tasks/mcpi/mcpi.yml.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tekton/src/tasks/mcpi/mcpi.yml.j2 b/tekton/src/tasks/mcpi/mcpi.yml.j2 index 1ec7f8f559b..184a12fdd77 100644 --- a/tekton/src/tasks/mcpi/mcpi.yml.j2 +++ b/tekton/src/tasks/mcpi/mcpi.yml.j2 @@ -93,7 +93,7 @@ spec: steps: - name: mcpicfg - image: quay.io/ibmmas/cli:latest + image: "quay.io/ibmmas/cli:latest" imagePullPolicy: $(params.image_pull_policy) command: - /opt/app-root/src/run-role.sh From f2d89ce82f5555b9899237ed92dacedbc4848dcb Mon Sep 17 00:00:00 2001 From: Thierno-Bah1 Date: Thu, 3 Sep 2026 13:52:47 -0400 Subject: [PATCH 04/13] feat(mcpi-cli-install): add mcpi-install command routing in CLI --- image/cli/mascli/mas | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/image/cli/mascli/mas b/image/cli/mascli/mas index 9bbe495e9c8..0e8449e8596 100755 --- a/image/cli/mascli/mas +++ b/image/cli/mascli/mas @@ -287,6 +287,16 @@ case $1 in mas-cli aiservice-install "$@" ;; + mcpi-install) + echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >> $LOGFILE + echo "!! mcpi-install !!" >> $LOGFILE + echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >> $LOGFILE + # Take the first parameter off (it will be "mcpi-install") + shift + # Run the new Python-based mcpi-install + mas-cli mcpi-install "$@" + ;; + aiservice-upgrade) echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >> $LOGFILE echo "!! aiservice-upgrade !!" >> $LOGFILE From 7a682a4ca91a8d592fe703a2efbe4c501fb105df Mon Sep 17 00:00:00 2001 From: Thierno-Bah1 Date: Tue, 8 Sep 2026 10:51:27 -0400 Subject: [PATCH 05/13] docs(mcpi-cli-install): add mcpi-install entry to CLI help text --- image/cli/app-root/src/.bashrc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/image/cli/app-root/src/.bashrc b/image/cli/app-root/src/.bashrc index 3bacaf4273f..c9474cb5a4b 100644 --- a/image/cli/app-root/src/.bashrc +++ b/image/cli/app-root/src/.bashrc @@ -48,6 +48,8 @@ if [ $arch != "s390x" ] && [ $arch != "ppc64le" ]; then echo " - ${TEXT_BOLD}${COLOR_GREEN}mas provision-fyre${TEXT_RESET} to provision an OCP cluster on IBM DevIT Fyre (internal)" echo " - ${TEXT_BOLD}${COLOR_GREEN}mas setup-rbac${TEXT_RESET} to setup RBAC resources for MAS installation in a cluster" echo " - ${TEXT_BOLD}${COLOR_GREEN}mas pre-install${TEXT_RESET} to set up pre-install RBAC for MAS installation in a cluster" + echo "MCPI Management:" + echo " - ${TEXT_BOLD}${COLOR_GREEN}mas mcpi-install${TEXT_RESET} to install MCPI into an existing MAS instance" echo "AI Service (Standalone) Management:" echo " - ${TEXT_BOLD}${COLOR_GREEN}mas aiservice-install${TEXT_RESET} to install a new AI Service instance" echo " - ${TEXT_BOLD}${COLOR_GREEN}mas aiservice-upgrade${TEXT_RESET} to upgrade a existing AI Service instance" From c8101a8841ca10a9684eb93f797d15efe42fe0f4 Mon Sep 17 00:00:00 2001 From: Thierno-Bah1 Date: Thu, 24 Sep 2026 14:33:23 -0400 Subject: [PATCH 06/13] refactor(mcpi-cli-install): remove redundant params from mcpi-install pipeline --- tekton/src/pipelines/mcpi-install.yml.j2 | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tekton/src/pipelines/mcpi-install.yml.j2 b/tekton/src/pipelines/mcpi-install.yml.j2 index 2134cc1a142..dcd542c36eb 100644 --- a/tekton/src/pipelines/mcpi-install.yml.j2 +++ b/tekton/src/pipelines/mcpi-install.yml.j2 @@ -25,14 +25,6 @@ spec: # ------------------------------------------------------------------------- {{ lookup('template', params_src_dir ~ '/install-common.yml.j2') | indent(4) }} - - name: mas_instance_id - type: string - description: MAS Instance ID - - - name: ibm_entitlement_key - type: string - description: IBM entitlement key - # MCPI Add-on Parameters # ------------------------------------------------------------------------- {{ lookup('template', params_src_dir ~ '/install-mcpi.yml.j2') | indent(4) }} From 49da514d6b8b658bf2f0af3b7500d260ac078f03 Mon Sep 17 00:00:00 2001 From: Thierno-Bah1 Date: Thu, 1 Oct 2026 12:29:09 -0400 Subject: [PATCH 07/13] feat(mcpi-cli-install): add SLS, DRO, cert manager and ICR params to mcpi-install pipeline --- python/src/mas/cli/mcpi/install/params.py | 8 +++ tekton/src/pipelines/mcpi-install.yml.j2 | 68 +++++++++++++++++++ .../src/pipelines/taskdefs/mcpi/mcpi.yml.j2 | 6 ++ 3 files changed, 82 insertions(+) diff --git a/python/src/mas/cli/mcpi/install/params.py b/python/src/mas/cli/mcpi/install/params.py index ee1f2804f01..136d39ec8de 100644 --- a/python/src/mas/cli/mcpi/install/params.py +++ b/python/src/mas/cli/mcpi/install/params.py @@ -29,11 +29,19 @@ # Pipeline "image_pull_policy", "service_account_name", + "skip_pre_check", # Catalogue "mas_catalog_digest", + # Certificate Manager + "cert_manager_provider", + "cert_manager_action", # SLS "sls_namespace", + "sls_channel", + "sls_entitlement_file", + "sls_action", # DRO + "dro_action", "dro_namespace", # MCPI routing "routing_mode", diff --git a/tekton/src/pipelines/mcpi-install.yml.j2 b/tekton/src/pipelines/mcpi-install.yml.j2 index dcd542c36eb..65f7a2afb13 100644 --- a/tekton/src/pipelines/mcpi-install.yml.j2 +++ b/tekton/src/pipelines/mcpi-install.yml.j2 @@ -25,6 +25,74 @@ spec: # ------------------------------------------------------------------------- {{ lookup('template', params_src_dir ~ '/install-common.yml.j2') | indent(4) }} + # IBM Operator Catalog Parameters + # ------------------------------------------------------------------------- + {{ lookup('template', params_src_dir ~ '/install-ibmcatalogs.yml.j2') | indent(4) }} + + # Certificate Manager Parameters + # ------------------------------------------------------------------------- + {{ lookup('template', params_src_dir ~ '/install-cert-manager.yml.j2') | indent(4) }} + + # SLS Parameters + # ------------------------------------------------------------------------- + - name: sls_namespace + type: string + default: "" + - name: sls_entitlement_file + type: string + default: "" + - name: sls_mongodb_cfg_file + type: string + default: "/workspace/configs/mongo-mongoce.yml" + - name: sls_catalog_source + type: string + default: "" + - name: sls_channel + type: string + default: "" + - name: sls_icr_cpopen + type: string + default: "" + - name: sls_action + type: string + default: "" + + # DRO Parameters + # ------------------------------------------------------------------------- + - name: dro_action + type: string + default: "" + - name: dro_contact_email + type: string + default: "" + - name: dro_contact_firstname + type: string + default: "" + - name: dro_contact_lastname + type: string + default: "" + - name: dro_namespace + type: string + default: "" + - name: aiservice_instance_id + type: string + default: "" + - name: ocp_ingress_tls_secret_name + type: string + default: "" + + # MAS ICR / Service Mesh Parameters + # ------------------------------------------------------------------------- + - name: mas_icr_cp + type: string + default: "" + - name: mas_icr_cpopen + type: string + default: "" + - name: mas_use_service_mesh + type: string + default: "" + # MCPI Add-on Parameters # ------------------------------------------------------------------------- {{ lookup('template', params_src_dir ~ '/install-mcpi.yml.j2') | indent(4) }} diff --git a/tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2 b/tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2 index 1be10a16c10..1600be97eea 100644 --- a/tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2 +++ b/tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2 @@ -24,6 +24,12 @@ value: $(params.routing_mode) - name: manual_route_mgmt value: $(params.manual_route_mgmt) + - name: mas_icr_cp + value: $(params.mas_icr_cp) + - name: mas_icr_cpopen + value: $(params.mas_icr_cpopen) + - name: mas_use_service_mesh + value: $(params.mas_use_service_mesh) taskRef: name: mas-devops-mcpi From 9e99a2269c8a347740bea64d26bb5699a61576b5 Mon Sep 17 00:00:00 2001 From: Thierno-Bah1 Date: Wed, 7 Oct 2026 13:05:55 -0400 Subject: [PATCH 08/13] fix(mcpi-cli-install): skip license file requirement when reusing SLS --- .gitignore | 10 ++++++---- python/src/mas/cli/mcpi/install/app.py | 8 ++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index e1d88be8737..c468b9095ea 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,8 @@ image/cli/mascli/fyre-dev.noble3.tgke-env.sh image/cli/mascli/fyre-dev.noble3.tgke-db2u-manage-env.sh image/cli/podman-build* +build/lib/mas/cli + python/.venv python/.venv-windows python/build @@ -64,7 +66,7 @@ rbac-install.yaml .venv /.bob/notes -pytest.log -/dist -/.bob/.bob-errors -/.mkdocs_cache +pytest.log +/dist +/.bob/.bob-errors +/.mkdocs_cache diff --git a/python/src/mas/cli/mcpi/install/app.py b/python/src/mas/cli/mcpi/install/app.py index 9518647d7ff..16aa2f2adde 100644 --- a/python/src/mas/cli/mcpi/install/app.py +++ b/python/src/mas/cli/mcpi/install/app.py @@ -119,8 +119,12 @@ def nonInteractiveMode(self) -> None: else: self.fatalError(f"Unknown option: {key} {value}") - if self.slsLicenseFileLocal is None: - self.fatalError("--license-file must be set for new SLS install") + if self.slsLicenseFileLocal is None and self.getParam("sls_action") not in ["install", "none"]: + # Only require license file if we're not reusing an existing SLS instance + # and not explicitly skipping SLS via --sls-action none + slsNamespace = self.getParam("sls_namespace") + if not slsNamespace or slsNamespace == "ibm-sls": + self.fatalError("--license-file must be set for new SLS install") @logMethodCall def install(self, argv) -> int: From 6604ef6214f0750de3d37b89ceb748f2c745473e Mon Sep 17 00:00:00 2001 From: Thierno-Bah1 Date: Wed, 7 Oct 2026 15:24:45 -0400 Subject: [PATCH 09/13] fix(mcpi-cli-install): update jsonpath_ng import to use ext.parser --- python/src/mas/cli/must_gather/common/crd_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 546c7392f96..d8e2061e95e 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__) From 2fe684913ce7f4aff5f7080a83b024e959246f9f Mon Sep 17 00:00:00 2001 From: Thierno-Bah1 Date: Fri, 9 Oct 2026 01:51:04 -0400 Subject: [PATCH 10/13] refactor(mcpi-cli-install): remove routing mode and service mesh params, add sls-action arg --- python/src/mas/cli/mcpi/install/app.py | 2 ++ python/src/mas/cli/mcpi/install/argBuilder.py | 6 +--- python/src/mas/cli/mcpi/install/argParser.py | 5 +-- python/src/mas/cli/mcpi/install/params.py | 3 -- python/src/mas/cli/mcpi/install/summarizer.py | 4 --- tekton/src/params/install-mcpi.yml.j2 | 8 ----- tekton/src/pipelines/mcpi-install.yml.j2 | 5 +-- .../src/pipelines/taskdefs/mcpi/mcpi.yml.j2 | 6 ---- tekton/src/tasks/mcpi/mcpi.yml.j2 | 32 ++++++++----------- 9 files changed, 19 insertions(+), 52 deletions(-) diff --git a/python/src/mas/cli/mcpi/install/app.py b/python/src/mas/cli/mcpi/install/app.py index 16aa2f2adde..cc59d42554b 100644 --- a/python/src/mas/cli/mcpi/install/app.py +++ b/python/src/mas/cli/mcpi/install/app.py @@ -113,6 +113,8 @@ def nonInteractiveMode(self) -> None: "help", "advanced", "simplified", + "routing_mode", + "manual_route_mgmt", ]: pass diff --git a/python/src/mas/cli/mcpi/install/argBuilder.py b/python/src/mas/cli/mcpi/install/argBuilder.py index c15aad5eefb..4b861b942bd 100644 --- a/python/src/mas/cli/mcpi/install/argBuilder.py +++ b/python/src/mas/cli/mcpi/install/argBuilder.py @@ -37,12 +37,8 @@ def buildCommand(self) -> str: # MAS Instance ID command += f" --mas-instance-id \"{self.getParam('mas_instance_id')}\"{newline}" - # MCPI channel & routing + # MCPI channel command += f" --mcpi-channel \"{self.getParam('mcpi_channel')}\"{newline}" - if self.getParam("routing_mode") != "": - command += f" --routing-mode \"{self.getParam('routing_mode')}\"{newline}" - if self.getParam("manual_route_mgmt") != "": - command += f" --manual-route-mgmt \"{self.getParam('manual_route_mgmt')}\"{newline}" # MAS Advanced Configuration if self.localConfigDir is not None: diff --git a/python/src/mas/cli/mcpi/install/argParser.py b/python/src/mas/cli/mcpi/install/argParser.py index 1071d943e89..88db9e42fe6 100644 --- a/python/src/mas/cli/mcpi/install/argParser.py +++ b/python/src/mas/cli/mcpi/install/argParser.py @@ -64,10 +64,6 @@ def isValidFile(parser, arg) -> str: # ----------------------------------------------------------------------------- mcpiAdvancedArgGroup = mcpiInstallArgParser.add_argument_group("MCPI Advanced Configuration") mcpiAdvancedArgGroup.add_argument("--mcpi-channel", required=False, dest="mcpi_channel", help="Subscription channel for Maximo Cluster Performance Insights") -mcpiAdvancedArgGroup.add_argument( - "--routing-mode", required=False, dest="routing_mode", choices=["subdomain", "path"], help="Routing mode for MCPI (subdomain or path)" -) -mcpiAdvancedArgGroup.add_argument("--manual-route-mgmt", required=False, dest="manual_route_mgmt", help="Disable automatic route management for MCPI") # MAS Advanced Configuration # ----------------------------------------------------------------------------- @@ -92,6 +88,7 @@ def isValidFile(parser, arg) -> str: slsArgGroup = mcpiInstallArgParser.add_argument_group("IBM Suite License Service") slsArgGroup.add_argument("--license-file", required=False, help="Path to MAS license file", type=lambda x: isValidFile(mcpiInstallArgParser, x)) slsArgGroup.add_argument("--sls-namespace", required=False, help="Customize the SLS install namespace", default="ibm-sls") +slsArgGroup.add_argument("--sls-action", required=False, dest="sls_action", help="Override the SLS action (e.g. 'none' to skip SLS install)") slsArgGroup.add_argument("--dedicated-sls", action="store_true", default=False, help="Set the SLS namespace to mas--sls") # IBM Data Reporting Operator (DRO) diff --git a/python/src/mas/cli/mcpi/install/params.py b/python/src/mas/cli/mcpi/install/params.py index 136d39ec8de..8f0b60393aa 100644 --- a/python/src/mas/cli/mcpi/install/params.py +++ b/python/src/mas/cli/mcpi/install/params.py @@ -43,9 +43,6 @@ # DRO "dro_action", "dro_namespace", - # MCPI routing - "routing_mode", - "manual_route_mgmt", # Dev Mode "artifactory_username", "artifactory_token", diff --git a/python/src/mas/cli/mcpi/install/summarizer.py b/python/src/mas/cli/mcpi/install/summarizer.py index c7a58560ef3..009d1f79528 100644 --- a/python/src/mas/cli/mcpi/install/summarizer.py +++ b/python/src/mas/cli/mcpi/install/summarizer.py @@ -42,10 +42,6 @@ def mcpiSummary(self) -> None: self.printH2("Maximo Cluster Performance Insights (MCPI)") self.printParamSummary("MAS Instance ID", "mas_instance_id") self.printParamSummary("Subscription Channel", "mcpi_channel") - if self.getParam("routing_mode") != "": - self.printParamSummary("Routing Mode", "routing_mode") - if self.getParam("manual_route_mgmt") != "": - self.printParamSummary("Manual Route Management", "manual_route_mgmt") def droSummary(self) -> None: """Print DRO configuration summary.""" diff --git a/tekton/src/params/install-mcpi.yml.j2 b/tekton/src/params/install-mcpi.yml.j2 index 228ad7e4b88..09be14712f2 100644 --- a/tekton/src/params/install-mcpi.yml.j2 +++ b/tekton/src/params/install-mcpi.yml.j2 @@ -4,11 +4,3 @@ type: string description: Default channel for IBM Maximo Cluster Performance Insights operator subscription default: "" -- name: routing_mode - type: string - description: Routing mode for MCPI (subdomain or path) - default: "" -- name: manual_route_mgmt - type: string - description: Disable automatic route management for MCPI - default: "" diff --git a/tekton/src/pipelines/mcpi-install.yml.j2 b/tekton/src/pipelines/mcpi-install.yml.j2 index 65f7a2afb13..627366872f3 100644 --- a/tekton/src/pipelines/mcpi-install.yml.j2 +++ b/tekton/src/pipelines/mcpi-install.yml.j2 @@ -81,7 +81,7 @@ spec: type: string default: "" - # MAS ICR / Service Mesh Parameters + # MAS ICR Parameters # ------------------------------------------------------------------------- - name: mas_icr_cp type: string @@ -89,9 +89,6 @@ spec: - name: mas_icr_cpopen type: string default: "" - - name: mas_use_service_mesh - type: string - default: "" # MCPI Add-on Parameters # ------------------------------------------------------------------------- diff --git a/tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2 b/tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2 index 1600be97eea..850844ea8ea 100644 --- a/tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2 +++ b/tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2 @@ -20,16 +20,10 @@ value: $(params.mas_instance_id) - name: mcpi_channel value: "$(params.mcpi_channel)" - - name: routing_mode - value: $(params.routing_mode) - - name: manual_route_mgmt - value: $(params.manual_route_mgmt) - name: mas_icr_cp value: $(params.mas_icr_cp) - name: mas_icr_cpopen value: $(params.mas_icr_cpopen) - - name: mas_use_service_mesh - value: $(params.mas_use_service_mesh) taskRef: name: mas-devops-mcpi diff --git a/tekton/src/tasks/mcpi/mcpi.yml.j2 b/tekton/src/tasks/mcpi/mcpi.yml.j2 index 184a12fdd77..de5611c976d 100644 --- a/tekton/src/tasks/mcpi/mcpi.yml.j2 +++ b/tekton/src/tasks/mcpi/mcpi.yml.j2 @@ -26,20 +26,10 @@ spec: type: string description: MAS Instance ID - # MCPI Operator + # MCPI - name: mcpi_channel type: string - description: Catalog channel for the MCPI operator subscription - - # MCPI Routing - - name: routing_mode - type: string - default: "" - description: Routing mode for MCPI (subdomain or path) - - name: manual_route_mgmt - type: string - default: "" - description: Disable automatic route management for MCPI + description: Catalog channel for the MCPI operator (used by gencfg step) # Custom Label Support - name: custom_labels @@ -72,14 +62,12 @@ spec: # MAS instance - name: MAS_INSTANCE_ID value: $(params.mas_instance_id) + - name: MAS_CONFIG_DIR + value: /workspace/configs # MCPI - name: MCPI_CHANNEL value: $(params.mcpi_channel) - - name: ROUTING_MODE - value: $(params.routing_mode) - - name: MANUAL_ROUTE_MGMT - value: $(params.manual_route_mgmt) # Labels - name: CUSTOM_LABELS @@ -92,12 +80,20 @@ spec: value: $(params.mas_icr_cpopen) steps: - - name: mcpicfg + - name: gencfg-mcpicfg + image: "quay.io/ibmmas/cli:latest" + imagePullPolicy: $(params.image_pull_policy) + command: + - /opt/app-root/src/run-role.sh + - gencfg_mcpicfg + workingDir: /workspace/configs + + - name: suite-config image: "quay.io/ibmmas/cli:latest" imagePullPolicy: $(params.image_pull_policy) command: - /opt/app-root/src/run-role.sh - - mcpicfg + - suite_config workingDir: /workspace/configs workspaces: From 366c661026178500e4a2dc193ec608e1f6468e38 Mon Sep 17 00:00:00 2001 From: Thierno-Bah1 Date: Sun, 11 Oct 2026 12:07:37 -0400 Subject: [PATCH 11/13] refactor(mcpi-cli-install): replace standalone mcpi-install command with --mcpi-channel flag on mas-cli install --- .bob/plans/2025-01-31-mcpi-install.md | 339 ++++++++++++++++++ .bob/plans/2025-06-11-mcpi-install-cleanup.md | 298 +++++++++++++++ image/cli/app-root/src/.bashrc | 2 - image/cli/mascli/mas | 10 - pytest.ini | 2 - python/src/mas/cli/__main__.py | 6 - python/src/mas/cli/install/app.py | 9 + python/src/mas/cli/install/argBuilder.py | 2 + python/src/mas/cli/install/argParser.py | 5 + python/src/mas/cli/install/summarizer.py | 8 + python/src/mas/cli/mcpi/__init__.py | 9 - python/src/mas/cli/mcpi/install/__init__.py | 11 - python/src/mas/cli/mcpi/install/app.py | 244 ------------- python/src/mas/cli/mcpi/install/argBuilder.py | 86 ----- python/src/mas/cli/mcpi/install/argParser.py | 135 ------- python/src/mas/cli/mcpi/install/params.py | 52 --- python/src/mas/cli/mcpi/install/summarizer.py | 83 ----- .../integration/mcpi_install/__init__.py | 9 - .../integration/mcpi_install/conftest.py | 26 -- .../mcpi_install/test_mcpi_install_app.py | 117 ------ tekton/generate-tekton-pipelines.yml | 2 - tekton/generate-tekton-tasks.yml | 11 +- tekton/src/params/install-mcpi.yml.j2 | 6 - tekton/src/params/install.yml.j2 | 4 - tekton/src/pipelines/mas-install.yml.j2 | 11 +- tekton/src/pipelines/mcpi-install.yml.j2 | 183 ---------- .../src/pipelines/taskdefs/mcpi/mcpi.yml.j2 | 37 -- tekton/src/tasks/mcpi/mcpi.yml.j2 | 101 ------ 28 files changed, 663 insertions(+), 1145 deletions(-) create mode 100644 .bob/plans/2025-06-11-mcpi-install-cleanup.md delete mode 100644 python/src/mas/cli/mcpi/__init__.py delete mode 100644 python/src/mas/cli/mcpi/install/__init__.py delete mode 100644 python/src/mas/cli/mcpi/install/app.py delete mode 100644 python/src/mas/cli/mcpi/install/argBuilder.py delete mode 100644 python/src/mas/cli/mcpi/install/argParser.py delete mode 100644 python/src/mas/cli/mcpi/install/params.py delete mode 100644 python/src/mas/cli/mcpi/install/summarizer.py delete mode 100644 python/tests/integration/mcpi_install/__init__.py delete mode 100644 python/tests/integration/mcpi_install/conftest.py delete mode 100644 python/tests/integration/mcpi_install/test_mcpi_install_app.py delete mode 100644 tekton/src/params/install-mcpi.yml.j2 delete mode 100644 tekton/src/pipelines/mcpi-install.yml.j2 delete mode 100644 tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2 delete mode 100644 tekton/src/tasks/mcpi/mcpi.yml.j2 diff --git a/.bob/plans/2025-01-31-mcpi-install.md b/.bob/plans/2025-01-31-mcpi-install.md index 98dad8c9e5b..098dbea17a3 100644 --- a/.bob/plans/2025-01-31-mcpi-install.md +++ b/.bob/plans/2025-01-31-mcpi-install.md @@ -570,3 +570,342 @@ After both phases are complete: - Tekton YAML contains `mas-devops-mcpi` task and `mcpi-install` pipeline - MCPI task appears in `mas-install` pipeline guarded by `when: mcpi_channel != ""` - No regressions in existing test suite + +--- + +## Testing on a New Cluster — Checklist + +### Step 1 — Login + +```bash +oc login --web --server=https://:6443 +oc whoami --show-server # confirm correct cluster +``` + +### Step 2 — Discover cluster values + +```bash +# MAS instance ID +oc get suite -A + +# Storage classes +oc get storageclass + +# Existing SLS namespace +oc get namespace | grep sls + +# MongoDB namespace +oc get mongodbcommunity -A + +# Entitlement key +oc get secret ibm-entitlement -n mas--core \ + -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | \ + jq -r '.auths["cp.icr.io"].password' + +# Dev CLI image (from existing FVT pipeline pods) +oc get pod -n mas--pipelines -l tekton.dev/pipelineTask=suite-install \ + -o jsonpath='{.items[0].spec.containers[0].image}' +``` + +### Step 3 — Set up CLI from source + +```bash +cd /Users/tahamadou/Source/MAS/public-ibm-mas/cli + +# Install as editable — required so ibm-mas-tekton.yaml is read from source tree +uv tool uninstall mas-cli 2>/dev/null +uv tool install --editable . --with-editable ../python-devops +``` + +> **Do NOT use `uvx --from .`** — it builds a wheel that excludes `ibm-mas-tekton.yaml` (a generated file not in git). + +### Step 4 — Generate the Tekton template (macOS only) + +`build/bin/build-tekton.sh` uses `sed -i` which fails on macOS. Use this Python script instead (run from repo root after the Ansible playbooks have generated `tekton/target/`): + +```bash +python3 - <<'EOF' +import os, glob + +# Replace with the dev image digest from Step 2 +IMAGE = "quay.io/ibmmas/cli@sha256:" +OUT = "python/src/mas/cli/templates/ibm-mas-tekton.yaml" +skip = ("fvt-", "ivt-", "aiservice-fvt", "aiservice-launchfvt") +tasks = sorted(glob.glob("tekton/target/tasks/*.yaml")) +pipelines = sorted(glob.glob("tekton/target/pipelines/*.yaml")) + +with open(OUT, "w") as out: + for f in tasks + pipelines: + name = os.path.basename(f) + if any(name.startswith(s) or s in name for s in skip): + continue + content = open(f).read().replace("quay.io/ibmmas/cli:latest", IMAGE) + out.write(content + "\n") +print(f"Done - {sum(1 for _ in open(OUT))} lines") +EOF +``` + +### Step 5 — Copy `mas-devops` secret (dev clusters only) + +Dev FVT clusters have a `mas-devops` secret in `mas--pipelines` that the `pipeline-start` task needs to generate the MongoDB config file. Copy it to the MCPI namespace before the first run: + +```bash +oc get secret mas-devops -n mas--pipelines -o yaml | \ + sed "s/namespace: mas--pipelines/namespace: mcpi--pipelines/" | \ + oc apply -f - +``` + +> This only needs to be done once. The namespace is created by the CLI during the first run, so run the install command first, wait for the namespace to appear, copy the secret, then re-run. + +### Step 6 — Run the install + +```bash +mas-cli mcpi-install \ + --mas-catalog-version v9-250828-amd64 \ + --ibm-entitlement-key "" \ + --mas-instance-id \ + --storage-class-rwo \ + --storage-class-rwx \ + --storage-pipeline \ + --storage-accessmode ReadWriteMany \ + --sls-namespace sls- \ + --sls-action none \ + --mongodb-namespace \ + --contact-email \ + --contact-firstname \ + --contact-lastname \ + --mcpi-channel v9.2 \ + --image-pull-policy IfNotPresent \ + --accept-license \ + --no-confirm +``` + +### Step 7 — Monitor + +```bash +# Watch pipeline progress +oc get pipelinerun -n mcpi--pipelines -w + +# Check task statuses +oc get taskrun -n mcpi--pipelines \ + -o custom-columns='TASK:.metadata.labels.tekton\.dev/pipelineTask,STATUS:.status.conditions[0].reason' \ + --sort-by='.metadata.creationTimestamp' + +# Tail a specific task's logs +oc logs -n mcpi--pipelines -l tekton.dev/pipelineTask= --tail=50 +``` + +--- + +## Testing Session Notes (2025-01-31 / Cluster: pfvt-thierno-bah1-261006-18) + +### Cluster Values Used + +| Resource | Value | +|---|---| +| API URL | `https://api.pfvt-thierno-bah1-261006-18.cp.fyre.ibm.com:6443` | +| Console URL | `https://console-openshift-console.apps.pfvt-thierno-bah1-261006-18.cp.fyre.ibm.com` | +| MAS Instance ID | `thierno` | +| MAS Core Namespace | `mas-thierno-core` | +| MAS Pipelines Namespace | `mas-thierno-pipelines` | +| MCPI Pipelines Namespace | `mcpi-thierno-pipelines` | +| SLS Namespace | `sls-thierno` | +| MongoDB Namespace | `mongoce` | +| Storage Class (RWO + RWX) | `nfs-client` | +| Entitlement Secret | `ibm-entitlement` in `mas-thierno-core` (Artifactory JWT, not standard ICR key) | +| Dev CLI Image Digest | `quay.io/ibmmas/cli@sha256:d2591b6d413e08efbd7185d7ca574ffd82cb7409b92bcde3da11f87f19c6e29b` | + +### Pipeline Task Results (latest run) + +| Task | Status | Notes | +|---|---|---| +| `pipeline-start` | ✅ Succeeded | | +| `ibm-catalogs` | ✅ Succeeded | | +| `cert-manager` | ✅ Succeeded | | +| `dro` | ✅ Succeeded | | +| `sls` | ⏭ Skipped | `--sls-action none` used to bypass existing SLS | +| `mcpi` | ❌ Failed | `mcpicfg` role not found in published CLI image | +| `pipeline-finish` | ✅ Succeeded | | +| `sync-install` | ✅ Succeeded | | + +--- + +## Open Issues + +#### 1. `mcpicfg` role missing from published CLI image +**Severity:** Blocker for end-to-end validation +**Root cause:** The Tekton `mcpi` task uses `quay.io/ibmmas/cli:latest` which is the published image. The `mcpicfg` Ansible role only exists on the `mcpi-cli-install` branch of `ansible-devops` and hasn't been built into an image yet. +**Workaround:** Use the dev image digest from the FVT pipeline (see Step 2 above). +**Fix:** Build and publish a CLI image from the `mcpi-cli-install` branch and use that image reference in the Tekton task. + +#### 2. `--license-file` unconditionally required (Bug — Fixed) +**Severity:** Real bug affecting all users +**Root cause:** `nonInteractiveMode()` in `app.py` always errors if `--license-file` is not set, even when `--sls-namespace` points to an existing SLS. +**Fix applied:** [`app.py`](python/src/mas/cli/mcpi/install/app.py:122) — skip the check when a non-default `sls_namespace` is set. +**Note:** Same bug exists in `aiservice/install/app.py:437` — should be fixed there too. + +#### 3. `--sls-action` flag — dev workaround only +**Severity:** Low +**Fix applied:** Added to `argParser.py` to allow `--sls-action none` to skip SLS on dev clusters. +**Recommendation:** Remove before merging to production — not needed for real installs. + +#### 4. `pipeline-start` needs `mas-devops` secret for MongoDB config generation +**Severity:** Dev cluster only +**Root cause:** `pipeline-start` reads `DEVOPS_MONGO_URI` from `mas-devops` secret to write `mongo-.yml` to the workspace. This secret only exists in `mas--pipelines`, not the new `mcpi--pipelines` namespace. +**Workaround:** Copy secret (see Step 5 above). +**Recommendation:** `prepareMcpiPipelinesNamespace()` should auto-copy `mas-devops` from `mas-{instanceId}-pipelines` if it exists. + +#### 5. macOS `build/bin/build-tekton.sh` fails +**Severity:** Dev workflow only +**Root cause:** macOS `sed -i` requires a backup suffix extension. +**Workaround:** Use the Python script in Step 4 above. +**Recommendation:** Add macOS note to `CONTRIBUTING.md` or add `gsed` fallback to the script. + +#### 6. `build/lib/` not in `.gitignore` +**Status:** Fixed. + + +--- + +## Testing Session Notes (2025-02-XX / Cluster: visionary) + +### Cluster Values Used + +| Resource | Value | +|---|---| +| MAS Instance ID | `visionary` | +| MAS Core Namespace | `mas-visionary-core` | +| MCPI Pipelines Namespace | `mcpi-visionary-pipelines` | +| SLS Namespace | `sls-visionary` | +| MongoDB Namespace | `mongoce` | +| Storage Class (RWO + RWX) | `nfs-client` (single class, NFS supports RWX) | +| MAS Version | `9.2.3-pre.maint92cpi` | +| Entity Manager | `visionary-entitymgr-mcpicfg` (running in `mas-visionary-core`) | +| Dev CLI Image Digest | `quay.io/ibmmas/cli@sha256:865097dd25032aaad9fe462e6433faf76d34b758c34832fb5701795bc38d85aa` | + +### Pipeline Task Results (this run) + +| Task | Status | Notes | +|---|---|---| +| `pipeline-start` | ❌ Failed | `ibm-mas-tekton.yaml` had literal `` placeholder — Step 4 had not been run | +| All tasks | ❌ Failed | `TaskRunImagePullFailed` on every task due to invalid image reference | + +--- + +## Architecture Discovery (2025-02-XX) + +This session uncovered a fundamental design error in the original implementation. The following documents what was discovered and what was changed. + +### Discovery 1 — `McpiCfg` is owned by the existing `ibm-mas` operator + +The `ibm-mas` CSV already owns and watches `McpiCfg` CRs. There is **no separate MCPI operator subscription** to install. The entity manager deployment `visionary-entitymgr-mcpicfg` is already running in `mas-visionary-core` and reconciles any `McpiCfg` CR automatically. + +**Implication:** The pipeline's only job is to create the `McpiCfg` CR. Everything else (TLS certs, routes, deployment, OAuth registration) is handled by the operator. + +### Discovery 2 — The `mcpicfg` role in `ansible-devops` was a copy of the operator's internal role + +A `diff` showed the role was byte-for-byte identical to `ibm-mas/operator/ibm-mas-cfg-mcpi/roles/mcpicfg/`. Running it as a Tekton task was wrong for three reasons: + +1. It requires `ansible_operator_meta` vars (CR name/namespace) only available inside the operator container +2. It requires the `operator_sdk` collection not installed in the CLI image +3. It looks itself up via `k8s_info` on the `McpiCfg` CR — which doesn't exist yet at pipeline time + +### Discovery 3 — `mcpi_channel` was wired to nothing + +The param flowed from CLI → pipeline → task → env var `MCPI_CHANNEL`, but nothing in the `mcpicfg` role ever read it. Confirmed by `grep` returning zero results across the entire role. + +### Discovery 4 — `routing_mode` / `manual_route_mgmt` / `mas_use_service_mesh` were dead params + +These were copied from the `mas-install` pattern but the `mcpicfg` role never consumed them. They were threaded all the way through pipeline → taskdef → task → Python CLI with no effect. + +### Discovery 5 — `ibm-mas-tekton.yaml` had a literal placeholder + +The template still contained `quay.io/ibmmas/cli@sha256:` because Step 4 of the testing checklist had never been run. This caused all pipeline task pods to fail with `TaskRunImagePullFailed`. + +### Discovery 6 — `gencfg_mcpicfg` role was missing entirely + +The correct Ansible role — generating a `McpiCfg` YAML file into the pipeline workspace — did not exist. The `suite_config` pattern (generate CR → apply CR → operator reconciles) was never implemented. + +--- + +## Refactor Applied (2025-02-XX) + +### What was removed + +| What | Repo | Reason | +|---|---|---| +| Entire `mcpicfg` role (16 files) | `ansible-devops` | Operator's internal logic — belongs only inside `ibm-mas-cfg-mcpi` image | +| `run-role.sh mcpicfg` Tekton step | `cli` | Replaced with correct two-step pattern | +| `routing_mode`, `manual_route_mgmt` params | `cli` (Tekton + Python) | No consumer in any role | +| `mas_use_service_mesh` param | `cli` (Tekton pipeline) | No consumer in MCPI context | +| `--routing-mode`, `--manual-route-mgmt` CLI flags | `cli` (argParser/argBuilder/summarizer) | Removed from CLI surface | + +### What was added + +| What | Repo | Purpose | +|---|---|---| +| `gencfg_mcpicfg` role (4 files) | `ansible-devops` | Validates `mas_instance_id` + `mas_config_dir`, renders `McpiCfg` CR YAML to workspace | +| `gencfg-mcpicfg` Tekton step | `cli` | Runs `gencfg_mcpicfg` role to write CR to workspace | +| `suite-config` Tekton step | `cli` | Applies workspace YAML to cluster via `suite_config` role | +| `routing_mode`/`manual_route_mgmt` in ignored keys | `cli` (`app.py`) | Backward compat — old scripts with these flags won't crash | + +### Correct flow post-refactor + +``` +CLI → mcpi-install Pipeline + ↓ + mcpi Task + ① gencfg_mcpicfg role → writes -mcpi-system.yml to /workspace/configs + ② suite_config role → kubectl apply → McpiCfg CR created in mas--core + ↓ + visionary-entitymgr-mcpicfg (already running — watches McpiCfg) + ↓ reconciles automatically + Deploys: TLS certs, Routes, Deployment, Services, NetworkPolicy, OAuth redirect URI +``` + +### Commits + +| Repo | Commit | Change | +|---|---|---| +| `ansible-devops` | `refactor(mcpicfg): remove mcpicfg operator role` | Deleted 16-file `mcpicfg` role | +| `ansible-devops` | `feat(mcpicfg): add gencfg_mcpicfg role` | Added 4-file `gencfg_mcpicfg` role | +| `cli` | `refactor(mcpi-cli-install): remove routing mode and service mesh params, add sls-action arg` | All Python + Tekton cleanup | + +--- + +## Open Issues (Updated) + +#### 1. ~~`mcpicfg` role missing from published CLI image~~ — **Root cause was wrong** +**Status:** Closed / redesigned +**What actually happened:** The `mcpicfg` role was a copy of the operator's internal reconcile logic and was never the right approach. Replaced by `gencfg_mcpicfg` which generates the CR YAML, and `suite_config` which applies it. + +#### 2. `--license-file` unconditionally required (Bug — Fixed) +**Severity:** Real bug affecting all users +**Root cause:** `nonInteractiveMode()` in `app.py` always errors if `--license-file` is not set, even when `--sls-namespace` points to an existing SLS. +**Fix applied:** [`app.py`](python/src/mas/cli/mcpi/install/app.py:122) — skip the check when a non-default `sls_namespace` is set. +**Note:** Same bug exists in `aiservice/install/app.py:437` — should be fixed there too. + +#### 3. `--sls-action` flag — dev workaround only +**Severity:** Low +**Fix applied:** Added to `argParser.py` to allow `--sls-action none` to skip SLS on dev clusters. +**Recommendation:** Remove before merging to production — not needed for real installs. + +#### 4. `pipeline-start` needs `mas-devops` secret for MongoDB config generation +**Severity:** Dev cluster only +**Root cause:** `pipeline-start` reads `DEVOPS_MONGO_URI` from `mas-devops` secret to write `mongo-.yml` to the workspace. This secret only exists in `mas--pipelines`, not the new `mcpi--pipelines` namespace. +**Workaround:** Copy secret (see Step 5 above). +**Recommendation:** `prepareMcpiPipelinesNamespace()` should auto-copy `mas-devops` from `mas-{instanceId}-pipelines` if it exists. + +#### 5. macOS `build/bin/build-tekton.sh` fails +**Severity:** Dev workflow only +**Root cause:** macOS `sed -i` requires a backup suffix extension. +**Workaround:** Use the Python script in Step 4 above. +**Recommendation:** Add macOS note to `CONTRIBUTING.md` or add `gsed` fallback to the script. + +#### 6. `build/lib/` not in `.gitignore` +**Status:** Fixed. + +#### 7. `mcpi_channel` param is accepted but unused +**Severity:** Low — cosmetic +**Root cause:** `mcpi_channel` flows through the pipeline to the `mcpi` task env var `MCPI_CHANNEL`, but `gencfg_mcpicfg` does not read it (the channel is an operator subscription concern, not a CR field). +**Recommendation:** Either remove `mcpi_channel` from the pipeline entirely (the operator handles its own subscription via the catalog) or keep as a no-op for future use if a channel field is added to the `McpiCfg` spec. diff --git a/.bob/plans/2025-06-11-mcpi-install-cleanup.md b/.bob/plans/2025-06-11-mcpi-install-cleanup.md new file mode 100644 index 00000000000..e1c6a27adab --- /dev/null +++ b/.bob/plans/2025-06-11-mcpi-install-cleanup.md @@ -0,0 +1,298 @@ +# MCPI Install Cleanup Plan + +## Objective + +Remove the over-engineered standalone `mcpi-install` command and pipeline across three repos, and replace with the minimal correct approach: a `--mcpi-channel` flag on the existing `mas-cli install` command that includes the `McpiCfg` CR via `additional-configs`, applied by the already-present `suite-config` Tekton task. + +--- + +## Design Decisions + +### Why the standalone approach was wrong + +The over-built implementation treated MCPI like a standalone product (AI Service pattern) with its own: +- CLI command (`mas-cli mcpi-install`) +- Tekton pipeline (`mcpi-install.yml.j2`) +- Custom Tekton task (`mas-devops-mcpi`) running `gencfg_mcpicfg` + `suite_config` +- Ansible role (`gencfg_mcpicfg`) generating a `McpiCfg` CR +- python-devops namespace provisioning (`prepareMcpiPipelinesNamespace`, separate RBAC/PVC templates) + +### The McpiCfg CR is trivially simple + +`ibm/mas_devops/roles/gencfg_mcpicfg/templates/mcpicfg.yml.j2`: + +```yaml +apiVersion: config.mas.ibm.com/v1 +kind: McpiCfg +metadata: + name: "{{ mas_instance_id }}-mcpi-system" + namespace: "mas-{{ mas_instance_id }}-core" + labels: + mas.ibm.com/configScope: system + mas.ibm.com/instanceId: "{{ mas_instance_id }}" +spec: + displayName: "Maximo Cluster Performance Insights" + config: + enabled: true +``` + +There is no subscription channel, no database, no S3. The `mcpi_channel` param was never used in the CR template or anywhere in the `gencfg_mcpicfg` role — it was vestigial. + +### How suite_config handles additional-configs + +`run-role.sh` (line 3-5) copies `additional-configs/*` → `configs/` **before every role invocation**. The existing `suite-config` Tekton task calls `run-role.sh suite_config`. Therefore: + +- If a `McpiCfg` YAML is placed in the `additional-configs` workspace, `suite_config` will automatically apply it +- The `McpiCfg` CR uses `{{ mas_instance_id }}` in name/namespace which `suite_config` resolves via `kubernetes.core.k8s template:` + +### Decision: No new Tekton task needed, no `mcpi_channel` Tekton param needed + +The existing `suite-config` step in `mas-install.yml.j2` is sufficient to apply the McpiCfg, as long as the CR YAML is present in `additional-configs`. No separate Tekton step, no new params file, no `mcpi_channel` pipeline parameter. + +The `--mcpi-channel` CLI flag in `mas-cli install` serves as the user-facing gate. When set, the CLI: +1. Sets `self.installMcpi = True` +2. Sets `mcpi_channel` in params (for display in summary/argBuilder — the channel value itself isn't used functionally) +3. The McpiCfg YAML is bundled into the generated `additional-configs` secret when the `python-devops` `launchInstallPipeline` builds the PipelineRun + +**Important**: Because `mcpi_channel` is not used in the CR, passing it as a Tekton pipeline param serves no purpose. The `--mcpi-channel` flag is needed only to: +- Signal that MCPI should be installed (gate) +- Display in the install summary +- Be reproducible via `buildCommand()` + +--- + +## Critical Rules + +- Track progress only by updating checkboxes in this plan file — do NOT use chat todo lists +- Delete files entirely rather than commenting them out +- Each phase must be independently validatable before proceeding to the next +- Do NOT add error handling or abstractions beyond the minimal patterns already used by other apps (e.g., `facilities_channel`) +- Preserve all unrelated content in files being partially edited +- When removing lines from `__main__.py`, `mascli/mas`, `.bashrc`, `pytest.ini` — verify surrounding context before editing + +--- + +## Execution Plan + +### Phase 1 — Delete over-built files + +**Objective**: Remove all files that belong exclusively to the standalone mcpi-install implementation. + +[ ] **1.1** Delete `python/src/mas/cli/mcpi/` (entire directory — 7 files) + +[ ] **1.2** Delete `python/tests/integration/mcpi_install/` (entire directory — 3 files) + +[ ] **1.3** Delete `tekton/src/pipelines/mcpi-install.yml.j2` + +[ ] **1.4** Delete `tekton/src/tasks/mcpi/mcpi.yml.j2` and the `mcpi/` directory + +[ ] **1.5** Delete `tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2` and the `mcpi/` directory + +[ ] **1.6** Delete `tekton/src/params/install-mcpi.yml.j2` + +[ ] **1.7** Delete `ibm/mas_devops/roles/gencfg_mcpicfg/` (entire directory — 4 files across 4 subdirectories) in `ansible-devops` repo + +[ ] **1.8** Delete `src/mas/devops/templates/mcpi-pipelines-pvc.yml.j2` in `python-devops` repo + +[ ] **1.9** Delete `src/mas/devops/templates/mcpi-pipelines-rbac.yml.j2` in `python-devops` repo + +[ ] **1.10** Delete `src/mas/devops/templates/pipelinerun-mcpi-install.yml.j2` in `python-devops` repo + +[ ] **1.11** Delete `test/src/test_tekton_mcpi.py` in `python-devops` repo + +**Validation**: Confirm none of the deleted paths exist. No build or test run needed yet. + +--- + +### Phase 2 — Undo file modifications in `cli` repo + +**Objective**: Revert all partial changes to shared files that were modified to support the standalone approach. + +**Relevant files and what to undo:** + +#### `tekton/src/params/install.yml.j2` +Remove the MCPI Configuration section (lines ~862-864): +``` +# MCPI Configuration +{{ lookup('template', params_src_dir ~ '/install-mcpi.yml.j2') }} +``` + +#### `tekton/src/pipelines/mas-install.yml.j2` +- Remove section 15 "Install and configure MCPI" block (~lines 464-468): + ``` + # 15. Install and configure MCPI (Maximo Cluster Performance Insights) + # ------------------------------------------------------------------------- + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/mcpi/mcpi.yml.j2') | indent(4) }} + runAfter: + - suite-config + ``` +- Remove `- mcpi` from `post-install-verify` `runAfter` list (~line 484) +- Renumber section "16. Verify health..." back to "15." if sections were renumbered + +#### `tekton/generate-tekton-tasks.yml` +Remove the MCPI block (~lines 45-52): +```yaml + # 3. Generate Tasks (MCPI) + # ------------------------------------------------------------------------- + - name: Generate Tasks (MCPI) + ansible.builtin.template: + src: "{{ task_src_dir }}/mcpi/{{ item }}.yml.j2" + dest: "{{ task_target_dir }}/{{ item }}.yaml" + with_items: + - mcpi +``` +Also renumber subsequent sections (AI Service was originally section 3, not 4). + +#### `tekton/generate-tekton-pipelines.yml` +Remove the MCPI pipelines entry (~lines 46-47): +```yaml + # MCPI Pipelines + - mcpi-install +``` + +#### `python/src/mas/cli/__main__.py` +Remove: +- The usage text line: `" - mas-cli mcpi-install Install Maximo Cluster Performance Insights\n"` +- The dispatcher block (~lines 78-82): + ```python + if function == "mcpi-install": + from mas.cli.mcpi.install.app import McpiInstallApp + app = McpiInstallApp() + raise SystemExit(app.install(argv[2:])) + ``` + +#### `pytest.ini` +Remove the two MCPI markers (~lines 12-13): +``` + mcpi-install: MCPI installation tests (mas-cli mcpi-install) + mcpi_install: MCPI installation tests (mas-cli mcpi-install) +``` + +#### `image/cli/mascli/mas` +Remove the `mcpi-install` case block (~lines 290-298): +```bash + mcpi-install) + echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >> $LOGFILE + echo "!! mcpi-install !!" >> $LOGFILE + echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >> $LOGFILE + # Take the first parameter off (it will be "mcpi-install") + shift + # Run the new Python-based mcpi-install + mas-cli mcpi-install "$@" + ;; +``` + +#### `image/cli/app-root/src/.bashrc` +Remove the MCPI Management section (~lines 51-52): +``` +echo "MCPI Management:" +echo " - ${TEXT_BOLD}${COLOR_GREEN}mas mcpi-install${TEXT_RESET} to install MCPI into an existing MAS instance" +``` + +#### `python-devops` repo: `src/mas/devops/tekton.py` +Remove: +- `prepareMcpiPipelinesNamespace()` function (~lines 846-920) +- `launchMcpiInstallPipeline()` function (~lines 1610-1628) + +**Validation**: After all edits, run a grep to confirm no remaining `mcpi-install` or `mcpi_channel` references in shared files. Tekton generation playbooks should parse cleanly (no missing template references). + +--- + +### Phase 3 — Add `--mcpi-channel` to `mas-cli install` + +**Objective**: Implement MCPI as a standard MAS app option in the existing install command, following the same pattern as `facilities_channel`. + +#### `python/src/mas/cli/install/argParser.py` +In the "MAS Applications" argument group (after `--aiservice-channel`, before the `# Arcgis` section), add: +```python +masAppsArgGroup.add_argument( + "--mcpi-channel", + required=False, + help="Subscription channel for Maximo Cluster Performance Insights", +) +``` + +#### `python/src/mas/cli/install/app.py` — `nonInteractiveMode()` init block +At lines ~2204-2216, `nonInteractiveMode()` initializes all install booleans to `False`. Add `self.installMcpi = False` to this block alongside the other flags. + +#### `python/src/mas/cli/install/app.py` — `nonInteractiveMode()` key dispatch +After the `elif key == "aiservice_channel":` block (around line 2414), add: +```python +elif key == "mcpi_channel": + if value is not None and value != "": + self.setParam("mcpi_channel", value) + self.installMcpi = True +``` + +#### `python/src/mas/cli/install/app.py` — `configApps()` +After the AI Service block (end of method, around line 1356), add a simple unconditional prompt: +```python +self.installMcpi = self.yesOrNo("Install Maximo Cluster Performance Insights (MCPI)") +if self.installMcpi: + self.configAppChannel("mcpi") # OR: self.promptForString(...) +``` + +> **Note**: `McpiCfg` has no subscription channel field in the CR, but we still prompt for a channel to match the install pattern and for future use. If `configAppChannel` requires an entry in the compatibility matrix, use `promptForString("MCPI subscription channel", "mcpi_channel")` instead. + +#### `python/src/mas/cli/install/params.py` +Add `"mcpi_channel"` to `optionalParams` list (near the app channels section). + +#### `python/src/mas/cli/install/argBuilder.py` — `buildCommand()` +After the `if self.installAIService:` block (~line 285), add: +```python +if self.installMcpi: + command += f" --mcpi-channel \"{self.getParam('mcpi_channel')}\"{newline}" +``` + +#### `python/src/mas/cli/install/summarizer.py` +Add an `mcpiSummary()` method following the pattern of `facilitiesSummary()` or `assistSummary()`: +```python +def mcpiSummary(self) -> None: + if self.installMcpi: + self.printSummary("MCPI", self.getParam("mcpi_channel")) + else: + self.printSummary("MCPI", "Do Not Install") +``` +Call `self.mcpiSummary()` in the appropriate place in the summary output method. + +**Validation**: Run `python -m pytest python/tests -m unit -k install` to confirm existing install tests pass. Verify `--help` output for `mas-cli install` shows `--mcpi-channel`. + +--- + +### Phase 4 — Final validation + +**Objective**: Confirm the full state of all three repos is clean and correct. + +[ ] **4.1** In `cli` repo: run `grep -r "mcpi-install\|McpiInstall\|mcpi_channel\|gencfg_mcpicfg\|launchMcpiInstall\|prepareMcpiPipelines" python/ tekton/ image/ pytest.ini` — expect zero results (except `mcpi_channel` in the new optionalParams and argParser additions) + +[ ] **4.2** In `cli` repo: run `ansible-playbook tekton/generate-tekton-tasks.yml --check` — expect success (no missing template errors) + +[ ] **4.3** In `cli` repo: run `ansible-playbook tekton/generate-tekton-pipelines.yml --check` — expect success + +[ ] **4.4** In `cli` repo: run `.venv/bin/pytest python/tests -m unit` — all unit tests pass + +[ ] **4.5** In `ansible-devops` repo: confirm `ibm/mas_devops/roles/gencfg_mcpicfg/` directory is gone + +[ ] **4.6** In `python-devops` repo: run `.venv/bin/pytest test/` — all tests pass (mcpi test file removed, no broken imports) + +--- + +## Final Validation + +### Success criteria + +1. `grep -r "mcpi-install" cli/python/ cli/tekton/ cli/image/` returns zero matches +2. `ansible-playbook tekton/generate-tekton-tasks.yml` completes without errors +3. `ansible-playbook tekton/generate-tekton-pipelines.yml` completes without errors +4. `mas-cli install --help` shows `--mcpi-channel` in the MAS Applications group +5. `mas-cli mcpi-install` returns "command not found" or usage error (no longer a valid subcommand) +6. All unit tests in `cli` repo pass +7. All tests in `python-devops` pass +8. `gencfg_mcpicfg` role no longer exists in `ansible-devops` + +### Troubleshooting + +- If `generate-tekton-tasks.yml` fails: check that `tekton/src/tasks/mcpi/` directory was fully removed and no `with_items: - mcpi` block remains +- If `generate-tekton-pipelines.yml` fails: check that `tekton/src/pipelines/mcpi-install.yml.j2` was removed and `all_pipelines` list no longer contains `mcpi-install` +- If Python import fails: ensure `python/src/mas/cli/mcpi/` directory was fully removed and `__main__.py` dispatcher was cleaned up +- If `installMcpi` attribute error: ensure `self.installMcpi = False` default is set in `InstallApp.__init__()` alongside other `self.installXxx = False` defaults diff --git a/image/cli/app-root/src/.bashrc b/image/cli/app-root/src/.bashrc index c9474cb5a4b..3bacaf4273f 100644 --- a/image/cli/app-root/src/.bashrc +++ b/image/cli/app-root/src/.bashrc @@ -48,8 +48,6 @@ if [ $arch != "s390x" ] && [ $arch != "ppc64le" ]; then echo " - ${TEXT_BOLD}${COLOR_GREEN}mas provision-fyre${TEXT_RESET} to provision an OCP cluster on IBM DevIT Fyre (internal)" echo " - ${TEXT_BOLD}${COLOR_GREEN}mas setup-rbac${TEXT_RESET} to setup RBAC resources for MAS installation in a cluster" echo " - ${TEXT_BOLD}${COLOR_GREEN}mas pre-install${TEXT_RESET} to set up pre-install RBAC for MAS installation in a cluster" - echo "MCPI Management:" - echo " - ${TEXT_BOLD}${COLOR_GREEN}mas mcpi-install${TEXT_RESET} to install MCPI into an existing MAS instance" echo "AI Service (Standalone) Management:" echo " - ${TEXT_BOLD}${COLOR_GREEN}mas aiservice-install${TEXT_RESET} to install a new AI Service instance" echo " - ${TEXT_BOLD}${COLOR_GREEN}mas aiservice-upgrade${TEXT_RESET} to upgrade a existing AI Service instance" diff --git a/image/cli/mascli/mas b/image/cli/mascli/mas index 0e8449e8596..9bbe495e9c8 100755 --- a/image/cli/mascli/mas +++ b/image/cli/mascli/mas @@ -287,16 +287,6 @@ case $1 in mas-cli aiservice-install "$@" ;; - mcpi-install) - echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >> $LOGFILE - echo "!! mcpi-install !!" >> $LOGFILE - echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >> $LOGFILE - # Take the first parameter off (it will be "mcpi-install") - shift - # Run the new Python-based mcpi-install - mas-cli mcpi-install "$@" - ;; - aiservice-upgrade) echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >> $LOGFILE echo "!! aiservice-upgrade !!" >> $LOGFILE diff --git a/pytest.ini b/pytest.ini index 93dcd8d2138..1a713740de5 100644 --- a/pytest.ini +++ b/pytest.ini @@ -9,8 +9,6 @@ markers = install: Installation tests (mas-cli install) aiservice-install: AI Service installation tests (mas-cli aiservice-install) aiservice_install: AI Service installation tests (mas-cli aiservice-install) - mcpi-install: MCPI installation tests (mas-cli mcpi-install) - mcpi_install: MCPI installation tests (mas-cli mcpi-install) mirror: Mirror/catalog tests (mas-cli mirror-*) update: Update tests (mas-cli update) upgrade: Upgrade tests (mas-cli upgrade) diff --git a/python/src/mas/cli/__main__.py b/python/src/mas/cli/__main__.py index e7b068a3f95..5e495c2aef0 100644 --- a/python/src/mas/cli/__main__.py +++ b/python/src/mas/cli/__main__.py @@ -50,7 +50,6 @@ 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 mcpi-install Install Maximo Cluster Performance Insights\n" ) ) print_formatted_text(HTML("For usage information run mas-cli [action] --help\n")) @@ -75,11 +74,6 @@ def main() -> None: app = AiServiceInstallApp() raise SystemExit(app.install(argv[2:])) - if function == "mcpi-install": - from mas.cli.mcpi.install.app import McpiInstallApp - - app = McpiInstallApp() - raise SystemExit(app.install(argv[2:])) if function == "aiservice-upgrade": from mas.cli.aiservice.upgrade.app import AiServiceUpgradeApp diff --git a/python/src/mas/cli/install/app.py b/python/src/mas/cli/install/app.py index 77e43fb4a54..96877527f9c 100644 --- a/python/src/mas/cli/install/app.py +++ b/python/src/mas/cli/install/app.py @@ -1355,6 +1355,10 @@ def configApps(self): else: self.installAIService = False + self.installMcpi = self.yesOrNo("Install Maximo Cluster Performance Insights (MCPI)") + if self.installMcpi: + self.promptForString("MCPI subscription channel", "mcpi_channel", validator=NotEmptyValidator()) + @logMethodCall def configAppChannel(self, appId): versions = self.getCompatibleVersions(self.params["mas_channel"], appId) @@ -2214,6 +2218,7 @@ def nonInteractiveMode(self) -> None: self.db2SetAffinity = False self.db2SetTolerations = False self.installAIService = False + self.installMcpi = False self.slsLicenseFileLocal = None self.db2LicenseFileLocal = None self.aiserviceTenantSchedulingConfigFileLocal = None @@ -2385,6 +2390,10 @@ def nonInteractiveMode(self) -> None: if value is not None and value != "": self.setParam("mas_app_channel_facilities", value) self.installFacilities = True + elif key == "mcpi_channel": + if value is not None and value != "": + self.setParam("mcpi_channel", value) + self.installMcpi = True elif key == "aiservice_channel": if value is not None and value != "": self.setParam("aiservice_channel", value) diff --git a/python/src/mas/cli/install/argBuilder.py b/python/src/mas/cli/install/argBuilder.py index a4f71c2d42a..107995befe2 100644 --- a/python/src/mas/cli/install/argBuilder.py +++ b/python/src/mas/cli/install/argBuilder.py @@ -286,6 +286,8 @@ def buildCommand(self) -> str: command += f" --aiservice-channel \"{self.getParam('aiservice_channel')}\"{newline}" if self.getParam("configure_aiassistant") != "": command += f" --configure-aiassistant \"{self.getParam('configure_aiassistant')}\"{newline}" + if self.installMcpi: + command += f" --mcpi-channel \"{self.getParam('mcpi_channel')}\"{newline}" # Arcgis # ----------------------------------------------------------------------------- diff --git a/python/src/mas/cli/install/argParser.py b/python/src/mas/cli/install/argParser.py index 86d80d584c0..c0ad5317af5 100644 --- a/python/src/mas/cli/install/argParser.py +++ b/python/src/mas/cli/install/argParser.py @@ -604,6 +604,11 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Subscription channel for Maximo AI Service", ) +masAppsArgGroup.add_argument( + "--mcpi-channel", + required=False, + help="Subscription channel for Maximo Cluster Performance Insights", +) # Arcgis # ----------------------------------------------------------------------------- diff --git a/python/src/mas/cli/install/summarizer.py b/python/src/mas/cli/install/summarizer.py index 801a42e929b..874ebeed1f0 100644 --- a/python/src/mas/cli/install/summarizer.py +++ b/python/src/mas/cli/install/summarizer.py @@ -49,6 +49,7 @@ class InstallSummarizerMixin: installOptimizer: bool installFacilities: bool installAIService: bool + installMcpi: bool installArcgis: bool dynamicClient: DynamicClient applyPreInstallMASRBAC: bool @@ -461,6 +462,12 @@ def aiServiceSummary(self) -> None: self.printParamSummary("URL", "aiservice_watsonxai_url") self.printParamSummary("Project ID", "aiservice_watsonxai_project_id") + def mcpiSummary(self) -> None: + if self.installMcpi: + self.printSummary("Maximo Cluster Performance Insights (MCPI)", self.getParam("mcpi_channel")) + else: + self.printSummary("Maximo Cluster Performance Insights (MCPI)", "Do Not Install") + def db2Summary(self) -> None: if ( self.getParam("db2_action_system") == "install" @@ -666,6 +673,7 @@ def displayInstallSummary(self) -> None: self.inspectionSummary() self.facilitiesSummary() self.aiServiceSummary() + self.mcpiSummary() # Application Dependencies self.mongoSummary() diff --git a/python/src/mas/cli/mcpi/__init__.py b/python/src/mas/cli/mcpi/__init__.py deleted file mode 100644 index 10b45d523a5..00000000000 --- a/python/src/mas/cli/mcpi/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# ***************************************************************************** -# Copyright (c) 2026 IBM Corporation and other Contributors. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v1.0 -# which accompanies this distribution, and is available at -# http://www.eclipse.org/legal/epl-v10.html -# -# ***************************************************************************** diff --git a/python/src/mas/cli/mcpi/install/__init__.py b/python/src/mas/cli/mcpi/install/__init__.py deleted file mode 100644 index 7166ef936fe..00000000000 --- a/python/src/mas/cli/mcpi/install/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# ***************************************************************************** -# Copyright (c) 2026 IBM Corporation and other Contributors. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v1.0 -# which accompanies this distribution, and is available at -# http://www.eclipse.org/legal/epl-v10.html -# -# ***************************************************************************** - -from ...cli import BaseApp # noqa: F401 diff --git a/python/src/mas/cli/mcpi/install/app.py b/python/src/mas/cli/mcpi/install/app.py deleted file mode 100644 index cc59d42554b..00000000000 --- a/python/src/mas/cli/mcpi/install/app.py +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env python -# ***************************************************************************** -# Copyright (c) 2026 IBM Corporation and other Contributors. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v1.0 -# which accompanies this distribution, and is available at -# http://www.eclipse.org/legal/epl-v10.html -# -# ***************************************************************************** - -import logging - -from sys import exit -from prompt_toolkit import print_formatted_text, HTML -from halo import Halo - -from ...cli import BaseApp -from ...gencfg import ConfigGeneratorMixin -from .argBuilder import McpiInstallArgBuilderMixin -from .argParser import mcpiInstallArgParser -from .summarizer import McpiInstallSummarizerMixin -from .params import requiredParams, optionalParams - -from ...install.settings import InstallSettingsMixin - -from mas.devops.ocp import createNamespace -from mas.devops.tekton import ( - installOpenShiftPipelines, - updateTektonDefinitions, - prepareMcpiPipelinesNamespace, - prepareInstallSecrets, - testCLI, - launchMcpiInstallPipeline, -) - -logger = logging.getLogger(__name__) - - -def logMethodCall(func): - def wrapper(self, *args, **kwargs): - logger.debug(f">>> McpiInstallApp.{func.__name__}") - result = func(self, *args, **kwargs) - logger.debug(f"<<< McpiInstallApp.{func.__name__}") - return result - - return wrapper - - -class McpiInstallApp(BaseApp, McpiInstallArgBuilderMixin, McpiInstallSummarizerMixin, InstallSettingsMixin, ConfigGeneratorMixin): - """CLI App for installing Maximo Cluster Performance Insights (MCPI).""" - - @logMethodCall - def configMcpi(self) -> None: - """Prompt for MCPI-specific settings in interactive mode.""" - self.printH1("MCPI Configuration") - self.promptForString("MAS instance ID", "mas_instance_id") - self.promptForString("MCPI subscription channel", "mcpi_channel") - - @logMethodCall - def nonInteractiveMode(self) -> None: - """Process all CLI arguments for non-interactive install. - - Raises: - SystemExit: If a required parameter is missing or invalid. - """ - self.isInteractiveMode = False - self.storageClassProvider = "custom" - self.slsLicenseFileLocal = None - - for key, value in vars(self.args).items(): - if key in requiredParams: - if value is None: - self.fatalError(f"{key} must be set") - self.setParam(key, value) - - elif key in optionalParams: - if value is not None: - self.setParam(key, value) - - elif key == "license_file": - if value is not None and value != "": - self.slsLicenseFileLocal = value - self.setParam("sls_action", "install") - - elif key == "dedicated_sls": - if value: - self.setParam("sls_namespace", f"mas-{self.args.mas_instance_id}-sls") - - elif key == "storage_accessmode": - if value is None: - self.fatalError(f"{key} must be set") - self.pipelineStorageAccessMode = value - - elif key == "storage_pipeline": - if value is None: - self.fatalError(f"{key} must be set") - self.pipelineStorageClass = value - - elif key == "additional_configs": - self.localConfigDir = value - - elif key == "mongodb_namespace": - if value is not None and value != "": - self.setParam(key, value) - self.setParam("sls_mongodb_cfg_file", f"/workspace/configs/mongo-{value}.yml") - - elif key in [ - "accept_license", - "dev_mode", - "skip_pre_check", - "no_confirm", - "help", - "advanced", - "simplified", - "routing_mode", - "manual_route_mgmt", - ]: - pass - - else: - self.fatalError(f"Unknown option: {key} {value}") - - if self.slsLicenseFileLocal is None and self.getParam("sls_action") not in ["install", "none"]: - # Only require license file if we're not reusing an existing SLS instance - # and not explicitly skipping SLS via --sls-action none - slsNamespace = self.getParam("sls_namespace") - if not slsNamespace or slsNamespace == "ibm-sls": - self.fatalError("--license-file must be set for new SLS install") - - @logMethodCall - def install(self, argv) -> int: - """Install Maximo Cluster Performance Insights. - - Args: - argv (list): Command-line arguments to parse. - - Returns: - int: Exit code (0 on success). - """ - args = mcpiInstallArgParser.parse_args(args=argv) - - instanceId = args.mas_instance_id - self.noConfirm = args.no_confirm - self.licenseAccepted = args.accept_license - self.devMode = args.dev_mode - - if args.image_pull_policy and args.image_pull_policy != "": - self.setParam("image_pull_policy", args.image_pull_policy) - - if args.skip_pre_check: - self.setParam("skip_pre_check", "true") - - self.args = args - - if instanceId is None: - self.printH1("Set Target OpenShift Cluster") - self.connect() - else: - logger.debug("MAS instance ID is set, assuming already connected to the desired OCP cluster") - self.lookupTargetArchitecture() - - if self.dynamicClient is None: - print_formatted_text(HTML("Error: Not successfully connected to a Kubernetes cluster. See log file for details")) - exit(1) - - self.isAirgap() - - self.setParam("dro_action", "install") - - if instanceId is None: - self.interactiveMode(simplified=args.simplified, advanced=args.advanced) - else: - self.nonInteractiveMode() - - self.slsLicenseFile() - - self.printH1("Non-Interactive Install Command") - self.printDescription( - ["Save and re-use the following script to re-run this install without needing to answer the interactive prompts again", "", self.buildCommand()] - ) - - self.displayInstallSummary() - - if not self.noConfirm: - print() - self.printDescription(["Please carefully review your choices above, correcting mistakes now is much easier than after the install has begun"]) - continueWithInstall = self.yesOrNo("Proceed with these settings") - - if self.noConfirm or continueWithInstall: - self.createTektonFileWithDigest() - - self.printH1("Launch Install") - pipelinesNamespace = f"mcpi-{self.getParam('mas_instance_id')}-pipelines" - - with Halo(text="Validating OpenShift Pipelines installation", spinner=self.spinner) as h: - if installOpenShiftPipelines(self.dynamicClient, self.getParam("storage_class_rwx")): - h.stop_and_persist(symbol=self.successIcon, text="OpenShift Pipelines Operator is installed and ready to use") - else: - h.stop_and_persist(symbol=self.successIcon, text="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) - prepareMcpiPipelinesNamespace( - dynClient=self.dynamicClient, - instanceId=self.getParam("mas_instance_id"), - storageClass=self.pipelineStorageClass, - accessMode=self.pipelineStorageAccessMode, - configureRBAC=(self.getParam("service_account_name") == ""), - ) - prepareInstallSecrets( - dynClient=self.dynamicClient, - namespace=pipelinesNamespace, - slsLicenseFile=self.slsLicenseFileSecret, - additionalConfigs=self.additionalConfigsSecret, - podTemplates=self.podTemplatesSecret, - certs=self.certsSecret, - slack_token=self.getParam("slack_token"), - slack_channel=self.getParam("slack_channel"), - ) - h.stop_and_persist(symbol=self.successIcon, text=f"Namespace is ready ({pipelinesNamespace})") - - with Halo(text="Testing availability of MAS CLI image in cluster", spinner=self.spinner) as h: - testCLI() - h.stop_and_persist(symbol=self.successIcon, text="MAS CLI image deployment test completed") - - with Halo(text=f"Installing latest Tekton definitions (v{self.version})", spinner=self.spinner) as h: - updateTektonDefinitions(self.dynamicClient, pipelinesNamespace, self.tektonDefsPath) - h.stop_and_persist(symbol=self.successIcon, text=f"Latest Tekton definitions are installed (v{self.version})") - - with Halo(text=f"Submitting PipelineRun for {self.getParam('mas_instance_id')} MCPI install", spinner=self.spinner) as h: - pipelineURL = launchMcpiInstallPipeline(dynClient=self.dynamicClient, params=self.params) - if pipelineURL is not None: - h.stop_and_persist(symbol=self.successIcon, text=f"PipelineRun for {self.getParam('mas_instance_id')} install submitted") - print_formatted_text(HTML(f"\nView progress:\n {pipelineURL}\n")) - else: - h.stop_and_persist( - symbol=self.failureIcon, - text=f"Failed to submit PipelineRun for {self.getParam('mas_instance_id')} install, see log file for details", - ) - print() - - return 0 diff --git a/python/src/mas/cli/mcpi/install/argBuilder.py b/python/src/mas/cli/mcpi/install/argBuilder.py deleted file mode 100644 index 4b861b942bd..00000000000 --- a/python/src/mas/cli/mcpi/install/argBuilder.py +++ /dev/null @@ -1,86 +0,0 @@ -# ***************************************************************************** -# Copyright (c) 2026 IBM Corporation and other Contributors. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v1.0 -# which accompanies this distribution, and is available at -# http://www.eclipse.org/legal/epl-v10.html -# -# ***************************************************************************** - -import logging - -logger = logging.getLogger(__name__) - - -class McpiInstallArgBuilderMixin: - """Mixin that builds the equivalent non-interactive CLI command string.""" - - def buildCommand(self) -> str: - """Build a re-runnable mas mcpi-install shell command from current params. - - Returns: - str: Shell command string that reproduces the current configuration. - """ - newline = " \\\n" - command = "export IBM_ENTITLEMENT_KEY=x\n" - if self.getParam("artifactory_username") != "": - command += "export ARTIFACTORY_USERNAME=x\nexport ARTIFACTORY_TOKEN=x\n" - - command += f"mas mcpi-install --mas-catalog-version {self.getParam('mas_catalog_version')}" - - if self.getParam("mas_catalog_digest") != "": - command += f" --mas-catalog-digest {self.getParam('mas_catalog_digest')}" - - command += f" --ibm-entitlement-key $IBM_ENTITLEMENT_KEY{newline}" - - # MAS Instance ID - command += f" --mas-instance-id \"{self.getParam('mas_instance_id')}\"{newline}" - - # MCPI channel - command += f" --mcpi-channel \"{self.getParam('mcpi_channel')}\"{newline}" - - # MAS Advanced Configuration - if self.localConfigDir is not None: - command += f' --additional-configs "{self.localConfigDir}"{newline}' - - # Storage - command += f" --storage-class-rwo \"{self.getParam('storage_class_rwo')}\"" - command += f" --storage-class-rwx \"{self.getParam('storage_class_rwx')}\"{newline}" - command += f' --storage-pipeline "{self.pipelineStorageClass}"' - command += f' --storage-accessmode "{self.pipelineStorageAccessMode}"{newline}' - - # IBM Suite License Service - if self.getParam("sls_namespace") and self.getParam("sls_namespace") != "ibm-sls": - if self.getParam("sls_namespace") == f"mas-{self.getParam('mas_instance_id')}-sls": - command += " --dedicated-sls" - else: - command += f" --sls-namespace \"{self.getParam('sls_namespace')}\"" - if self.slsLicenseFileLocal: - command += f' --license-file "{self.slsLicenseFileLocal}"' - if (self.getParam("sls_namespace") and self.getParam("sls_namespace") != "ibm-sls") or self.slsLicenseFileLocal: - command += newline - - # IBM Data Reporting Operator (DRO) - command += f" --contact-email \"{self.getParam('dro_contact_email')}\"" - command += f" --contact-firstname \"{self.getParam('dro_contact_firstname')}\"" - command += f" --contact-lastname \"{self.getParam('dro_contact_lastname')}\"{newline}" - if self.getParam("dro_namespace") != "": - command += f" --dro-namespace \"{self.getParam('dro_namespace')}\"{newline}" - - # Development Mode - if self.getParam("artifactory_username") != "": - command += f" --artifactory-username $ARTIFACTORY_USERNAME --artifactory-token $ARTIFACTORY_TOKEN{newline}" - - # More Options - if self.devMode: - command += f" --dev-mode{newline}" - if self.getParam("skip_pre_check") is True: - command += f" --skip-pre-check{newline}" - if self.getParam("image_pull_policy") != "": - command += f" --image-pull-policy {self.getParam('image_pull_policy')}{newline}" - if self.getParam("service_account_name") != "": - command += f" --service-account {self.getParam('service_account_name')}{newline}" - - command += " --accept-license --no-confirm" - return command diff --git a/python/src/mas/cli/mcpi/install/argParser.py b/python/src/mas/cli/mcpi/install/argParser.py deleted file mode 100644 index 88db9e42fe6..00000000000 --- a/python/src/mas/cli/mcpi/install/argParser.py +++ /dev/null @@ -1,135 +0,0 @@ -# ***************************************************************************** -# Copyright (c) 2026 IBM Corporation and other Contributors. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v1.0 -# which accompanies this distribution, and is available at -# http://www.eclipse.org/legal/epl-v10.html -# -# ***************************************************************************** - -import argparse -from os import path - -from ... import __version__ as packageVersion -from ...cli import getHelpFormatter - - -def isValidFile(parser, arg) -> str: - """Validate that a file path exists. - - Args: - parser (argparse.ArgumentParser): The argument parser instance - arg (str): File path to validate - - Returns: - str: The validated file path - """ - if not path.exists(arg): - parser.error(f"Error: The file {arg} does not exist") - else: - return arg - - -mcpiInstallArgParser = argparse.ArgumentParser( - prog="mas mcpi-install", - description="\n".join( - [ - f"IBM Maximo Application Suite Admin CLI v{packageVersion}", - "Install Maximo Cluster Performance Insights (MCPI) by configuring and launching the Tekton Pipeline.\n", - "Interactive Mode:", - "Omitting the --mas-instance-id option will trigger an interactive prompt", - ] - ), - epilog="Refer to the online documentation for more information: https://ibm-mas.github.io/cli/", - formatter_class=getHelpFormatter(), - add_help=False, -) - -# MAS Catalog Selection & Entitlement -# ----------------------------------------------------------------------------- -catArgGroup = mcpiInstallArgParser.add_argument_group("MAS Catalog Selection & Entitlement") -catArgGroup.add_argument("-c", "--mas-catalog-version", required=False, help="IBM Maximo Operator Catalog to install") -catArgGroup.add_argument( - "--mas-catalog-digest", required=False, help="IBM Maximo Operator Catalog Digest, only required when installing development catalog sources" -) -catArgGroup.add_argument("--ibm-entitlement-key", required=False, help="IBM entitlement key") - -# MCPI Basic Configuration -# ----------------------------------------------------------------------------- -mcpiArgGroup = mcpiInstallArgParser.add_argument_group("MCPI Basic Configuration") -mcpiArgGroup.add_argument("-i", "--mas-instance-id", required=False, dest="mas_instance_id", help="MAS Instance ID") - -# MCPI Advanced Configuration -# ----------------------------------------------------------------------------- -mcpiAdvancedArgGroup = mcpiInstallArgParser.add_argument_group("MCPI Advanced Configuration") -mcpiAdvancedArgGroup.add_argument("--mcpi-channel", required=False, dest="mcpi_channel", help="Subscription channel for Maximo Cluster Performance Insights") - -# MAS Advanced Configuration -# ----------------------------------------------------------------------------- -masAdvancedArgGroup = mcpiInstallArgParser.add_argument_group("MAS Advanced Configuration") -masAdvancedArgGroup.add_argument("--additional-configs", required=False, help="Path to a directory containing additional configuration files to be applied") - -# Storage -# ----------------------------------------------------------------------------- -storageArgGroup = mcpiInstallArgParser.add_argument_group("Storage") -storageArgGroup.add_argument("--storage-class-rwo", required=False, help="ReadWriteOnce (RWO) storage class (e.g. ibmc-block-gold)") -storageArgGroup.add_argument("--storage-class-rwx", required=False, help="ReadWriteMany (RWX) storage class (e.g. ibmc-file-gold-gid)") -storageArgGroup.add_argument("--storage-pipeline", required=False, help="Install pipeline storage class (e.g. ibmc-file-gold-gid)") -storageArgGroup.add_argument( - "--storage-accessmode", - required=False, - help="Install pipeline storage class access mode (ReadWriteMany or ReadWriteOnce)", - choices=["ReadWriteMany", "ReadWriteOnce"], -) - -# IBM Suite License Service -# ----------------------------------------------------------------------------- -slsArgGroup = mcpiInstallArgParser.add_argument_group("IBM Suite License Service") -slsArgGroup.add_argument("--license-file", required=False, help="Path to MAS license file", type=lambda x: isValidFile(mcpiInstallArgParser, x)) -slsArgGroup.add_argument("--sls-namespace", required=False, help="Customize the SLS install namespace", default="ibm-sls") -slsArgGroup.add_argument("--sls-action", required=False, dest="sls_action", help="Override the SLS action (e.g. 'none' to skip SLS install)") -slsArgGroup.add_argument("--dedicated-sls", action="store_true", default=False, help="Set the SLS namespace to mas--sls") - -# IBM Data Reporting Operator (DRO) -# ----------------------------------------------------------------------------- -droArgGroup = mcpiInstallArgParser.add_argument_group("IBM Data Reporting Operator (DRO)") -droArgGroup.add_argument("--contact-email", "--uds-email", dest="dro_contact_email", required=False, help="Contact e-mail address") -droArgGroup.add_argument("--contact-firstname", "--uds-firstname", dest="dro_contact_firstname", required=False, help="Contact first name") -droArgGroup.add_argument("--contact-lastname", "--uds-lastname", dest="dro_contact_lastname", required=False, help="Contact last name") -droArgGroup.add_argument("--dro-namespace", required=False, help="Namespace for the Data Reporting Operator") - -# MongoDb Community Operator -# ----------------------------------------------------------------------------- -mongoArgGroup = mcpiInstallArgParser.add_argument_group("MongoDb Community Operator") -mongoArgGroup.add_argument("--mongodb-namespace", required=False, help="Namespace for the MongoDB Community Operator") - -# Development Mode -# ----------------------------------------------------------------------------- -devArgGroup = mcpiInstallArgParser.add_argument_group("Development Mode") -devArgGroup.add_argument("--artifactory-username", required=False, help="Username for access to development builds on Artifactory") -devArgGroup.add_argument("--artifactory-token", required=False, help="API Token for access to development builds on Artifactory") - -# More Options -# ----------------------------------------------------------------------------- -otherArgGroup = mcpiInstallArgParser.add_argument_group("More") -otherArgGroup.add_argument("--advanced", action="store_true", default=False, help="Show advanced install options (in interactive mode)") -otherArgGroup.add_argument("--simplified", action="store_true", default=False, help="Don't show advanced install options (in interactive mode)") -otherArgGroup.add_argument("--accept-license", action="store_true", default=False, help="Accept all license terms without prompting") -otherArgGroup.add_argument("--dev-mode", required=False, action="store_true", default=False, help="Configure installation for development mode") -otherArgGroup.add_argument("--skip-pre-check", required=False, action="store_true", help="Disable the 'pre-install-check' at the start of the install pipeline") -otherArgGroup.add_argument("--no-confirm", required=False, action="store_true", default=False, help="Launch the install without prompting for confirmation") -otherArgGroup.add_argument( - "--image-pull-policy", dest="image_pull_policy", required=False, help="Manually set the image pull policy used in the Tekton Pipeline" -) -otherArgGroup.add_argument( - "--service-account", - dest="service_account_name", - required=False, - help="Run the install pipeline under a custom service account", -) -otherArgGroup.add_argument("--slack-token", dest="slack_token", required=False, help="Slack bot token for sending pipeline notifications") -otherArgGroup.add_argument( - "--slack-channel", dest="slack_channel", required=False, help="Slack channel(s) for notifications (comma-separated for multiple channels)" -) -otherArgGroup.add_argument("-h", "--help", action="help", default=False, help="Show this help message and exit") diff --git a/python/src/mas/cli/mcpi/install/params.py b/python/src/mas/cli/mcpi/install/params.py deleted file mode 100644 index 8f0b60393aa..00000000000 --- a/python/src/mas/cli/mcpi/install/params.py +++ /dev/null @@ -1,52 +0,0 @@ -# ***************************************************************************** -# Copyright (c) 2026 IBM Corporation and other Contributors. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v1.0 -# which accompanies this distribution, and is available at -# http://www.eclipse.org/legal/epl-v10.html -# -# ***************************************************************************** - -requiredParams = [ - # MAS - "mas_catalog_version", - "mas_instance_id", - # Storage classes - "storage_class_rwo", - "storage_class_rwx", - # Entitlement - "ibm_entitlement_key", - # MCPI channel - "mcpi_channel", - # DRO - "dro_contact_email", - "dro_contact_firstname", - "dro_contact_lastname", -] - -optionalParams = [ - # Pipeline - "image_pull_policy", - "service_account_name", - "skip_pre_check", - # Catalogue - "mas_catalog_digest", - # Certificate Manager - "cert_manager_provider", - "cert_manager_action", - # SLS - "sls_namespace", - "sls_channel", - "sls_entitlement_file", - "sls_action", - # DRO - "dro_action", - "dro_namespace", - # Dev Mode - "artifactory_username", - "artifactory_token", - # Notifications - "slack_token", - "slack_channel", -] diff --git a/python/src/mas/cli/mcpi/install/summarizer.py b/python/src/mas/cli/mcpi/install/summarizer.py deleted file mode 100644 index 009d1f79528..00000000000 --- a/python/src/mas/cli/mcpi/install/summarizer.py +++ /dev/null @@ -1,83 +0,0 @@ -# ***************************************************************************** -# Copyright (c) 2026 IBM Corporation and other Contributors. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v1.0 -# which accompanies this distribution, and is available at -# http://www.eclipse.org/legal/epl-v10.html -# -# ***************************************************************************** - -import logging -import yaml -from mas.devops.ocp import getConsoleURL - -logger = logging.getLogger(__name__) - - -class McpiInstallSummarizerMixin: - """Mixin that renders the pre-launch settings review.""" - - def ocpSummary(self) -> None: - """Print OpenShift platform and pipeline configuration summary.""" - self.printH2("Pipeline Configuration") - self.printParamSummary("Service Account", "service_account_name") - self.printParamSummary("Image Pull Policy", "image_pull_policy") - self.printSummary("Skip Pre-Install Healthcheck", "Yes" if self.getParam("skip_pre_check") == "true" else "No") - - self.printH2("OpenShift Container Platform") - self.printSummary("Worker Node Architecture", self.architecture) - self.printSummary("Storage Class Provider", self.storageClassProvider) - self.printParamSummary("ReadWriteOnce Storage Class", "storage_class_rwo") - self.printParamSummary("ReadWriteMany Storage Class", "storage_class_rwx") - self.printParamSummary("Certificate Manager", "cert_manager_provider") - - def mcpiSummary(self) -> None: - """Print MCPI operator configuration summary.""" - self.printH2("Maximo Operator Catalog") - self.printParamSummary("Catalog Version", "mas_catalog_version") - if self.getParam("mas_catalog_digest") != "": - self.printParamSummary("Catalog Digest", "mas_catalog_digest") - - self.printH2("Maximo Cluster Performance Insights (MCPI)") - self.printParamSummary("MAS Instance ID", "mas_instance_id") - self.printParamSummary("Subscription Channel", "mcpi_channel") - - def droSummary(self) -> None: - """Print DRO configuration summary.""" - self.printH2("IBM Data Reporter Operator (DRO) Configuration") - self.printParamSummary("Contact e-mail", "dro_contact_email") - self.printParamSummary("First name", "dro_contact_firstname") - self.printParamSummary("Last name", "dro_contact_lastname") - self.printParamSummary("Install Namespace", "dro_namespace") - - def slsSummary(self) -> None: - """Print SLS configuration summary.""" - self.printH2("IBM Suite License Service") - self.printParamSummary("Namespace", "sls_namespace") - if self.getParam("sls_action") == "install": - self.printSummary("Subscription Channel", "3.x") - if self.slsLicenseFileLocal: - self.printSummary("License File", self.slsLicenseFileLocal) - - def slackSummary(self) -> None: - """Print Slack integration summary.""" - self.printH2("Slack Integration") - if self.getParam("slack_channel") != "": - self.printParamSummary("Slack Channel", "slack_channel") - else: - self.printSummary("Slack Channel", "Not Configured") - - def displayInstallSummary(self) -> None: - """Render the full pre-launch settings review.""" - self.printH1("Review Settings") - self.printDescription(["Connected to:", f" - {getConsoleURL(self.dynamicClient)}"]) - - logger.debug("PipelineRun parameters:") - logger.debug(yaml.dump(self.params, default_flow_style=False)) - - self.ocpSummary() - self.mcpiSummary() - self.droSummary() - self.slsSummary() - self.slackSummary() diff --git a/python/tests/integration/mcpi_install/__init__.py b/python/tests/integration/mcpi_install/__init__.py deleted file mode 100644 index 10b45d523a5..00000000000 --- a/python/tests/integration/mcpi_install/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# ***************************************************************************** -# Copyright (c) 2026 IBM Corporation and other Contributors. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v1.0 -# which accompanies this distribution, and is available at -# http://www.eclipse.org/legal/epl-v10.html -# -# ***************************************************************************** diff --git a/python/tests/integration/mcpi_install/conftest.py b/python/tests/integration/mcpi_install/conftest.py deleted file mode 100644 index aef0b08cd98..00000000000 --- a/python/tests/integration/mcpi_install/conftest.py +++ /dev/null @@ -1,26 +0,0 @@ -# ***************************************************************************** -# Copyright (c) 2026 IBM Corporation and other Contributors. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v1.0 -# which accompanies this distribution, and is available at -# http://www.eclipse.org/legal/epl-v10.html -# -# ***************************************************************************** - -"""Conftest for mcpi_install tests. - -Automatically applies the 'mcpi-install' marker to all tests in this directory and subdirectories. -""" - -import pytest -from pathlib import Path - - -def pytest_collection_modifyitems(config, items): - """Auto-apply mcpi-install marker to all tests in this directory.""" - mcpi_install_dir = Path(__file__).parent - for item in items: - item_path = Path(item.fspath) - if mcpi_install_dir in item_path.parents or item_path.parent == mcpi_install_dir: - item.add_marker(pytest.mark.mcpi_install) diff --git a/python/tests/integration/mcpi_install/test_mcpi_install_app.py b/python/tests/integration/mcpi_install/test_mcpi_install_app.py deleted file mode 100644 index a25f51f2472..00000000000 --- a/python/tests/integration/mcpi_install/test_mcpi_install_app.py +++ /dev/null @@ -1,117 +0,0 @@ -# ***************************************************************************** -# Copyright (c) 2026 IBM Corporation and other Contributors. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v1.0 -# which accompanies this distribution, and is available at -# http://www.eclipse.org/legal/epl-v10.html -# -# ***************************************************************************** - -"""Integration tests for McpiInstallApp non-interactive mode. - -Tests verify that the CLI correctly collects parameters and launches the -Tekton MCPI install pipeline. -""" - -from unittest import mock -from unittest.mock import MagicMock - -from kubernetes.dynamic import DynamicClient -from kubernetes.dynamic.exceptions import NotFoundError -from kubernetes.client.exceptions import ApiException - -from mas.cli.mcpi.install.app import McpiInstallApp - - -def test_install_noninteractive(tmpdir): - """Test full non-interactive MCPI install completes without error. - - GIVEN all required CLI flags are provided (mas-catalog-version, ibm-entitlement-key, - mas-instance-id, storage classes, license-file, contact details, mcpi-channel) - WHEN McpiInstallApp.install() is called with those flags and --no-confirm - THEN launchMcpiInstallPipeline is called exactly once and returns without error. - """ - tmpdir.join("authorized_entitlement.lic").write("testLicense") - - with mock.patch("mas.cli.cli.config"): - dynamicClient = MagicMock(DynamicClient) - resources = MagicMock() - dynamicClient.resources = resources - - routesApi = MagicMock() - catalogApi = MagicMock() - crdApi = MagicMock() - namespaceApi = MagicMock() - clusterRoleBindingApi = MagicMock() - pvcApi = MagicMock() - secretApi = MagicMock() - - resourceApis = { - "CatalogSource": catalogApi, - "Route": routesApi, - "CustomResourceDefinition": crdApi, - "Namespace": namespaceApi, - "ClusterRoleBinding": clusterRoleBindingApi, - "PersistentVolumeClaim": pvcApi, - "Secret": secretApi, - } - resources.get.side_effect = lambda **kwargs: resourceApis.get(kwargs["kind"], None) - - route = MagicMock() - route.spec = MagicMock() - route.spec.host = "maximo.ibm.com" - routesApi.get.return_value = route - catalogApi.get.side_effect = NotFoundError(ApiException(status="404")) - - with ( - mock.patch("mas.cli.cli.DynamicClient") as mockDynamicClientClass, - mock.patch("mas.cli.cli.getNodes") as mockGetNodes, - mock.patch("mas.cli.cli.isAirgapInstall") as mockIsAirgapInstall, - mock.patch("mas.cli.mcpi.install.app.installOpenShiftPipelines"), - mock.patch("mas.cli.mcpi.install.app.updateTektonDefinitions"), - mock.patch("mas.cli.mcpi.install.app.prepareMcpiPipelinesNamespace"), - mock.patch("mas.cli.mcpi.install.app.prepareInstallSecrets"), - mock.patch("mas.cli.mcpi.install.app.testCLI"), - mock.patch("mas.cli.mcpi.install.app.launchMcpiInstallPipeline") as mockLaunchPipeline, - ): - mockDynamicClientClass.return_value = dynamicClient - mockGetNodes.return_value = [{"status": {"nodeInfo": {"architecture": "amd64"}}}] - mockIsAirgapInstall.return_value = False - mockLaunchPipeline.return_value = "https://pipeline.test.maximo.ibm.com" - - with mock.patch("mas.cli.cli.isSNO") as mockIsSno: - mockIsSno.return_value = False - app = McpiInstallApp() - app.install( - [ - "--mas-catalog-version", - "v9-250828-amd64", - "--ibm-entitlement-key", - "testEntitlementKey", - "--mas-instance-id", - "test1", - "--storage-class-rwo", - "nfs-client", - "--storage-class-rwx", - "nfs-client", - "--storage-pipeline", - "nfs-client", - "--storage-accessmode", - "ReadWriteMany", - "--license-file", - f"{tmpdir}/authorized_entitlement.lic", - "--contact-email", - "maximo@ibm.com", - "--contact-firstname", - "Test", - "--contact-lastname", - "User", - "--mcpi-channel", - "v9.2", - "--accept-license", - "--no-confirm", - ] - ) - - mockLaunchPipeline.assert_called_once() diff --git a/tekton/generate-tekton-pipelines.yml b/tekton/generate-tekton-pipelines.yml index e52c371aebf..ceaa590a00d 100644 --- a/tekton/generate-tekton-pipelines.yml +++ b/tekton/generate-tekton-pipelines.yml @@ -43,8 +43,6 @@ - mas-fvt-launcher - mas-ivt-manage - mas-fvt-facilities - # MCPI Pipelines - - mcpi-install # AI Service Pipelines - aiservice-upgrade # AI Service FVT Pipelines diff --git a/tekton/generate-tekton-tasks.yml b/tekton/generate-tekton-tasks.yml index 54416006c4a..df80d8d9a95 100644 --- a/tekton/generate-tekton-tasks.yml +++ b/tekton/generate-tekton-tasks.yml @@ -42,16 +42,7 @@ - sls - turbonomic - # 3. Generate Tasks (MCPI) - # ------------------------------------------------------------------------- - - name: Generate Tasks (MCPI) - ansible.builtin.template: - src: "{{ task_src_dir }}/mcpi/{{ item }}.yml.j2" - dest: "{{ task_target_dir }}/{{ item }}.yaml" - with_items: - - mcpi - - # 4. Generate Tasks (AI Service) + # 3. Generate Tasks (AI Service) # ------------------------------------------------------------------------- - name: Generate Tasks (AI Service) ansible.builtin.template: diff --git a/tekton/src/params/install-mcpi.yml.j2 b/tekton/src/params/install-mcpi.yml.j2 deleted file mode 100644 index 09be14712f2..00000000000 --- a/tekton/src/params/install-mcpi.yml.j2 +++ /dev/null @@ -1,6 +0,0 @@ -# MAS Add-on Configuration - Maximo Cluster Performance Insights (MCPI) -# ----------------------------------------------------------------------------- -- name: mcpi_channel - type: string - description: Default channel for IBM Maximo Cluster Performance Insights operator subscription - default: "" diff --git a/tekton/src/params/install.yml.j2 b/tekton/src/params/install.yml.j2 index d676529c412..ae17c11b2df 100644 --- a/tekton/src/params/install.yml.j2 +++ b/tekton/src/params/install.yml.j2 @@ -859,7 +859,3 @@ # ----------------------------------------------------------------------------- {{ lookup('template', params_src_dir ~ '/install-aiservice.yml.j2') }} -# MCPI Configuration -# ----------------------------------------------------------------------------- -{{ lookup('template', params_src_dir ~ '/install-mcpi.yml.j2') }} - diff --git a/tekton/src/pipelines/mas-install.yml.j2 b/tekton/src/pipelines/mas-install.yml.j2 index 55bee44e171..63bd612498e 100644 --- a/tekton/src/pipelines/mas-install.yml.j2 +++ b/tekton/src/pipelines/mas-install.yml.j2 @@ -50,8 +50,6 @@ spec: # 12. Install & Configure Visual Inspection # 13. Install & Configure Facilities # 14. Install & Configure AI Service with Default Tenant - # 15. Install & Configure MCPI - - name: pipeline-start timeout: "0" taskRef: @@ -461,13 +459,7 @@ spec: operator: notin values: ["", "9.0.*", "9.1.*"] - # 15. Install and configure MCPI (Maximo Cluster Performance Insights) - # ------------------------------------------------------------------------- - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/mcpi/mcpi.yml.j2') | indent(4) }} - runAfter: - - suite-config - - # 16. Verify health of the cluster before we consider the install complete + # 15. Verify health of the cluster before we consider the install complete # ------------------------------------------------------------------------- {{ lookup('template', pipeline_src_dir ~ '/taskdefs/cluster-setup/ocp-verify.yml.j2', template_vars={'name': 'post-install-verify', 'devops_suite_name': 'post-install-verify'}) | indent(4) }} runAfter: @@ -481,7 +473,6 @@ spec: - eck - cognos - aiservice - - mcpi finally: # Update synchronization configmap diff --git a/tekton/src/pipelines/mcpi-install.yml.j2 b/tekton/src/pipelines/mcpi-install.yml.j2 deleted file mode 100644 index 627366872f3..00000000000 --- a/tekton/src/pipelines/mcpi-install.yml.j2 +++ /dev/null @@ -1,183 +0,0 @@ ---- -apiVersion: tekton.dev/v1 -kind: Pipeline -metadata: - name: mcpi-install -spec: - workspaces: - # The generated configuration files - - name: shared-configs - # Any pre-generated configs that will be copied into the shared-configs workspace during suite-install - - name: shared-additional-configs - # The SLS entitlement key file that will be installed during install-sls - - name: shared-entitlement - # Pre-generated certificates - - name: shared-certificates - # PodTemplates configurations - - name: shared-pod-templates - - params: - # 1. Common Parameters - # ------------------------------------------------------------------------- - {{ lookup('template', params_src_dir ~ '/common.yml.j2') | indent(4) }} - - # 2. Installation - # ------------------------------------------------------------------------- - {{ lookup('template', params_src_dir ~ '/install-common.yml.j2') | indent(4) }} - - # IBM Operator Catalog Parameters - # ------------------------------------------------------------------------- - {{ lookup('template', params_src_dir ~ '/install-ibmcatalogs.yml.j2') | indent(4) }} - - # Certificate Manager Parameters - # ------------------------------------------------------------------------- - {{ lookup('template', params_src_dir ~ '/install-cert-manager.yml.j2') | indent(4) }} - - # SLS Parameters - # ------------------------------------------------------------------------- - - name: sls_namespace - type: string - default: "" - - name: sls_entitlement_file - type: string - default: "" - - name: sls_mongodb_cfg_file - type: string - default: "/workspace/configs/mongo-mongoce.yml" - - name: sls_catalog_source - type: string - default: "" - - name: sls_channel - type: string - default: "" - - name: sls_icr_cpopen - type: string - default: "" - - name: sls_action - type: string - default: "" - - # DRO Parameters - # ------------------------------------------------------------------------- - - name: dro_action - type: string - default: "" - - name: dro_contact_email - type: string - default: "" - - name: dro_contact_firstname - type: string - default: "" - - name: dro_contact_lastname - type: string - default: "" - - name: dro_namespace - type: string - default: "" - - name: aiservice_instance_id - type: string - default: "" - - name: ocp_ingress_tls_secret_name - type: string - default: "" - - # MAS ICR Parameters - # ------------------------------------------------------------------------- - - name: mas_icr_cp - type: string - default: "" - - name: mas_icr_cpopen - type: string - default: "" - - # MCPI Add-on Parameters - # ------------------------------------------------------------------------- - {{ lookup('template', params_src_dir ~ '/install-mcpi.yml.j2') | indent(4) }} - - tasks: - - name: pipeline-start - timeout: "0" - taskRef: - kind: Task - name: mas-devops-update-pipeline-status - params: - - name: image_pull_policy - value: $(params.image_pull_policy) - - name: pipeline_status - value: Started - - name: pipeline_name - value: $(context.pipeline.name) - - name: pipelinerun_name - value: $(context.pipelineRun.name) - - name: pipelinerun_namespace - value: $(context.pipelineRun.namespace) - - # 1. IBM Operator Catalog - # ------------------------------------------------------------------------- - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/cluster-setup/ibm-catalogs.yml.j2') | indent(4) }} - runAfter: - - pipeline-start - - # 2. Certificate Manager - # ------------------------------------------------------------------------- - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/cluster-setup/cert-manager.yml.j2') | indent(4) }} - runAfter: - - ibm-catalogs - - # 3. IBM SLS - # ------------------------------------------------------------------------- - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/sls.yml.j2') | indent(4) }} - runAfter: - - cert-manager - - # 4. DRO - # ------------------------------------------------------------------------- - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/dependencies/dro.yml.j2') | indent(4) }} - runAfter: - - ibm-catalogs - - # 5. Install and configure MCPI - # ------------------------------------------------------------------------- - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/mcpi/mcpi.yml.j2') | indent(4) }} - runAfter: - - sls - - dro - - # 6. Verify health of the cluster - # ------------------------------------------------------------------------- - {{ lookup('template', pipeline_src_dir ~ '/taskdefs/cluster-setup/ocp-verify.yml.j2', template_vars={'name': 'post-install-verify', 'devops_suite_name': 'post-install-verify'}) | indent(4) }} - runAfter: - - mcpi - - finally: - # Update synchronization configmap - # ------------------------------------------------------------------------- - - name: sync-install - timeout: "0" - taskRef: - kind: Task - name: mas-devops-update-configmap - params: - - name: image_pull_policy - value: $(params.image_pull_policy) - - name: configmap_name - value: sync-install - - name: configmap_value - value: $(tasks.status) - - - name: pipeline-finish - timeout: "0" - taskRef: - kind: Task - name: mas-devops-update-pipeline-status - params: - - name: image_pull_policy - value: $(params.image_pull_policy) - - name: pipeline_status - value: $(tasks.status) - - name: pipeline_name - value: $(context.pipeline.name) - - name: pipelinerun_name - value: $(context.pipelineRun.name) - - name: pipelinerun_namespace - value: $(context.pipelineRun.namespace) diff --git a/tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2 b/tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2 deleted file mode 100644 index 850844ea8ea..00000000000 --- a/tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2 +++ /dev/null @@ -1,37 +0,0 @@ -- name: mcpi - timeout: "0" - params: - {{ lookup('template', 'taskdefs/common/cli-params.yml.j2') | indent(4) }} - - name: devops_suite_name - value: mcpi - - - name: artifactory_username - value: $(params.artifactory_username) - - name: artifactory_token - value: $(params.artifactory_token) - - - name: ibm_entitlement_key - value: $(params.ibm_entitlement_key) - - - name: custom_labels - value: $(params.custom_labels) - - - name: mas_instance_id - value: $(params.mas_instance_id) - - name: mcpi_channel - value: "$(params.mcpi_channel)" - - name: mas_icr_cp - value: $(params.mas_icr_cp) - - name: mas_icr_cpopen - value: $(params.mas_icr_cpopen) - - taskRef: - name: mas-devops-mcpi - kind: Task - when: - - input: "$(params.mcpi_channel)" - operator: notin - values: [""] - workspaces: - - name: configs - workspace: shared-configs diff --git a/tekton/src/tasks/mcpi/mcpi.yml.j2 b/tekton/src/tasks/mcpi/mcpi.yml.j2 deleted file mode 100644 index de5611c976d..00000000000 --- a/tekton/src/tasks/mcpi/mcpi.yml.j2 +++ /dev/null @@ -1,101 +0,0 @@ ---- -apiVersion: tekton.dev/v1 -kind: Task -metadata: - name: mas-devops-mcpi -spec: - params: - {{ lookup('template', task_src_dir ~ '/common/cli-params.yml.j2') | indent(4) }} - - # Pre-Release Support - - name: artifactory_username - default: '' - type: string - description: Required to use development MAS builds - - name: artifactory_token - default: '' - type: string - description: Required to use development MAS builds - - # Entitlement - - name: ibm_entitlement_key - type: string - - # MAS instance - - name: mas_instance_id - type: string - description: MAS Instance ID - - # MCPI - - name: mcpi_channel - type: string - description: Catalog channel for the MCPI operator (used by gencfg step) - - # Custom Label Support - - name: custom_labels - type: string - description: Optional MAS custom labels, comma separated list of key=value pairs - default: "" - - # ICR - - name: mas_icr_cp - type: string - default: "" - - name: mas_icr_cpopen - type: string - default: "" - - stepTemplate: - env: - {{ lookup('template', task_src_dir ~ '/common/cli-env.yml.j2') | indent(6) }} - - # Pre-Release Support - - name: ARTIFACTORY_USERNAME - value: $(params.artifactory_username) - - name: ARTIFACTORY_TOKEN - value: $(params.artifactory_token) - - # Entitlement - - name: IBM_ENTITLEMENT_KEY - value: $(params.ibm_entitlement_key) - - # MAS instance - - name: MAS_INSTANCE_ID - value: $(params.mas_instance_id) - - name: MAS_CONFIG_DIR - value: /workspace/configs - - # MCPI - - name: MCPI_CHANNEL - value: $(params.mcpi_channel) - - # Labels - - name: CUSTOM_LABELS - value: $(params.custom_labels) - - # ICR - - name: MAS_ICR_CP - value: $(params.mas_icr_cp) - - name: MAS_ICR_CPOPEN - value: $(params.mas_icr_cpopen) - - steps: - - name: gencfg-mcpicfg - image: "quay.io/ibmmas/cli:latest" - imagePullPolicy: $(params.image_pull_policy) - command: - - /opt/app-root/src/run-role.sh - - gencfg_mcpicfg - workingDir: /workspace/configs - - - name: suite-config - image: "quay.io/ibmmas/cli:latest" - imagePullPolicy: $(params.image_pull_policy) - command: - - /opt/app-root/src/run-role.sh - - suite_config - workingDir: /workspace/configs - - workspaces: - - name: configs - optional: true From d7ba653ee62c3493396996b49129f306477ba473 Mon Sep 17 00:00:00 2001 From: Thierno-Bah1 Date: Sun, 11 Oct 2026 12:19:24 -0400 Subject: [PATCH 12/13] test(mcpi-cli-install): add mcpi prompt response to interactive install tests --- .../integration/install/test_aiservice_interactive_dns.py | 1 + python/tests/integration/install/test_arcgis_cluster_mode.py | 3 +++ python/tests/integration/install/test_dev_mode.py | 3 +++ python/tests/integration/install/test_existing_catalog.py | 1 + python/tests/integration/install/test_letsencrypt.py | 1 + python/tests/integration/install/test_no_catalog.py | 1 + 6 files changed, 10 insertions(+) diff --git a/python/tests/integration/install/test_aiservice_interactive_dns.py b/python/tests/integration/install/test_aiservice_interactive_dns.py index 472eef0d31b..9c51aa7a2c9 100644 --- a/python/tests/integration/install/test_aiservice_interactive_dns.py +++ b/python/tests/integration/install/test_aiservice_interactive_dns.py @@ -77,6 +77,7 @@ def _base_prompts(tmpdir): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "y", + ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", ".*Custom channel for AI Service.*": lambda msg: "9.2.x", ".*Customize database settings.*": lambda msg: "n", ".*Enter AI Service Tenant ID to bind with Manage:.*": lambda msg: "user", diff --git a/python/tests/integration/install/test_arcgis_cluster_mode.py b/python/tests/integration/install/test_arcgis_cluster_mode.py index 24d67f1a555..4e3e2b12f05 100755 --- a/python/tests/integration/install/test_arcgis_cluster_mode.py +++ b/python/tests/integration/install/test_arcgis_cluster_mode.py @@ -128,6 +128,7 @@ def test_install_arcgis_cluster_mode_success(tmpdir): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", + ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", ".*Include IBM Maximo Location Services for Esri.*": lambda msg: "y", ".*Do you accept the license terms?.*": lambda msg: "y", # 23. Grafana configuration (appears when advanced options are enabled) @@ -290,6 +291,7 @@ def test_install_arcgis_namespace_mode_error(tmpdir, caplog): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", + ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", # ERROR OCCURS HERE - after selecting ArcGIS with cluster mode ".*Include IBM Maximo Location Services for Esri.*": lambda msg: "y", } @@ -433,6 +435,7 @@ def test_install_arcgis_minimal_mode_error(tmpdir, caplog): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", + ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", # ERROR OCCURS HERE - after selecting ArcGIS with minimal mode ".*Include IBM Maximo Location Services for Esri.*": lambda msg: "y", } diff --git a/python/tests/integration/install/test_dev_mode.py b/python/tests/integration/install/test_dev_mode.py index 22c6f423cf3..4e4fd535d25 100644 --- a/python/tests/integration/install/test_dev_mode.py +++ b/python/tests/integration/install/test_dev_mode.py @@ -98,6 +98,7 @@ def test_install_master_dev_mode(tmpdir): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", + ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", # 11. Grafana configuration ".*Install Grafana.*": lambda msg: "y", # 12. MongoDB configuration @@ -181,6 +182,7 @@ def test_install_master_dev_mode_existing_catalog(tmpdir): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", + ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", # 11. Grafana configuration ".*Install Grafana.*": lambda msg: "y", # 12. MongoDB configuration @@ -322,6 +324,7 @@ def test_install_master_dev_mode_with_path_routing(tmpdir): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", + ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", # 23. Grafana configuration (appears when advanced options are enabled) ".*Install Grafana.*": lambda msg: "y", # 24. MongoDB configuration diff --git a/python/tests/integration/install/test_existing_catalog.py b/python/tests/integration/install/test_existing_catalog.py index 7c8f5960b8d..e2f84abe3ac 100644 --- a/python/tests/integration/install/test_existing_catalog.py +++ b/python/tests/integration/install/test_existing_catalog.py @@ -61,6 +61,7 @@ def test_install_interactive_existing_catalog(tmpdir): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", + ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", # 12. Grafana configuration ".*Install Grafana.*": lambda msg: "y", # 13. MongoDB configuration diff --git a/python/tests/integration/install/test_letsencrypt.py b/python/tests/integration/install/test_letsencrypt.py index 6c3b63aa7d0..b33278a6d2a 100644 --- a/python/tests/integration/install/test_letsencrypt.py +++ b/python/tests/integration/install/test_letsencrypt.py @@ -361,6 +361,7 @@ def _common_handlers(self, tmpdir, le_response): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", + ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", ".*Do you want to configure AiCfg.*": lambda msg: "n", ".*Install Grafana.*": lambda msg: "y", # MongoDB diff --git a/python/tests/integration/install/test_no_catalog.py b/python/tests/integration/install/test_no_catalog.py index caea1d5e766..b803cdeeb26 100644 --- a/python/tests/integration/install/test_no_catalog.py +++ b/python/tests/integration/install/test_no_catalog.py @@ -61,6 +61,7 @@ def test_install_interactive_no_catalog(tmpdir): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", + ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", # 12a. Grafana configuration ".*Install Grafana.*": lambda msg: "y", # 12. MongoDB configuration From 2f05b194a312d6768ae42de2fee9c274c9cddc94 Mon Sep 17 00:00:00 2001 From: Thierno-Bah1 Date: Tue, 13 Oct 2026 00:33:59 -0400 Subject: [PATCH 13/13] refactor(mcpi-cli-install): replace --mcpi-channel with --install-mcpi --- python/src/mas/cli/install/app.py | 16 ++-- python/src/mas/cli/install/argBuilder.py | 2 +- python/src/mas/cli/install/argParser.py | 7 +- .../cli/install/settings/additionalConfigs.py | 27 ++++++ python/src/mas/cli/install/summarizer.py | 2 +- .../integration/install/test_dev_mode.py | 2 - .../install/test_existing_catalog.py | 1 - .../integration/install/test_no_catalog.py | 1 - python/tests/unit/test_install_mcpi_config.py | 84 +++++++++++++++++++ python/tests/unit/test_install_mcpi_flag.py | 49 +++++++++++ 10 files changed, 177 insertions(+), 14 deletions(-) create mode 100644 python/tests/unit/test_install_mcpi_config.py create mode 100644 python/tests/unit/test_install_mcpi_flag.py diff --git a/python/src/mas/cli/install/app.py b/python/src/mas/cli/install/app.py index 96877527f9c..e10f0924355 100644 --- a/python/src/mas/cli/install/app.py +++ b/python/src/mas/cli/install/app.py @@ -1355,9 +1355,11 @@ def configApps(self): else: self.installAIService = False - self.installMcpi = self.yesOrNo("Install Maximo Cluster Performance Insights (MCPI)") - if self.installMcpi: - self.promptForString("MCPI subscription channel", "mcpi_channel", validator=NotEmptyValidator()) + # MCPI is only available from MAS 9.2 + if isVersionEqualOrAfter("9.2.0", self.getParam("mas_channel")): + self.installMcpi = self.yesOrNo("Install Maximo Cluster Performance Insights (MCPI)") + else: + self.installMcpi = False @logMethodCall def configAppChannel(self, appId): @@ -2390,9 +2392,8 @@ def nonInteractiveMode(self) -> None: if value is not None and value != "": self.setParam("mas_app_channel_facilities", value) self.installFacilities = True - elif key == "mcpi_channel": - if value is not None and value != "": - self.setParam("mcpi_channel", value) + elif key == "install_mcpi": + if value: self.installMcpi = True elif key == "aiservice_channel": if value is not None and value != "": @@ -2640,6 +2641,9 @@ def nonInteractiveMode(self) -> None: self.licensePrompt() self.setParam("db2u_kind", "db2ucluster") + if self.installMcpi and not isVersionEqualOrAfter("9.2.0", self.getParam("mas_channel")): + self.fatalError(f"--install-mcpi is only supported for MAS 9.2+ (selected channel: {self.getParam('mas_channel')})") + if self.getParam("mas_issuer_kind") != "" and not isVersionEqualOrAfter("9.2.0", self.getParam("mas_channel")): self.fatalError(f"--mas-issuer-kind is only supported for MAS 9.2+ (selected channel: {self.getParam('mas_channel')})") diff --git a/python/src/mas/cli/install/argBuilder.py b/python/src/mas/cli/install/argBuilder.py index 107995befe2..6d885a8a50f 100644 --- a/python/src/mas/cli/install/argBuilder.py +++ b/python/src/mas/cli/install/argBuilder.py @@ -287,7 +287,7 @@ def buildCommand(self) -> str: if self.getParam("configure_aiassistant") != "": command += f" --configure-aiassistant \"{self.getParam('configure_aiassistant')}\"{newline}" if self.installMcpi: - command += f" --mcpi-channel \"{self.getParam('mcpi_channel')}\"{newline}" + command += f" --install-mcpi{newline}" # Arcgis # ----------------------------------------------------------------------------- diff --git a/python/src/mas/cli/install/argParser.py b/python/src/mas/cli/install/argParser.py index c0ad5317af5..b85d309adb2 100644 --- a/python/src/mas/cli/install/argParser.py +++ b/python/src/mas/cli/install/argParser.py @@ -605,9 +605,12 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: help="Subscription channel for Maximo AI Service", ) masAppsArgGroup.add_argument( - "--mcpi-channel", + "--install-mcpi", + dest="install_mcpi", required=False, - help="Subscription channel for Maximo Cluster Performance Insights", + action="store_true", + default=False, + help="Install Maximo Cluster Performance Insights (MAS 9.2 or later)", ) # Arcgis diff --git a/python/src/mas/cli/install/settings/additionalConfigs.py b/python/src/mas/cli/install/settings/additionalConfigs.py index 197bded9bb8..2e8a4e80b99 100644 --- a/python/src/mas/cli/install/settings/additionalConfigs.py +++ b/python/src/mas/cli/install/settings/additionalConfigs.py @@ -19,6 +19,22 @@ logger = logging.getLogger(__name__) +# McpiCfg resource applied by the suite-config task (Jinja2 variables are resolved by kubernetes.core.k8s template) +MCPI_CONFIG_FILENAME = "mcpi-system.yaml" +MCPI_CONFIG_TEMPLATE = """--- +apiVersion: config.mas.ibm.com/v1 +kind: McpiCfg +metadata: + name: "{{ mas_instance_id }}-mcpi-system" + namespace: "mas-{{ mas_instance_id }}-core" + labels: + mas.ibm.com/configScope: system + mas.ibm.com/instanceId: "{{ mas_instance_id }}" +spec: + displayName: "Maximo Cluster Performance Insights" + config: + enabled: true +""" if TYPE_CHECKING: from prompt_toolkit.completion import WordCompleter @@ -29,6 +45,7 @@ class AdditionalConfigsMixin: if TYPE_CHECKING: # Attributes from BaseApp and other mixins params: Dict[str, str] + installMcpi: bool _interactiveMode: bool localConfigDir: str | None noConfirm: bool @@ -119,6 +136,16 @@ def additionalConfigs(self) -> None: logger.debug(additionalConfigsSecret) self.additionalConfigsSecret = additionalConfigsSecret + # MCPI is enabled by an McpiCfg resource applied by the suite-config task, so inject it into the secret + if self.installMcpi: + if self.additionalConfigsSecret is None: + self.additionalConfigsSecret = {"apiVersion": "v1", "kind": "Secret", "type": "Opaque", "metadata": {"name": "pipeline-additional-configs"}} + secretData = self.additionalConfigsSecret.setdefault("data", {}) + # A user supplied file with the same name takes precedence + if MCPI_CONFIG_FILENAME not in secretData: + secretData[MCPI_CONFIG_FILENAME] = b64encode(MCPI_CONFIG_TEMPLATE.encode("ascii")).decode("ascii") + print_formatted_text(f"Maximo Cluster Performance Insights (MCPI) configuration will be applied: {MCPI_CONFIG_FILENAME}") + def podTemplates(self) -> None: if self.isInteractiveMode and self.showAdvancedOptions: self.printH1("Configure Pod Templates") diff --git a/python/src/mas/cli/install/summarizer.py b/python/src/mas/cli/install/summarizer.py index 874ebeed1f0..5f0d0a1e23a 100644 --- a/python/src/mas/cli/install/summarizer.py +++ b/python/src/mas/cli/install/summarizer.py @@ -464,7 +464,7 @@ def aiServiceSummary(self) -> None: def mcpiSummary(self) -> None: if self.installMcpi: - self.printSummary("Maximo Cluster Performance Insights (MCPI)", self.getParam("mcpi_channel")) + self.printSummary("Maximo Cluster Performance Insights (MCPI)", "Install") else: self.printSummary("Maximo Cluster Performance Insights (MCPI)", "Do Not Install") diff --git a/python/tests/integration/install/test_dev_mode.py b/python/tests/integration/install/test_dev_mode.py index 4e4fd535d25..b381a00bf8c 100644 --- a/python/tests/integration/install/test_dev_mode.py +++ b/python/tests/integration/install/test_dev_mode.py @@ -98,7 +98,6 @@ def test_install_master_dev_mode(tmpdir): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", - ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", # 11. Grafana configuration ".*Install Grafana.*": lambda msg: "y", # 12. MongoDB configuration @@ -182,7 +181,6 @@ def test_install_master_dev_mode_existing_catalog(tmpdir): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", - ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", # 11. Grafana configuration ".*Install Grafana.*": lambda msg: "y", # 12. MongoDB configuration diff --git a/python/tests/integration/install/test_existing_catalog.py b/python/tests/integration/install/test_existing_catalog.py index e2f84abe3ac..7c8f5960b8d 100644 --- a/python/tests/integration/install/test_existing_catalog.py +++ b/python/tests/integration/install/test_existing_catalog.py @@ -61,7 +61,6 @@ def test_install_interactive_existing_catalog(tmpdir): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", - ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", # 12. Grafana configuration ".*Install Grafana.*": lambda msg: "y", # 13. MongoDB configuration diff --git a/python/tests/integration/install/test_no_catalog.py b/python/tests/integration/install/test_no_catalog.py index b803cdeeb26..caea1d5e766 100644 --- a/python/tests/integration/install/test_no_catalog.py +++ b/python/tests/integration/install/test_no_catalog.py @@ -61,7 +61,6 @@ def test_install_interactive_no_catalog(tmpdir): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", - ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", # 12a. Grafana configuration ".*Install Grafana.*": lambda msg: "y", # 12. MongoDB configuration diff --git a/python/tests/unit/test_install_mcpi_config.py b/python/tests/unit/test_install_mcpi_config.py new file mode 100644 index 00000000000..483807df118 --- /dev/null +++ b/python/tests/unit/test_install_mcpi_config.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** + +"""Test that the install CLI injects the McpiCfg resource into the additional configs secret.""" + +from base64 import b64decode + +from mas.cli.install.settings.additionalConfigs import AdditionalConfigsMixin, MCPI_CONFIG_FILENAME + + +class FakeApp(AdditionalConfigsMixin): + """Minimal host for AdditionalConfigsMixin.""" + + def __init__(self, installMcpi, localConfigDir=None): + self.isInteractiveMode = False + self.noConfirm = True + self.installMcpi = installMcpi + self.localConfigDir = localConfigDir + self.additionalConfigsSecret = None + + +def test_mcpi_config_added_when_mcpi_enabled(): + """Test that the McpiCfg file is added to the secret when MCPI is enabled. + + GIVEN MCPI is enabled and no local config directory is set + WHEN additionalConfigs() runs + THEN the pipeline-additional-configs secret contains the McpiCfg YAML + """ + app = FakeApp(installMcpi=True) + app.additionalConfigs() + + assert app.additionalConfigsSecret["metadata"]["name"] == "pipeline-additional-configs" + content = b64decode(app.additionalConfigsSecret["data"][MCPI_CONFIG_FILENAME]).decode() + assert "kind: McpiCfg" in content + assert "{{ mas_instance_id }}-mcpi-system" in content + + +def test_mcpi_config_not_added_when_mcpi_disabled(): + """Test that no secret is created when MCPI is disabled. + + GIVEN MCPI is not enabled and no local config directory is set + WHEN additionalConfigs() runs + THEN no additional configs secret is created + """ + app = FakeApp(installMcpi=False) + app.additionalConfigs() + + assert app.additionalConfigsSecret is None + + +def test_mcpi_config_merged_with_user_configs(tmp_path): + """Test that the McpiCfg file is merged with user supplied configs. + + GIVEN MCPI is enabled and a local config directory with one YAML file + WHEN additionalConfigs() runs + THEN the secret contains both the user file and the McpiCfg file + """ + (tmp_path / "user.yaml").write_text("kind: ConfigMap\n") + app = FakeApp(installMcpi=True, localConfigDir=str(tmp_path)) + app.additionalConfigs() + + assert set(app.additionalConfigsSecret["data"].keys()) == {"user.yaml", MCPI_CONFIG_FILENAME} + + +def test_user_supplied_mcpi_config_takes_precedence(tmp_path): + """Test that a user supplied file with the same name is not overwritten. + + GIVEN MCPI is enabled and the local config directory already has the MCPI file + WHEN additionalConfigs() runs + THEN the user's content is preserved + """ + (tmp_path / MCPI_CONFIG_FILENAME).write_text("kind: McpiCfg\n# custom\n") + app = FakeApp(installMcpi=True, localConfigDir=str(tmp_path)) + app.additionalConfigs() + + assert "# custom" in b64decode(app.additionalConfigsSecret["data"][MCPI_CONFIG_FILENAME]).decode() diff --git a/python/tests/unit/test_install_mcpi_flag.py b/python/tests/unit/test_install_mcpi_flag.py new file mode 100644 index 00000000000..78af1b68d0a --- /dev/null +++ b/python/tests/unit/test_install_mcpi_flag.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** + +"""Test the --install-mcpi flag on the install command argument parser.""" + +import pytest + +from mas.cli.install.argParser import installArgParser + + +def test_install_mcpi_defaults_to_false(): + """Test that MCPI is not installed unless requested. + + GIVEN no --install-mcpi argument + WHEN the install arguments are parsed + THEN install_mcpi is False + """ + args = installArgParser.parse_args([]) + assert args.install_mcpi is False + + +def test_install_mcpi_flag_sets_true(): + """Test that --install-mcpi enables the install. + + GIVEN the --install-mcpi argument + WHEN the install arguments are parsed + THEN install_mcpi is True + """ + args = installArgParser.parse_args(["--install-mcpi"]) + assert args.install_mcpi is True + + +def test_install_mcpi_flag_takes_no_value(): + """Test that --install-mcpi is a plain on/off flag. + + GIVEN --install-mcpi followed by a value + WHEN the install arguments are parsed + THEN the parser rejects the unrecognized value + """ + with pytest.raises(SystemExit): + installArgParser.parse_args(["--install-mcpi", "v9.2"])