From c05c354d88019680df28c09acd49b92898712079 Mon Sep 17 00:00:00 2001 From: David Parker Date: Thu, 14 May 2026 15:08:06 +0100 Subject: [PATCH 1/2] [minor] Support optional Kafka Image Processor (Manage 9.2+) --- .../2026-05-14-kafka-civil-image-processor.md | 359 ++++ .bob/rules/general-guidelines.md | 21 +- python/src/mas/cli/install/app.py | 1861 +++++++++++------ python/src/mas/cli/install/argBuilder.py | 558 +++-- python/src/mas/cli/install/argParser.py | 610 +++--- python/src/mas/cli/install/params.py | 12 +- .../mas/cli/install/settings/kafkaSettings.py | 51 +- .../cli/install/settings/manageSettings.py | 169 +- python/src/mas/cli/install/summarizer.py | 232 +- tekton/src/params/install.yml.j2 | 4 + .../pipelines/taskdefs/apps/manage-app.yml.j2 | 2 + 11 files changed, 2568 insertions(+), 1311 deletions(-) create mode 100644 .bob/plans/2026-05-14-kafka-civil-image-processor.md diff --git a/.bob/plans/2026-05-14-kafka-civil-image-processor.md b/.bob/plans/2026-05-14-kafka-civil-image-processor.md new file mode 100644 index 00000000000..a23b8bf0a0b --- /dev/null +++ b/.bob/plans/2026-05-14-kafka-civil-image-processor.md @@ -0,0 +1,359 @@ +# Kafka and Civil Infrastructure Image Processor + +## Objective + +Review and update the CLI install logic to properly handle Kafka configuration for IoT and Manage Civil Infrastructure component with optional Kafka Image Processor support. + +## Current State Analysis + +### Kafka Configuration Flow (Current) + +**Interactive Mode** ([`interactiveMode()`](python/src/mas/cli/install/app.py:1669-1726)): +1. Apps are configured via [`configApps()`](python/src/mas/cli/install/app.py:1137-1268) +2. Manage settings configured via [`manageSettings()`](python/src/mas/cli/install/settings/manageSettings.py:96-110) +3. Kafka configured via [`configKafka()`](python/src/mas/cli/install/settings/kafkaSettings.py:95-199) + +**Kafka Requirements** ([`kafkaSettings.py`](python/src/mas/cli/install/settings/kafkaSettings.py:76-93)): +- [`_requiresKafkaIoT()`](python/src/mas/cli/install/settings/kafkaSettings.py:76-77): Returns `True` if IoT is being installed +- [`_requiresKafkaCivil()`](python/src/mas/cli/install/settings/kafkaSettings.py:79-85): Returns `True` if: + - Manage is being installed AND + - Civil component is enabled (`"civil="` in `mas_appws_components`) AND + - Manage version is 9.2.0 or later + +**Civil Component Selection** ([`manageSettings.py`](python/src/mas/cli/install/settings/manageSettings.py:144-145)): +- Civil is selected as a simple yes/no prompt in [`manageSettingsComponents()`](python/src/mas/cli/install/settings/manageSettings.py:128-195) +- No mention of Kafka Image Processor during Civil selection + +**Non-Interactive Mode Validation** ([`app.py`](python/src/mas/cli/install/app.py:2149-2163)): +- Validates Kafka is configured if IoT is installed (lines 2149-2153) +- Validates Kafka is configured if Civil is enabled in Manage 9.2+ (lines 2155-2163) + +### Issues Identified + +1. **Missing Kafka Image Processor Option**: When Civil component is selected, user is not prompted about enabling Kafka Image Processor +2. **No PVC Storage Size Prompt**: If Kafka Image Processor is enabled, no prompt for PVC storage size +3. **Kafka Configuration Timing**: Kafka configuration happens AFTER Manage settings, but the decision about Kafka Image Processor should be made during Civil component selection +4. **No Parameter for Image Processor**: No parameter exists to track whether Kafka Image Processor is enabled for Civil + +## Required Changes + +### 1. Add New Instance Variable and Parameter + +Need to add: +- **Instance variable**: `self.enableKafkaImageProcessor` (boolean) - Similar to `self.devMode`, tracks whether Kafka Image Processor should be enabled. This is the **single control point** for Kafka Image Processor functionality in the CLI. +- **Parameter**: `mas_appws_bindings_kafka_manage` - Kafka binding configuration (similar to `mas_appws_bindings_jdbc_manage`) + +**Note**: +- No PVC size parameter needed - the pipeline uses a default of 10GB +- No storage class parameter needed - the pipeline will use `storage_class_rwx` automatically +- The `mas_appws_bindings_kafka_manage` parameter should be set to "system" when Kafka Image Processor is enabled (following the same pattern as JDBC bindings) + +### 2. Update Civil Component Selection Flow + +In [`manageSettingsComponents()`](python/src/mas/cli/install/settings/manageSettings.py:128-195): + +**Current flow** (line 144-145): +```python +if self.yesOrNo(" - Civil Infrastructure"): + self.params["mas_appws_components"] += ",civil=latest" +``` + +**New flow**: +```python +if self.yesOrNo(" - Civil Infrastructure"): + self.params["mas_appws_components"] += ",civil=latest" + + # Check if Manage version supports Kafka Image Processor (9.2+) + manageChannel = self.getParam("mas_app_channel_manage") + if manageChannel and isVersionEqualOrAfter('9.2.0', manageChannel): + self.printDescription([ + "", + "Civil Infrastructure Defect Detection with Kafka Image Processor:", + "The Kafka Image Processor enables advanced defect detection capabilities.", + "This requires a Kafka instance and uses 10GB of storage for image processing." + ]) + + if self.yesOrNo("Enable Kafka Image Processor for Civil Infrastructure"): + self.enableKafkaImageProcessor = True + # Bind Manage to system Kafka (similar to JDBC binding pattern) + self.setParam("mas_appws_bindings_kafka_manage", "system") +``` + +### 3. Simplify Kafka Requirements Logic + +In [`kafkaSettings.py`](python/src/mas/cli/install/settings/kafkaSettings.py:79-93), the [`_requiresKafkaCivil()`](python/src/mas/cli/install/settings/kafkaSettings.py:79-85) function is now **redundant** and should be **removed**. + +**Reason**: `self.enableKafkaImageProcessor` is now the single control point. We don't need a separate function to check if Civil requires Kafka - we just check the boolean directly. + +**Update [`_getKafkaRequirements()`](python/src/mas/cli/install/settings/kafkaSettings.py:87-93)**: + +**Current**: +```python +def _getKafkaRequirements(self) -> List[str]: + requirements = [] + if self._requiresKafkaIoT(): + requirements.append("Maximo IoT") + if self._requiresKafkaCivil(): + requirements.append("Manage Civil Infrastructure (9.2+) Defect Detection") + return requirements +``` + +**Updated**: +```python +def _getKafkaRequirements(self) -> List[str]: + requirements = [] + if self._requiresKafkaIoT(): + requirements.append("Maximo IoT") + if self.enableKafkaImageProcessor: + requirements.append("Manage Civil Infrastructure (9.2+) Kafka Image Processor") + return requirements +``` + +### 4. Update Kafka Configuration Logic + +In [`configKafka()`](python/src/mas/cli/install/settings/kafkaSettings.py:95-199), update to use `self.enableKafkaImageProcessor` directly: + +**Current** (lines 95-111): +```python +def configKafka(self) -> None: + requirements = self._getKafkaRequirements() + + if requirements: + self.printH1("Configure Kafka") + + # Build description based on what requires Kafka + hasIoT = self._requiresKafkaIoT() + hasCivil = self._requiresKafkaCivil() + + description = [] + if hasIoT and hasCivil: + description.append("Maximo IoT and Manage Civil Infrastructure (9.2+) Defect Detection require a shared system-scope Kafka instance") + elif hasIoT: + description.append("Maximo IoT requires a shared system-scope Kafka instance") + elif hasCivil: + description.append("Manage Civil Infrastructure (9.2+) Defect Detection functionality requires a shared system-scope Kafka instance") +``` + +**Updated**: +```python +def configKafka(self) -> None: + requirements = self._getKafkaRequirements() + + if requirements: + self.printH1("Configure Kafka") + + # Build description based on what requires Kafka + hasIoT = self._requiresKafkaIoT() + hasImageProcessor = self.enableKafkaImageProcessor + + description = [] + if hasIoT and hasImageProcessor: + description.append("Maximo IoT and Manage Civil Infrastructure Kafka Image Processor require a shared system-scope Kafka instance") + elif hasIoT: + description.append("Maximo IoT requires a shared system-scope Kafka instance") + elif hasImageProcessor: + description.append("Manage Civil Infrastructure Kafka Image Processor requires a shared system-scope Kafka instance") +``` + +**Key Change**: Use `self.enableKafkaImageProcessor` directly instead of calling `_requiresKafkaCivil()`. This makes `self.enableKafkaImageProcessor` the single control point. + +### 5. Update Non-Interactive Mode Validation + +In [`nonInteractiveMode()`](python/src/mas/cli/install/app.py:2155-2163), **replace** the existing Civil/Kafka validation with simplified parameter validation: + +**Remove Current Validation** (lines 2155-2163): +```python +# Validate Kafka requirements for CIVIL installation in non-interactive mode +isCivilEnabled = self.installManage and "civil=" in self.getParam("mas_appws_components") +if isCivilEnabled: + manageChannel = self.getParam("mas_app_channel_manage") + if manageChannel and isVersionEqualOrAfter('9.2.0', manageChannel): + kafkaAction = self.getParam("kafka_action_system") + hasKafkaConfig = kafkaAction in ["install", "byo"] + if not hasKafkaConfig: + self.fatalError("--manage-components with 'civil=' in Manage 9.2+ requires Kafka configuration...") +``` + +**Add New Validation** (simple parameter checks): +```python +# Validate --manage-kafka parameter requirements +if self.getParam("mas_appws_bindings_kafka_manage") != "": + # Validate Kafka provider is configured + kafkaProvider = self.getParam("kafka_provider") + if not kafkaProvider or kafkaProvider == "": + self.fatalError("--manage-kafka requires --kafka-provider to be set") + + # Validate Manage version compatibility + manageChannel = self.getParam("mas_app_channel_manage") + if manageChannel and not isVersionEqualOrAfter('9.2.0', manageChannel): + self.fatalError(f"--manage-kafka requires Manage version 9.2.0 or later. Current version: {manageChannel}") +``` + +**Key Changes:** +1. Simple parameter validation - no state management needed +2. Check if `--kafka-provider` is set when `--manage-kafka` is provided +3. Check if Manage version >= 9.2.0 when `--manage-kafka` is provided +4. No need to set `self.enableKafkaImageProcessor` in non-interactive mode - it's only used for interactive flow control + +### 6. Add CLI Arguments + +In [`argParser.py`](python/src/mas/cli/install/argParser.py), add new argument in the Manage section: + +```python +manageArgGroup.add_argument( + "--manage-kafka", + dest="mas_appws_bindings_kafka_manage", + required=False, + help="Select the Kafka configuration to bind to Manage (e.g., 'system'). Required for Civil Infrastructure Kafka Image Processor.", + default="" +) +``` + +**Note**: When this parameter is set to "system" in non-interactive mode, it indicates that Kafka Image Processor should be enabled. + +### 7. Update Parameter Lists + +Add new parameter to [`params.py`](python/src/mas/cli/install/params.py): + +```python +"mas_appws_bindings_kafka_manage", +``` + +**Note**: This should be added near `mas_appws_bindings_jdbc_manage` for consistency. + +### 8. Update Command Builder + +In [`argBuilder.py`](python/src/mas/cli/install/argBuilder.py), add command line generation for new parameter (near the `--manage-jdbc` line): + +```python +if self.getParam('mas_appws_bindings_kafka_manage') != "": + command += f" --manage-kafka \"{self.getParam('mas_appws_bindings_kafka_manage')}\"{newline}" +``` + +### 9. Update Installation Summary + +In [`summarizer.py`](python/src/mas/cli/install/summarizer.py), add display of Civil Image Processor settings when Civil is enabled: + +```python +if "civil=" in self.getParam("mas_appws_components"): + self.printSummary(" + Kafka Image Processor", + "Enabled" if self.enableKafkaImageProcessor else "Disabled") + if self.enableKafkaImageProcessor: + self.printParamSummary(" + Kafka Binding", "mas_appws_bindings_kafka_manage") +``` + +### 10. Update Tekton Pipeline Definitions + +**Add Parameter** in [`tekton/src/params/install.yml.j2`](tekton/src/params/install.yml.j2:557-560): + +Add after `mas_appws_bindings_jdbc_manage`: +```yaml +- name: mas_appws_bindings_kafka_manage + type: string + description: Select the Kafka configuration to bind to Manage + default: "" +``` + +**Pass Parameter to Task** in [`tekton/src/pipelines/taskdefs/apps/manage-app.yml.j2`](tekton/src/pipelines/taskdefs/apps/manage-app.yml.j2:37-38): + +Add after the `mas_appws_bindings_jdbc` parameter: +```yaml +- name: mas_appws_bindings_kafka + value: "$(params.mas_appws_bindings_kafka_manage)" +``` + +## Execution Plan + +- [x] **Phase 1: Add Instance Variable and Parameter** + - [x] Add `self.enableKafkaImageProcessor` instance variable initialization in [`__init__()`](python/src/mas/cli/install/app.py) + - [x] Add `mas_appws_bindings_kafka_manage` parameter to [`params.py`](python/src/mas/cli/install/params.py) + - [x] Add `--manage-kafka` CLI argument to [`argParser.py`](python/src/mas/cli/install/argParser.py) + - [x] Validate additions + +- [x] **Phase 2: Update Interactive Flow** + - [x] Modify [`manageSettingsComponents()`](python/src/mas/cli/install/settings/manageSettings.py:128-195) to prompt for Kafka Image Processor when Civil is selected + - [x] Set `self.enableKafkaImageProcessor = True` when user enables it + - [x] Set `mas_appws_bindings_kafka_manage` to "system" when enabled + +- [x] **Phase 3: Simplify Kafka Logic** + - [x] **Remove** [`_requiresKafkaCivil()`](python/src/mas/cli/install/settings/kafkaSettings.py:79-85) function (now redundant) + - [x] Update [`_getKafkaRequirements()`](python/src/mas/cli/install/settings/kafkaSettings.py:87-93) to use `self.enableKafkaImageProcessor` directly + - [x] Update [`configKafka()`](python/src/mas/cli/install/settings/kafkaSettings.py:95-199) to use `self.enableKafkaImageProcessor` directly + +- [x] **Phase 4: Update Non-Interactive Mode** + - [x] Update validation in [`nonInteractiveMode()`](python/src/mas/cli/install/app.py:2155-2163) to check `self.enableKafkaImageProcessor` + - [x] Set `self.enableKafkaImageProcessor = True` when `mas_appws_bindings_kafka_manage` parameter is set to "system" + +- [x] **Phase 5: Update Tekton Pipeline** + - [x] Add `mas_appws_bindings_kafka_manage` parameter to [`tekton/src/params/install.yml.j2`](tekton/src/params/install.yml.j2) + - [x] Pass parameter to manage task in [`tekton/src/pipelines/taskdefs/apps/manage-app.yml.j2`](tekton/src/pipelines/taskdefs/apps/manage-app.yml.j2) + - [x] Regenerate Tekton definitions using `ansible-playbook tekton/generate-tekton-pipelines.yml` + +- [x] **Phase 6: Update Supporting Code** + - [x] Update [`argBuilder.py`](python/src/mas/cli/install/argBuilder.py) for command generation + - [x] Update [`summarizer.py`](python/src/mas/cli/install/summarizer.py) to display Image Processor status and Kafka binding + - [x] Validate all Python changes with `autopep8` and `flake8` + +- [ ] **Phase 7: Automated Testing** + - [ ] Create `python/test/install/test_manage92_civil_kafka.py` with two test functions: + - [ ] `test_manage92_civil_no_kafka_interactive()` - Interactive test: Manage 9.2 with Civil, decline Kafka Image Processor + - Prompt handlers for Civil component selection + - Decline Kafka Image Processor when prompted + - Verify no Kafka configuration required + - Verify `mas_appws_bindings_kafka_manage` is empty + - [ ] `test_manage92_civil_with_kafka_interactive()` - Interactive test: Manage 9.2 with Civil, enable Kafka Image Processor + - Prompt handlers for Civil component selection + - Accept Kafka Image Processor when prompted + - Prompt handlers for Kafka configuration + - Verify `mas_appws_bindings_kafka_manage = "system"` + - Verify Kafka provider is configured + - [ ] `test_manage92_civil_with_kafka_non_interactive()` - Non-interactive test: Manage 9.2 with Civil and `--manage-kafka system` + - Use argv similar to [`test_install_master_dev_mode_non_interactive()`](python/test/install/test_dev_mode.py:372) + - Include `--manage-channel 9.2.x-dev` + - Include `--manage-components "base=latest,civil=latest"` + - Include `--manage-kafka system` + - Include `--kafka-provider strimzi` + - Verify installation completes successfully + - [ ] `test_manage92_civil_kafka_validation_errors()` - Non-interactive validation tests: + - Test `--manage-kafka` without `--kafka-provider` → expect error + - Test `--manage-kafka` with Manage < 9.2.0 → expect error + +## Validation + +After implementation, verify: + +**Interactive Mode:** +1. ✅ IoT installation prompts for Kafka configuration +2. ✅ Civil component selection (Manage 9.2+) prompts for Kafka Image Processor +3. ✅ If Image Processor enabled, sets `mas_appws_bindings_kafka_manage` to "system" +4. ✅ Kafka configuration only required if IoT OR `self.enableKafkaImageProcessor` is True + +**Non-Interactive Mode:** +5. ✅ Validates `--kafka-provider` is set when `--manage-kafka` is provided +6. ✅ Validates Manage version >= 9.2.0 when `--manage-kafka` is provided +7. ✅ Error message clear when Manage version < 9.2.0 +8. ✅ Error message clear when `--kafka-provider` missing +9. ✅ No state management needed - pure parameter validation + +**Code Quality:** +10. ✅ `self.enableKafkaImageProcessor` is the single control point for Image Processor functionality +11. ✅ `_requiresKafkaCivil()` function removed (redundant) +12. ✅ Installation summary displays Civil Image Processor status and Kafka binding +13. ✅ Generated command includes `--manage-kafka` parameter + +**Tekton Integration:** +14. ✅ Tekton pipeline receives `mas_appws_bindings_kafka_manage` parameter +15. ✅ Tekton task passes `mas_appws_bindings_kafka` to ansible-devops role +16. ✅ Pipeline uses default 10GB PVC size and `storage_class_rwx` automatically + +**Automated Tests:** +17. ✅ Interactive test: Manage 9.2 with Civil, decline Kafka Image Processor +18. ✅ Interactive test: Manage 9.2 with Civil, enable Kafka Image Processor +19. ✅ Non-interactive test: Manage 9.2 with Civil and `--manage-kafka system` +20. ✅ Validation error tests: Missing `--kafka-provider` and incompatible Manage version +21. ✅ All test cases pass successfully + +**Final Checks:** +20. ✅ All Python code passes `black` and `flake8` validation +21. ✅ Tekton definitions regenerate successfully +14. ✅ All Python code passes `black` and `flake8` validation \ No newline at end of file diff --git a/.bob/rules/general-guidelines.md b/.bob/rules/general-guidelines.md index d607e182ae6..d45d138995d 100644 --- a/.bob/rules/general-guidelines.md +++ b/.bob/rules/general-guidelines.md @@ -39,9 +39,15 @@ ## Windows Development with WSL Check `environment_details` for the operating system, if `operating system: windows` then wrap Linux commands with WSL: +**Important:** When using `wsl bash -lc`, do NOT use `cd` before the command. The WSL session automatically starts in the current workspace directory, so commands should be run directly: + ```bash -# `wsl` launches with the current directory already set to the project root -wsl bash -lc "ls -l" +# ✅ CORRECT - Run command directly +wsl bash -lc "black image/majel/majel-cli.py" + +# ❌ INCORRECT - Don't cd first +wsl bash -lc "cd image/majel && black majel-cli.py" +cd image/majel && wsl bash -lc "black majel-cli.py" ``` @@ -79,8 +85,11 @@ wsl bash -lc "ls -l" - Do not leave open questions in the plan, prompt the developer to make decisions and provide answers where necessary ### Tracking Progress -**Critical:** Update the plan document to reflect progress in real-time -- **For iterative tasks** (migrating multiple tests, processing multiple files): Update the checklist immediately after each successful iteration/validation +**Critical:** Track progress ONLY in the plan document, NOT in chat +- Do NOT use `update_todo_list` tool - it creates redundant tracking in chat +- Update the plan markdown file directly using `apply_diff` to mark completed items +- Mark completed items with `[x]` and add completion notes/timestamps if helpful +- **For iterative tasks**: Update checklist after each successful iteration/validation - **For multi-step phases**: Update after completing each major step within the phase -- **Before using `attempt_completion`**: Ensure the plan reflects all completed work -- The plan document should always show current progress, not just final completion +- **Before using `attempt_completion`**: Ensure plan reflects all completed work +- The plan document is the single source of truth for task progress diff --git a/python/src/mas/cli/install/app.py b/python/src/mas/cli/install/app.py index 8b098fab5ab..65513b84a16 100644 --- a/python/src/mas/cli/install/app.py +++ b/python/src/mas/cli/install/app.py @@ -45,7 +45,7 @@ StorageClassValidator, JsonValidator, OptimizerInstallPlanValidator, - BucketPrefixValidator + BucketPrefixValidator, ) from mas.devops.ocp import ( @@ -53,12 +53,9 @@ getStorageClasses, getClusterVersion, isClusterVersionInRange, - configureIngressForPathBasedRouting -) -from mas.devops.mas import ( - getCurrentCatalog, - getDefaultStorageClasses + configureIngressForPathBasedRouting, ) +from mas.devops.mas import getCurrentCatalog, getDefaultStorageClasses from mas.devops.utils import isVersionEqualOrAfter from mas.devops.sls import findSLSByNamespace from mas.devops.data import getCatalog, getCatalogEditorial, NoSuchCatalogError @@ -69,7 +66,7 @@ preparePipelinesNamespace, prepareInstallSecrets, testCLI, - launchInstallPipeline + launchInstallPipeline, ) from mas.devops.pre_install import applyPreInstallMASRBAC, permissionCheckForRBAC @@ -82,10 +79,18 @@ def wrapper(self, *args, **kwargs): result = func(self, *args, **kwargs) logger.debug(f"<<< InstallApp.{func.__name__}") return result + return wrapper -class InstallApp(BaseApp, InstallSettingsMixin, InstallSummarizerMixin, ConfigGeneratorMixin, installArgBuilderMixin): +class InstallApp( + BaseApp, + InstallSettingsMixin, + InstallSummarizerMixin, + ConfigGeneratorMixin, + installArgBuilderMixin, +): + enableKafkaImageProcessor: bool def getSelectedApps(self) -> list[str]: selectedApps = ["core"] @@ -115,13 +120,13 @@ def getSelectedApps(self) -> list[str]: def evaluatePreInstallRBACAccess(self) -> None: self.applyPreInstallMASRBAC = False - if not isVersionEqualOrAfter('9.2.0', self.getParam("mas_channel")): + if not isVersionEqualOrAfter("9.2.0", self.getParam("mas_channel")): return # TODO: Sort out the openshift-ingress exception properly. # For now, keep continue pre-install RBAC for minimal mode here. # if self.getParam("mas_permission_mode") == "minimal": - # return + # return if self.getParam("skip_preinstall_rbac") == "true": return @@ -134,25 +139,33 @@ def evaluatePreInstallRBACAccess(self) -> None: return if self.isInteractiveMode: - self.printDescription([ - "", - f"You selected the '{self.getParam('mas_permission_mode')}' permission mode.", - "The pre-install RBAC required for this permission mode has not been applied by your current cluster login.", - "This step must be completed by an OpenShift cluster administrator before MAS installation can continue.", - "Ask your OpenShift administrator to run 'mas pre-install' for this MAS instance, MAS version, permission mode, and selected apps.", - "If that has already been done, you can continue the installation without applying it again." - ]) - - if not self.yesOrNo("Has your OpenShift administrator already run 'mas pre-install' for this installation"): - self.fatalError("Installation aborted. Ask your OpenShift administrator to run 'mas pre-install' for this installation and then run mas install again with --skip-preinstall-rbac.") - else: - self.fatalError( - "\n".join([ + self.printDescription( + [ + "", f"You selected the '{self.getParam('mas_permission_mode')}' permission mode.", "The pre-install RBAC required for this permission mode has not been applied by your current cluster login.", "This step must be completed by an OpenShift cluster administrator before MAS installation can continue.", - "Ask your OpenShift administrator to run 'mas pre-install' for this installation and then rerun 'mas install' with --skip-preinstall-rbac." - ]) + "Ask your OpenShift administrator to run 'mas pre-install' for this MAS instance, MAS version, permission mode, and selected apps.", + "If that has already been done, you can continue the installation without applying it again.", + ] + ) + + if not self.yesOrNo( + "Has your OpenShift administrator already run 'mas pre-install' for this installation" + ): + self.fatalError( + "Installation aborted. Ask your OpenShift administrator to run 'mas pre-install' for this installation and then run mas install again with --skip-preinstall-rbac." + ) + else: + self.fatalError( + "\n".join( + [ + f"You selected the '{self.getParam('mas_permission_mode')}' permission mode.", + "The pre-install RBAC required for this permission mode has not been applied by your current cluster login.", + "This step must be completed by an OpenShift cluster administrator before MAS installation can continue.", + "Ask your OpenShift administrator to run 'mas pre-install' for this installation and then rerun 'mas install' with --skip-preinstall-rbac.", + ] + ) ) @logMethodCall @@ -160,11 +173,17 @@ def validateCatalogSource(self): # Check supported OCP versions - but we can only do this in non-development mode because in development mode # we do not load catalog metadata files if not self.devMode: - assert self.chosenCatalog is not None, "validateCatalogSource() called before catalog was chosen" + assert ( + self.chosenCatalog is not None + ), "validateCatalogSource() called before catalog was chosen" ocpVersion = getClusterVersion(self.dynamicClient) supportedReleases = self.chosenCatalog.get("ocp_compatibility", []) - if len(supportedReleases) > 0 and not isClusterVersionInRange(ocpVersion, supportedReleases): - self.fatalError(f"IBM Maximo Operator Catalog {self.getParam('mas_catalog_version')} is not compatible with OpenShift v{ocpVersion}. Compatible OpenShift releases are {supportedReleases}") + if len(supportedReleases) > 0 and not isClusterVersionInRange( + ocpVersion, supportedReleases + ): + self.fatalError( + f"IBM Maximo Operator Catalog {self.getParam('mas_catalog_version')} is not compatible with OpenShift v{ocpVersion}. Compatible OpenShift releases are {supportedReleases}" + ) # Compare with any existing installed catalog catalogsAPI = self.dynamicClient.resources.get(api_version="operators.coreos.com/v1alpha1", kind="CatalogSource") @@ -180,11 +199,17 @@ def validateCatalogSource(self): elif re.match(r".+v8-amd64", catalogDisplayName): catalogId = "v8-amd64" else: - self.fatalError(f"IBM Maximo Operator Catalog is already installed on this cluster. However, it is not possible to identify its version. If you wish to install a new MAS instance using the {self.getParam('mas_catalog_version')} catalog please first run 'mas update' to switch to this catalog, this will ensure the appropriate actions are performed as part of the catalog update") - assert False, "fatalError() should have exited" # Let basepyright know that fatalError() will exit + self.fatalError( + f"IBM Maximo Operator Catalog is already installed on this cluster. However, it is not possible to identify its version. If you wish to install a new MAS instance using the {self.getParam('mas_catalog_version')} catalog please first run 'mas update' to switch to this catalog, this will ensure the appropriate actions are performed as part of the catalog update" + ) + assert ( + False + ), "fatalError() should have exited" # Let basepyright know that fatalError() will exit if catalogId != self.getParam("mas_catalog_version"): - self.fatalError(f"IBM Maximo Operator Catalog {catalogId} is already installed on this cluster, if you wish to install a new MAS instance using the {self.getParam('mas_catalog_version')} catalog please first run 'mas update' to switch to this catalog, this will ensure the appropriate actions are performed as part of the catalog update") + self.fatalError( + f"IBM Maximo Operator Catalog {catalogId} is already installed on this cluster, if you wish to install a new MAS instance using the {self.getParam('mas_catalog_version')} catalog please first run 'mas update' to switch to this catalog, this will ensure the appropriate actions are performed as part of the catalog update" + ) except NotFoundError: # There's no existing catalog installed pass @@ -202,23 +227,29 @@ def validateInternalRegistryAvailable(self): serviceAPI.get(name="image-registry", namespace="openshift-image-registry") except NotFoundError: self.fatalError( - "\n".join([ - "Unable to proceed with installation of Maximo Manage. Could not detect the required \"image-registry\" service in the openshift-image-registry namespace", - "For more information refer to https://www.ibm.com/docs/en/masv-and-l/continuous-delivery?topic=installing-enabling-openshift-internal-image-registry" - ]) + "\n".join( + [ + 'Unable to proceed with installation of Maximo Manage. Could not detect the required "image-registry" service in the openshift-image-registry namespace', + "For more information refer to https://www.ibm.com/docs/en/masv-and-l/continuous-delivery?topic=installing-enabling-openshift-internal-image-registry", + ] + ) ) @logMethodCall def licensePrompt(self): if not self.licenseAccepted: self.printH1("License Terms") - self.printDescription([ - "To continue with the installation, you must accept the license terms:", - self.licenses[self.getParam('mas_channel')] - ]) + self.printDescription( + [ + "To continue with the installation, you must accept the license terms:", + self.licenses[self.getParam("mas_channel")], + ] + ) if self.noConfirm: - self.fatalError("You must accept the license terms with --accept-license when using the --no-confirm flag") + self.fatalError( + "You must accept the license terms with --accept-license when using the --no-confirm flag" + ) else: if not self.yesOrNo("Do you accept the license terms"): exit(1) @@ -258,10 +289,12 @@ def formatCatalog(self, name: str) -> str: @logMethodCall def processCatalogChoice(self) -> list: - assert self.chosenCatalog is not None, "processCatalogChoice() called before catalog was chosen" + assert (self.chosenCatalog is not None), "processCatalogChoice() called before catalog was chosen" self.catalogDigest = self.chosenCatalog["catalog_digest"] self.catalogMongoDbVersion = self.chosenCatalog["mongo_extras_version_default"] - self.catalogDb2Channel = self.chosenCatalog.get("db2_channel_default", "v110509.0") # Returns fallback "v110509.0" for old catalogs without this field + self.catalogDb2Channel = self.chosenCatalog.get( + "db2_channel_default", "v110509.0" + ) # Returns fallback "v110509.0" for old catalogs without this field if self.architecture != "s390x" and self.architecture != "ppc64le": self.catalogCp4dVersion = self.chosenCatalog["cpd_product_version_default"] @@ -290,17 +323,28 @@ def processCatalogChoice(self) -> list: # based on mas core for channel in self.chosenCatalog["mas_core_version"]: # {"9.1-feature": "9.1.x-feature"} - self.catalogReleases.update({channel.replace('.x', ''): channel}) + self.catalogReleases.update({channel.replace(".x", ""): channel}) # Generate catalogTable for application, key in applications.items(): # Add 9.1-feature channel based off 9.0 to those apps that have not onboarded yet if key in self.chosenCatalog: tempChosenCatalog = self.chosenCatalog[key].copy() - if '9.1.x-feature' not in tempChosenCatalog and '9.0.x' in tempChosenCatalog: + if ( + "9.1.x-feature" not in tempChosenCatalog and + "9.0.x" in tempChosenCatalog + ): tempChosenCatalog.update({"9.1.x-feature": tempChosenCatalog["9.0.x"]}) - self.catalogTable.append({"": application} | {key.replace(".x", ""): value for key, value in sorted(tempChosenCatalog.items(), reverse=True)}) + self.catalogTable.append( + {"": application} | + { + key.replace(".x", ""): value + for key, value in sorted( + tempChosenCatalog.items(), reverse=True + ) + } + ) if self.architecture == "s390x" or self.architecture == "ppc64le": summary = [ @@ -323,28 +367,28 @@ def processCatalogChoice(self) -> list: ] # Add editorial content (What's New and Known Issues) - editorial = getCatalogEditorial(self.getParam('mas_catalog_version')) + editorial = getCatalogEditorial(self.getParam("mas_catalog_version")) if editorial: # Add What's New section - if 'whats_new' in editorial and editorial['whats_new']: + if "whats_new" in editorial and editorial["whats_new"]: summary.append("") summary.append("What's New") - for item in editorial['whats_new']: + for item in editorial["whats_new"]: # Replace **text** with text in title - title = item.get('title', '') - title = title.replace('**', '', 1).replace('**', '', 1) + title = item.get("title", "") + title = title.replace("**", "", 1).replace("**", "", 1) summary.append(title) # Add details if present - if 'details' in item and item['details']: - for detail in item['details']: + if "details" in item and item["details"]: + for detail in item["details"]: summary.append(f" - {detail}") # Add Known Issues section - if 'known_issues' in editorial and editorial['known_issues']: + if "known_issues" in editorial and editorial["known_issues"]: summary.append("") summary.append("Known Issues") - for issue in editorial['known_issues']: - title = issue.get('title', '') + for issue in editorial["known_issues"]: + title = issue.get("title", "") summary.append(f"- {title}") return summary @@ -359,12 +403,14 @@ def configCatalog(self): catalogInfo = getCurrentCatalog(self.dynamicClient) if catalogInfo is None: - self.printDescription([ - "The catalog you choose dictates the version of everything that is installed, with Maximo Application Suite this is the only version you need to remember; all other versions are determined by this choice.", - "Older catalogs can still be used, but we recommend using an older version of the CLI that aligns with the release date of the catalog.", - " - Learn more: https://ibm-mas.github.io/cli/catalogs/", - "" - ]) + self.printDescription( + [ + "The catalog you choose dictates the version of everything that is installed, with Maximo Application Suite this is the only version you need to remember; all other versions are determined by this choice.", + "Older catalogs can still be used, but we recommend using an older version of the CLI that aligns with the release date of the catalog.", + " - Learn more: https://ibm-mas.github.io/cli/catalogs/", + "", + ] + ) print("Supported Catalogs:") for catalog in self.catalogOptions: catalogString = self.formatCatalog(catalog) @@ -375,21 +421,25 @@ def configCatalog(self): catalogSelection = self.promptForString("Select catalog", completer=catalogCompleter) self.setParam("mas_catalog_version", catalogSelection) else: - self.printDescription([ - f"The IBM Maximo Operator Catalog is already installed in this cluster ({catalogInfo['catalogId']}). If you wish to install MAS using a newer version of the catalog please first update the catalog using mas update." - ]) + self.printDescription( + [ + f"The IBM Maximo Operator Catalog is already installed in this cluster ({catalogInfo['catalogId']}). If you wish to install MAS using a newer version of the catalog please first update the catalog using mas update." + ] + ) self.setParam("mas_catalog_version", catalogInfo["catalogId"]) self.chosenCatalog = getCatalog(self.getParam("mas_catalog_version")) catalogSummary = self.processCatalogChoice() self.printDescription(catalogSummary) - self.printDescription([ - "", - "Two types of release are available:", - " - GA releases of Maximo Application Suite are supported under IBM's standard 3+1+3 support lifecycle policy.", - " - 'Feature' releases allow early access to new features for evaluation in non-production environments and are only supported through to the next GA release.", - "" - ]) + self.printDescription( + [ + "", + "Two types of release are available:", + " - GA releases of Maximo Application Suite are supported under IBM's standard 3+1+3 support lifecycle policy.", + " - 'Feature' releases allow early access to new features for evaluation in non-production environments and are only supported through to the next GA release.", + "", + ] + ) print(tabulate(self.catalogTable, headers="keys", tablefmt="simple_grid")) @@ -425,9 +475,13 @@ def configSLS(self) -> None: self.fatalError(f"Invalid selection: {self.slsMode}") if not (self.slsMode == 2 and not self.getParam("sls_namespace")): - sls_namespace = "ibm-sls" if self.slsMode == 1 else self.getParam("sls_namespace") + sls_namespace = ("ibm-sls" if self.slsMode == 1 else self.getParam("sls_namespace")) if findSLSByNamespace(sls_namespace, dynClient=self.dynamicClient): - print_formatted_text(HTML(f"SLS auto-detected: {sls_namespace}")) + print_formatted_text( + HTML( + f"SLS auto-detected: {sls_namespace}" + ) + ) print() if not self.yesOrNo("Upload/Replace the license file"): self.setParam("sls_action", "gencfg") @@ -482,7 +536,9 @@ def configGrafana(self) -> None: if self.isInteractiveMode and self.showAdvancedOptions: self.printH1("Configure Grafana") if self.getParam("grafana_action") == "none": - print_formatted_text("The Grafana operator package is not available in any catalogs on the target cluster, the installation of Grafana will be disabled") + print_formatted_text( + "The Grafana operator package is not available in any catalogs on the target cluster, the installation of Grafana will be disabled" + ) else: self.promptForString("Install namespace", "grafana_v5_namespace", default="grafana5") self.promptForString("Grafana storage size", "grafana_instance_storage_size", default="10Gi") @@ -510,13 +566,15 @@ def arcgisSettings(self) -> None: # Only prompt if ArcGIS is needed and we're on MAS 9.x # Check the appropriate channel based on what's being installed if needsArcGIS: - channel = self.getParam("mas_app_channel_manage") or self.getParam("mas_app_channel_facilities") - if channel and isVersionEqualOrAfter('9.0.0', channel): + channel = self.getParam("mas_app_channel_manage") or self.getParam( + "mas_app_channel_facilities" + ) + if channel and isVersionEqualOrAfter("9.0.0", channel): # Build description based on what's being installed description = [ "", "Geospatial capabilities require a map server provider", - "You may choose your preferred map provider later or you can enable IBM Maximo Location Services for Esri now" + "You may choose your preferred map provider later or you can enable IBM Maximo Location Services for Esri now", ] # Add specific details based on installed applications @@ -546,14 +604,21 @@ def arcgisSettings(self) -> None: def configSpecialCharacters(self): if self.showAdvancedOptions: self.printH1("Configure special characters for userID and username") - self.printDescription([ - "By default Maximo Application Suite will not allow special characters in usernames and userIDs, and this is the recommended setting. However, legacy Maximo products allowed this, so for maximum compatibilty when migrating from EAM 7 you can choose to enable this support." - ]) - self.yesOrNo("Allow special characters for user IDs and usernames", "mas_special_characters") + self.printDescription( + [ + "By default Maximo Application Suite will not allow special characters in usernames and userIDs, and this is the recommended setting. However, legacy Maximo products allowed this, so for maximum compatibilty when migrating from EAM 7 you can choose to enable this support." + ] + ) + self.yesOrNo( + "Allow special characters for user IDs and usernames", + "mas_special_characters", + ) @logMethodCall def configReportAdoptionMetricsFlag(self): - if self.showAdvancedOptions and isVersionEqualOrAfter('9.1.0', self.getParam("mas_channel")): + if self.showAdvancedOptions and isVersionEqualOrAfter( + "9.1.0", self.getParam("mas_channel") + ): self.printH1("Adoption Metrics Reporting") self.printDescription([ "Adoption Metrics are used by IBM to measure feature adoption, user engagement, and the success of product initiatives.", @@ -580,7 +645,7 @@ def configCP4D(self): if self.getParam("mas_catalog_version") in self.catalogOptions: # Note: this will override any version provided by the user (which is intentional!) logger.debug(f"Using automatic CP4D product version: {self.getParam('cpd_product_version')}") - assert self.chosenCatalog is not None, "chosenCatalog should be set in this scenario but was not" + assert (self.chosenCatalog is not None), "chosenCatalog should be set in this scenario but was not" self.setParam("cpd_product_version", self.chosenCatalog["cpd_product_version_default"]) elif self.getParam("cpd_product_version") == "": if self.noConfirm: @@ -653,10 +718,16 @@ def configMAS(self): "Workspace display name restrictions:", " - Must be 3-300 characters long" ]) - self.promptForString("Workspace name", "mas_workspace_name", validator=WorkspaceNameFormatValidator()) + self.promptForString( + "Workspace name", + "mas_workspace_name", + validator=WorkspaceNameFormatValidator(), + ) if self.slsMode == 2 and not self.getParam("sls_namespace"): - self.setParam("sls_namespace", f"mas-{self.getParam('mas_instance_id')}-sls") + self.setParam( + "sls_namespace", f"mas-{self.getParam('mas_instance_id')}-sls" + ) self.configOperationMode() self.configCATrust() @@ -690,7 +761,9 @@ def configOperationMode(self): " 1. Production", " 2. Non-Production" ]) - self.operationalMode = self.promptForInt("Operational Mode", default=1, min=1, max=2) + self.operationalMode = self.promptForInt( + "Operational Mode", default=1, min=1, max=2 + ) if self.operationalMode == 1: self.setParam("environment_type", "production") self.setParam("aiservice_odh_model_deployment_type", "raw") @@ -704,7 +777,7 @@ def configOperationMode(self): @logMethodCall def configPermissionMode(self): - if isVersionEqualOrAfter('9.2.0', self.getParam("mas_channel")): + if isVersionEqualOrAfter("9.2.0", self.getParam("mas_channel")): if self.showAdvancedOptions: self.printH1("Configure Permission Mode") self.printDescription([ @@ -727,9 +800,13 @@ def configPermissionMode(self): " - DNS integration is not available in this mode. If you use a custom domain, you need to configure DNS manually." ]) - permissionModeInt = self.promptForInt("Permission Mode", default=1, min=1, max=3) + permissionModeInt = self.promptForInt( + "Permission Mode", default=1, min=1, max=3 + ) permissionModeMap = {1: "cluster", 2: "namespaced", 3: "minimal"} - self.setParam("mas_permission_mode", permissionModeMap[permissionModeInt]) + self.setParam( + "mas_permission_mode", permissionModeMap[permissionModeInt] + ) if self.getParam("mas_permission_mode") in ["namespaced", "minimal"]: self.setParam("mas_issuer_kind", "Issuer") @@ -742,7 +819,7 @@ def configPermissionMode(self): " - You can not get CLI-managed DNS integration", "", " 2. ClusterIssuer", - " - MAS uses a cluster-scoped clusterissuer resource for certificates" + " - MAS uses a cluster-scoped clusterissuer resource for certificates", ]) issuerKindChoice = self.promptForInt("Certificate issuer kind", min=1, max=2, default=2) self.setParam("mas_issuer_kind", "ClusterIssuer" if issuerKindChoice == 2 else "Issuer") @@ -751,14 +828,14 @@ def configPermissionMode(self): self.setParam("mas_issuer_kind", "ClusterIssuer") def _handleDNSIntegrationRestriction(self): - if not isVersionEqualOrAfter('9.2.0', self.getParam("mas_channel")): + if not isVersionEqualOrAfter("9.2.0", self.getParam("mas_channel")): return False if self.getParam("mas_permission_mode") in ["namespaced", "minimal"]: self.printDescription([ f"You are using the {self.getParam('mas_permission_mode')} permission mode.", "DNS integration is not available in this mode.", - "If you use a custom domain, you need to configure DNS manually." + "If you use a custom domain, you need to configure DNS manually.", ]) return True @@ -766,7 +843,7 @@ def _handleDNSIntegrationRestriction(self): self.printDescription([ "You selected Issuer as the certificate issuer kind.", "DNS integration is not available when the certificate issuer kind is Issuer.", - "If you use a custom domain, you need to configure DNS manually." + "If you use a custom domain, you need to configure DNS manually.", ]) return True @@ -777,13 +854,12 @@ def _getMasDomainForDisplay(self): if not masDomain: try: ingressAPI = self.dynamicClient.resources.get( - api_version="config.openshift.io/v1", - kind="Ingress" + api_version="config.openshift.io/v1", kind="Ingress" ) ingressConfig = ingressAPI.get(name="cluster") - masDomain = ingressConfig.spec.get('domain', 'yourdomain.com') + masDomain = ingressConfig.spec.get("domain", "yourdomain.com") except Exception: - masDomain = 'yourdomain.com' + masDomain = "yourdomain.com" masInstanceId = self.getParam("mas_instance_id") if masInstanceId: @@ -800,17 +876,17 @@ def _promptForIngressController(self): ingressControllers = ingressControllerAPI.get(namespace="openshift-ingress-operator") availableControllers = [] for ic in ingressControllers.items: - if hasattr(ic, 'status') and hasattr(ic.status, 'conditions'): + if hasattr(ic, "status") and hasattr(ic.status, "conditions"): for condition in ic.status.conditions: - if condition.type == 'Available' and condition.status == 'True': + if condition.type == "Available" and condition.status == "True": availableControllers.append({ - 'name': ic.metadata.name, - 'domain': ic.status.domain if hasattr(ic.status, 'domain') else 'N/A' + "name": ic.metadata.name, + "domain": ic.status.domain if hasattr(ic.status, "domain") else "N/A" }) break if len(availableControllers) == 1: - selectedController = availableControllers[0]['name'] + selectedController = availableControllers[0]["name"] logger.info(f"Only one IngressController available, using: {selectedController}") return selectedController @@ -826,21 +902,27 @@ def _promptForIngressController(self): print(f" {idx}. {ic['name']} (domain: {ic['domain']})") print("") - selection = self.promptForInt("IngressController", min=1, max=len(availableControllers)) - selectedController = availableControllers[selection - 1]['name'] + selection = self.promptForInt( + "IngressController", min=1, max=len(availableControllers) + ) + selectedController = availableControllers[selection - 1]["name"] logger.info(f"User selected IngressController: {selectedController}") return selectedController logger.warning("No available IngressControllers found, defaulting to 'default'") - return 'default' + return "default" except Exception as e: logger.warning(f"Failed to list IngressControllers: {e}") - return 'default' + return "default" @logMethodCall def configRoutingMode(self): - if self.showAdvancedOptions and isVersionEqualOrAfter('9.2.0', self.getParam("mas_channel")) and self.getParam("mas_channel") != '9.2.x-feature': + if ( + self.showAdvancedOptions and + isVersionEqualOrAfter("9.2.0", self.getParam("mas_channel")) and + self.getParam("mas_channel") != "9.2.x-feature" + ): self.printH1("Configure Routing Mode") masDomain = self._getMasDomainForDisplay() @@ -859,7 +941,7 @@ def configRoutingMode(self): "different namespaces, which is necessary for path-based routing to function correctly.", "", "For more information refer to:", - "https://docs.redhat.com/en/documentation/openshift_container_platform/4.20/html/ingress_and_load_balancing/routes#nw-route-admission-policy_configuring-routes" + "https://docs.redhat.com/en/documentation/openshift_container_platform/4.20/html/ingress_and_load_balancing/routes#nw-route-admission-policy_configuring-routes", ]) routingModeInt = self.promptForInt("Routing Mode", default=1, min=1, max=2) @@ -889,11 +971,15 @@ def configRoutingMode(self): self.setParam("mas_ingress_controller_name", selectedController) # Check if selected IngressController is configured for path-based routing - _, isConfigured = self._checkIngressControllerForPathRouting(selectedController) + _, isConfigured = self._checkIngressControllerForPathRouting( + selectedController + ) if isConfigured: self.setParam("mas_routing_mode", "path") - self.printDescription([f"IngressController '{selectedController}' is configured for path-based routing."]) + self.printDescription([ + f"IngressController '{selectedController}' is configured for path-based routing." + ]) else: self.printDescription([ "", @@ -910,7 +996,9 @@ def configRoutingMode(self): if self.yesOrNo("Configure ingress namespace ownership policy to enable path-based routing for MAS"): self.setParam("mas_routing_mode", "path") self.setParam("mas_configure_ingress", "true") - self.printDescription([f"IngressController '{selectedController}' will be configured before MAS installation begins."]) + self.printDescription([ + f"IngressController '{selectedController}' will be configured before MAS installation begins." + ]) else: self.printDescription([ "", @@ -922,7 +1010,7 @@ def configRoutingMode(self): else: self.setParam("mas_routing_mode", "subdomain") - def _checkIngressControllerForPathRouting(self, controllerName='default'): + def _checkIngressControllerForPathRouting(self, controllerName="default"): """Check if a specific IngressController exists and is configured for path-based routing. Returns: @@ -932,8 +1020,7 @@ def _checkIngressControllerForPathRouting(self, controllerName='default'): """ try: ingressControllerAPI = self.dynamicClient.resources.get( - api_version="operator.openshift.io/v1", - kind="IngressController" + api_version="operator.openshift.io/v1", kind="IngressController" ) ingressController = ingressControllerAPI.get( @@ -941,11 +1028,11 @@ def _checkIngressControllerForPathRouting(self, controllerName='default'): namespace="openshift-ingress-operator" ) - spec = ingressController.get('spec', {}) - routeAdmission = spec.get('routeAdmission', {}) - namespaceOwnership = routeAdmission.get('namespaceOwnership', '') + spec = ingressController.get("spec", {}) + routeAdmission = spec.get("routeAdmission", {}) + namespaceOwnership = routeAdmission.get("namespaceOwnership", "") - if namespaceOwnership == 'InterNamespaceAllowed': + if namespaceOwnership == "InterNamespaceAllowed": logger.info(f"IngressController '{controllerName}' is configured for path-based routing") return (True, True) @@ -958,7 +1045,7 @@ def _checkIngressControllerForPathRouting(self, controllerName='default'): logger.warning(f"Failed to check IngressController '{controllerName}' configuration: {e}") return (False, False) - def _checkIngressControllerPermissions(self, controllerName='default'): + def _checkIngressControllerPermissions(self, controllerName="default"): try: ingressControllerAPI = self.dynamicClient.resources.get( api_version="operator.openshift.io/v1", @@ -992,7 +1079,9 @@ def configServiceMesh(self) -> None: @logMethodCall def configAnnotations(self): if self.operationalMode == 2: - self.setParam("mas_annotations", "mas.ibm.com/operationalMode=nonproduction") + self.setParam( + "mas_annotations", "mas.ibm.com/operationalMode=nonproduction" + ) @logMethodCall def configSNO(self): @@ -1005,17 +1094,25 @@ def configSNO(self): def configDNSAndCerts(self): if self.showAdvancedOptions: self.printH1("Cluster Ingress Secret Override") - self.printDescription([ - "In most OpenShift clusters the installation is able to automatically locate the default ingress certificate, however in some configurations it is necessary to manually configure the name of the secret", - "Unless you see an error during the ocp-verify stage indicating that the secret can not be determined you do not need to set this and can leave the response empty" - ]) - self.promptForString("Cluster ingress certificate secret name", "ocp_ingress_tls_secret_name", default="") + self.printDescription( + [ + "In most OpenShift clusters the installation is able to automatically locate the default ingress certificate, however in some configurations it is necessary to manually configure the name of the secret", + "Unless you see an error during the ocp-verify stage indicating that the secret can not be determined you do not need to set this and can leave the response empty", + ] + ) + self.promptForString( + "Cluster ingress certificate secret name", + "ocp_ingress_tls_secret_name", + default="", + ) self.printH1("Configure Domain & Certificate Management") - configureDomainAndCertMgmt = self.yesOrNo('Configure domain & certificate management') + configureDomainAndCertMgmt = self.yesOrNo( + "Configure domain & certificate management" + ) if configureDomainAndCertMgmt: dnsIntegrationRestricted = self._handleDNSIntegrationRestriction() - configureDomain = self.yesOrNo('Configure custom domain') + configureDomain = self.yesOrNo("Configure custom domain") if configureDomain: self.promptForString("MAS top-level domain", "mas_domain") @@ -1023,14 +1120,16 @@ def configDNSAndCerts(self): self.setParam("dns_provider", "") self.setParam("mas_cluster_issuer", "") else: - self.printDescription([ - "", - "DNS Integrations:", - " 1. Cloudflare", - " 2. IBM Cloud Internet Services", - " 3. AWS Route 53", - " 4. None (I will set up DNS myself)" - ]) + self.printDescription( + [ + "", + "DNS Integrations:", + " 1. Cloudflare", + " 2. IBM Cloud Internet Services", + " 3. AWS Route 53", + " 4. None (I will set up DNS myself)", + ] + ) dnsProvider = self.promptForInt("DNS Provider", min=1, max=4) @@ -1046,11 +1145,15 @@ def configDNSAndCerts(self): self.setParam("mas_cluster_issuer", "") if dnsProvider in [1, 2]: - self.printDescription([ - "By default, DNS CNAME records will be created pointing to the domain of the cluster ingress (ingress.config.openshift.io/cluster).", - "CloudFlare and CIS DNS integrations support the ability to provide an alternative domain, which may be necessary if you are using OpenShift Container Platform in a non-standard networking configuration." - ]) - self.promptForString("Cluster Ingress Domain Override", "ocp_ingress") + self.printDescription( + [ + "By default, DNS CNAME records will be created pointing to the domain of the cluster ingress (ingress.config.openshift.io/cluster).", + "CloudFlare and CIS DNS integrations support the ability to provide an alternative domain, which may be necessary if you are using OpenShift Container Platform in a non-standard networking configuration.", + ] + ) + self.promptForString( + "Cluster Ingress Domain Override", "ocp_ingress" + ) else: # Use MAS default self-signed cluster issuer with the default domain @@ -1060,7 +1163,10 @@ def configDNSAndCerts(self): self.manualCerts = self.yesOrNo("Configure manual certificates") self.setParam("mas_manual_cert_mgmt", str(self.manualCerts).lower()) if self.getParam("mas_manual_cert_mgmt").lower() == "true": - self.manualCertsDir = self.promptForDir("Enter the path containing the manual certificates", mustExist=True) + self.manualCertsDir = self.promptForDir( + "Enter the path containing the manual certificates", + mustExist=True, + ) else: self.manualCertsDir = None @@ -1073,17 +1179,19 @@ def configDNSAndCertsCloudflare(self): self.promptForString("Cloudflare zone", "cloudflare_zone") self.promptForString("Cloudflare subdomain", "cloudflare_subdomain") - self.printDescription([ - "Certificate Issuer:", - " 1. LetsEncrypt (Production)", - " 2. LetsEncrypt (Staging)", - " 3. Self-Signed" - ]) + self.printDescription( + [ + "Certificate Issuer:", + " 1. LetsEncrypt (Production)", + " 2. LetsEncrypt (Staging)", + " 3. Self-Signed", + ] + ) certIssuer = self.promptForInt("Certificate issuer", min=1, max=3) certIssuerOptions = [ f"{self.getParam('mas_instance_id')}-cloudflare-le-prod", f"{self.getParam('mas_instance_id')}-cloudflare-le-stg", - "" + "", ] self.setParam("mas_cluster_issuer", certIssuerOptions[certIssuer - 1]) @@ -1095,44 +1203,58 @@ def configDNSAndCertsCIS(self): self.promptForString("CIS CRN", "cis_crn") self.promptForString("CIS subdomain", "cis_subdomain") - self.printDescription([ - "Certificate Issuer:", - " 1. LetsEncrypt (Production)", - " 2. LetsEncrypt (Staging)", - " 3. Self-Signed" - ]) + self.printDescription( + [ + "Certificate Issuer:", + " 1. LetsEncrypt (Production)", + " 2. LetsEncrypt (Staging)", + " 3. Self-Signed", + ] + ) certIssuer = self.promptForInt("Certificate issuer", min=1, max=3) certIssuerOptions = [ f"{self.getParam('mas_instance_id')}-cis-le-prod", f"{self.getParam('mas_instance_id')}-cis-le-stg", - "" + "", ] self.setParam("mas_cluster_issuer", certIssuerOptions[certIssuer - 1]) @logMethodCall def configDNSAndCertsRoute53(self): self.setParam("dns_provider", "route53") - self.printDescription([ - "Provide your AWS account access key ID and secret access key", - "This will be used to authenticate into the AWS account where your AWS Route 53 hosted zone instance is located", - "" - ]) + self.printDescription( + [ + "Provide your AWS account access key ID and secret access key", + "This will be used to authenticate into the AWS account where your AWS Route 53 hosted zone instance is located", + "", + ] + ) self.promptForString("AWS Access Key ID", "aws_access_key_id", isPassword=True) - self.promptForString("AWS Secret Access Key", "aws_secret_access_key", isPassword=True) + self.promptForString( + "AWS Secret Access Key", "aws_secret_access_key", isPassword=True + ) - self.printDescription([ - "Provide your AWS Route 53 hosted zone instance details", - "This information will be used to create webhook resources between your cluster and your AWS Route 53 instance (cluster issuer and cname records)", - "in order for it to be able to resolve DNS entries for all the subdomains created for your Maximo Application Suite instance", - "", - "Therefore, the AWS Route 53 subdomain + the AWS Route 53 hosted zone name defined, when combined, needs to match with the chosen MAS Top Level domain, otherwise the DNS records won't be able to get resolved" - ]) - self.promptForString("AWS Route 53 hosted zone name", "route53_hosted_zone_name") - self.promptForString("AWS Route 53 hosted zone region", "route53_hosted_zone_region") + self.printDescription( + [ + "Provide your AWS Route 53 hosted zone instance details", + "This information will be used to create webhook resources between your cluster and your AWS Route 53 instance (cluster issuer and cname records)", + "in order for it to be able to resolve DNS entries for all the subdomains created for your Maximo Application Suite instance", + "", + "Therefore, the AWS Route 53 subdomain + the AWS Route 53 hosted zone name defined, when combined, needs to match with the chosen MAS Top Level domain, otherwise the DNS records won't be able to get resolved", + ] + ) + self.promptForString( + "AWS Route 53 hosted zone name", "route53_hosted_zone_name" + ) + self.promptForString( + "AWS Route 53 hosted zone region", "route53_hosted_zone_region" + ) self.promptForString("AWS Route 53 subdomain", "route53_subdomain") self.promptForString("AWS Route 53 e-mail", "route53_email") - self.setParam("mas_cluster_issuer", f"{self.getParam('mas_instance_id')}-route53-le-prod") + self.setParam( + "mas_cluster_issuer", f"{self.getParam('mas_instance_id')}-route53-le-prod" + ) @logMethodCall def configApps(self): @@ -1155,40 +1277,50 @@ def configApps(self): # Validate version compatibility between IoT and Monitor monitorChannel = self.getParam("mas_app_channel_monitor") if iotChannel and monitorChannel: - iotIs920OrLater = isVersionEqualOrAfter('9.2.0', iotChannel) - monitorIs920OrLater = isVersionEqualOrAfter('9.2.0', monitorChannel) + iotIs920OrLater = isVersionEqualOrAfter("9.2.0", iotChannel) + monitorIs920OrLater = isVersionEqualOrAfter("9.2.0", monitorChannel) # IoT >= 9.2.0 requires Monitor >= 9.2.0 if iotIs920OrLater and not monitorIs920OrLater: - self.printDescription([ - "", - "Error: IoT version 9.2.0 or later requires Monitor version 9.2.0 or later.", - f"IoT channel: {iotChannel}, Monitor channel: {monitorChannel}", - "Please select compatible versions for both applications.", - "" - ]) - self.fatalError("Incompatible IoT and Monitor versions selected") + self.printDescription( + [ + "", + "Error: IoT version 9.2.0 or later requires Monitor version 9.2.0 or later.", + f"IoT channel: {iotChannel}, Monitor channel: {monitorChannel}", + "Please select compatible versions for both applications.", + "", + ] + ) + self.fatalError( + "Incompatible IoT and Monitor versions selected" + ) # IoT < 9.2.0 requires Monitor < 9.2.0 if not iotIs920OrLater and monitorIs920OrLater: - self.printDescription([ - "", - "Error: IoT version earlier than 9.2.0 requires Monitor version earlier than 9.2.0.", - f"IoT channel: {iotChannel}, Monitor channel: {monitorChannel}", - "Please select compatible versions for both applications.", - "" - ]) - self.fatalError("Incompatible IoT and Monitor versions selected") + self.printDescription( + [ + "", + "Error: IoT version earlier than 9.2.0 requires Monitor version earlier than 9.2.0.", + f"IoT channel: {iotChannel}, Monitor channel: {monitorChannel}", + "Please select compatible versions for both applications.", + "", + ] + ) + self.fatalError( + "Incompatible IoT and Monitor versions selected" + ) else: # User declined Monitor installation # Validate: IoT >= 9.2.0 requires Monitor - if iotChannel and isVersionEqualOrAfter('9.2.0', iotChannel): - self.printDescription([ - "", - "Error: IoT version 9.2.0 or later requires Monitor to be installed.", - "Please install Monitor to proceed with IoT 9.2.0+ installation.", - "" - ]) + if iotChannel and isVersionEqualOrAfter("9.2.0", iotChannel): + self.printDescription( + [ + "", + "Error: IoT version 9.2.0 or later requires Monitor to be installed.", + "Please install Monitor to proceed with IoT 9.2.0+ installation.", + "", + ] + ) self.fatalError("IoT 9.2.0+ requires Monitor to be installed") # For IoT < 9.2.0, Monitor is optional else: @@ -1198,13 +1330,17 @@ def configApps(self): self.configAppChannel("monitor") monitorChannel = self.getParam("mas_app_channel_monitor") # For Monitor < 9.2.0, Monitor requires IoT - if monitorChannel and not isVersionEqualOrAfter('9.2.0', monitorChannel): - self.printDescription([ - "", - "Error: Monitor version earlier than 9.2.0 requires IoT to be installed.", - "Please install IoT first, or choose Monitor 9.2.0+ for standalone installation.", - "" - ]) + if monitorChannel and not isVersionEqualOrAfter( + "9.2.0", monitorChannel + ): + self.printDescription( + [ + "", + "Error: Monitor version earlier than 9.2.0 requires IoT to be installed.", + "Please install IoT first, or choose Monitor 9.2.0+ for standalone installation.", + "", + ] + ) self.installMonitor = False # Initialize ArcGIS flag (will be set to True later in arcgisSettings() if needed) @@ -1217,12 +1353,18 @@ def configApps(self): # If the selection was to not install manage but we are in mas_channel 9.1 or later, we need to set self.isManageFoundation to True # Also, we need to force self.installManage to be True because Manage must always be installed in MAS 9.1 or later if not self.installManage: - if not self.getParam("mas_channel").startswith("8.") and not self.getParam("mas_channel").startswith("9.0"): + if not self.getParam("mas_channel").startswith("8.") and not self.getParam( + "mas_channel" + ).startswith("9.0"): self.installManage = True self.isManageFoundation = True self.setParam("mas_app_settings_aio_flag", "false") self.manageAppName = "Manage foundation" - self.printDescription([f"{self.manageAppName} installs the following capabilities: User, Security groups, Application configurator and Mobile configurator."]) + self.printDescription( + [ + f"{self.manageAppName} installs the following capabilities: User, Security groups, Application configurator and Mobile configurator." + ] + ) if self.installManage: self.configAppChannel("manage") @@ -1237,7 +1379,7 @@ def configApps(self): self.configAppChannel("predict") # Assist is only installable on MAS 9.0.x due to withdrawal of support for Watson Discovery in our managed dependency stack and the inability of Assist 8.x to support this - if isVersionEqualOrAfter('9.0.0', self.getParam("mas_channel")): + if isVersionEqualOrAfter("9.0.0", self.getParam("mas_channel")): self.installAssist = self.yesOrNo("Install Assist") if self.installAssist: self.configAppChannel("assist") @@ -1252,7 +1394,10 @@ def configApps(self): if self.installInspection: self.configAppChannel("visualinspection") - if isVersionEqualOrAfter('9.1.0', self.getParam("mas_channel")) and self.getParam("mas_channel") != '9.1.x-feature': + if ( + isVersionEqualOrAfter("9.1.0", self.getParam("mas_channel")) and + self.getParam("mas_channel") != "9.1.x-feature" + ): self.installFacilities = self.yesOrNo("Install Real Estate and Facilities") if self.installFacilities: self.configAppChannel("facilities") @@ -1260,7 +1405,7 @@ def configApps(self): self.installFacilities = False # AI Service is only installable on Manage 9.x as AI Config Application is not supported on Manage 8.x - if isVersionEqualOrAfter('9.0.0', self.getParam("mas_app_channel_manage")): + if isVersionEqualOrAfter("9.0.0", self.getParam("mas_app_channel_manage")): self.installAIService = self.yesOrNo("Install AI Service") if self.installAIService: self.configAIService() @@ -1271,44 +1416,83 @@ def configApps(self): def configAppChannel(self, appId): versions = self.getCompatibleVersions(self.params["mas_channel"], appId) if len(versions) == 0: - self.params[f"mas_app_channel_{appId}"] = prompt(HTML(f"Custom channel for {appId}")) + self.params[f"mas_app_channel_{appId}"] = prompt( + HTML(f"Custom channel for {appId}") + ) else: self.params[f"mas_app_channel_{appId}"] = versions[0] @logMethodCall def configStorageClasses(self): self.printH1("Configure Storage Class Usage") - self.printDescription([ - "Maximo Application Suite and it's dependencies require storage classes that support ReadWriteOnce (RWO) and ReadWriteMany (RWX) access modes:", - " - ReadWriteOnce volumes can be mounted as read-write by multiple pods on a single node.", - " - ReadWriteMany volumes can be mounted as read-write by multiple pods across many nodes.", - "" - ]) + self.printDescription( + [ + "Maximo Application Suite and it's dependencies require storage classes that support ReadWriteOnce (RWO) and ReadWriteMany (RWX) access modes:", + " - ReadWriteOnce volumes can be mounted as read-write by multiple pods on a single node.", + " - ReadWriteMany volumes can be mounted as read-write by multiple pods across many nodes.", + "", + ] + ) defaultStorageClasses = getDefaultStorageClasses(self.dynamicClient) if defaultStorageClasses.provider is not None: - print_formatted_text(HTML(f"Storage provider auto-detected: {defaultStorageClasses.providerName}")) - print_formatted_text(HTML(f" - Storage class (ReadWriteOnce): {defaultStorageClasses.rwo}")) - print_formatted_text(HTML(f" - Storage class (ReadWriteMany): {defaultStorageClasses.rwx}")) + print_formatted_text( + HTML( + f"Storage provider auto-detected: {defaultStorageClasses.providerName}" + ) + ) + print_formatted_text( + HTML( + f" - Storage class (ReadWriteOnce): {defaultStorageClasses.rwo}" + ) + ) + print_formatted_text( + HTML( + f" - Storage class (ReadWriteMany): {defaultStorageClasses.rwx}" + ) + ) self.storageClassProvider = defaultStorageClasses.provider self.params["storage_class_rwo"] = defaultStorageClasses.rwo self.params["storage_class_rwx"] = defaultStorageClasses.rwx overrideStorageClasses = False - if "storage_class_rwx" in self.params and self.params["storage_class_rwx"] != "": - overrideStorageClasses = not self.yesOrNo("Use the auto-detected storage classes") + if ( + "storage_class_rwx" in self.params and + self.params["storage_class_rwx"] != "" + ): + overrideStorageClasses = not self.yesOrNo( + "Use the auto-detected storage classes" + ) - if "storage_class_rwx" not in self.params or self.params["storage_class_rwx"] == "" or overrideStorageClasses: + if ( + "storage_class_rwx" not in self.params or + self.params["storage_class_rwx"] == "" or + overrideStorageClasses + ): self.storageClassProvider = "custom" - self.printDescription([ - "Select the ReadWriteOnce and ReadWriteMany storage classes to use from the list below:", - "Enter 'none' for the ReadWriteMany storage class if you do not have a suitable class available in the cluster, however this will limit what can be installed" - ]) + self.printDescription( + [ + "Select the ReadWriteOnce and ReadWriteMany storage classes to use from the list below:", + "Enter 'none' for the ReadWriteMany storage class if you do not have a suitable class available in the cluster, however this will limit what can be installed", + ] + ) for storageClass in getStorageClasses(self.dynamicClient): - print_formatted_text(HTML(f" - {storageClass.metadata.name}")) + print_formatted_text( + HTML( + f" - {storageClass.metadata.name}" + ) + ) - self.params["storage_class_rwo"] = prompt(HTML('ReadWriteOnce (RWO) storage class '), validator=StorageClassValidator(), validate_while_typing=False) - self.params["storage_class_rwx"] = prompt(HTML('ReadWriteMany (RWX) storage class '), validator=StorageClassValidator(), validate_while_typing=False) + self.params["storage_class_rwo"] = prompt( + HTML("ReadWriteOnce (RWO) storage class "), + validator=StorageClassValidator(), + validate_while_typing=False, + ) + self.params["storage_class_rwx"] = prompt( + HTML("ReadWriteMany (RWX) storage class "), + validator=StorageClassValidator(), + validate_while_typing=False, + ) # Configure storage class for pipeline PVC # We prefer to use ReadWriteMany, but we can cope with ReadWriteOnce if necessary @@ -1322,8 +1506,14 @@ def configStorageClasses(self): @logMethodCall def setIoTStorageClasses(self) -> None: if self.installIoT: - self.setParam("mas_app_settings_iot_fpl_pvc_storage_class", self.getParam("storage_class_rwo")) - self.setParam("mas_app_settings_iot_mqttbroker_pvc_storage_class", self.getParam("storage_class_rwo")) + self.setParam( + "mas_app_settings_iot_fpl_pvc_storage_class", + self.getParam("storage_class_rwo"), + ) + self.setParam( + "mas_app_settings_iot_mqttbroker_pvc_storage_class", + self.getParam("storage_class_rwo"), + ) @logMethodCall def setMonitorInstallOrder(self) -> None: @@ -1334,16 +1524,21 @@ def setMonitorInstallOrder(self) -> None: """ if self.installMonitor and self.installIoT: from mas.devops.utils import isVersionEqualOrAfter + monitorChannel = self.getParam("mas_app_channel_monitor") - if monitorChannel and isVersionEqualOrAfter('9.2.0', monitorChannel): + if monitorChannel and isVersionEqualOrAfter("9.2.0", monitorChannel): # Monitor >= 9.2.0: Install Monitor before IoT self.setParam("mas_monitor_install_order", "before-iot") - logger.debug(f"Monitor channel {monitorChannel} >= 9.2.0: Monitor will install before IoT") + logger.debug( + f"Monitor channel {monitorChannel} >= 9.2.0: Monitor will install before IoT" + ) else: # Monitor < 9.2.0: Install Monitor after IoT (legacy) self.setParam("mas_monitor_install_order", "after-iot") - logger.debug(f"Monitor channel {monitorChannel} < 9.2.0: Monitor will install after IoT (legacy behavior)") + logger.debug( + f"Monitor channel {monitorChannel} < 9.2.0: Monitor will install after IoT (legacy behavior)" + ) elif self.installMonitor: # Only Monitor, no IoT - order doesn't matter but set default self.setParam("mas_monitor_install_order", "before-iot") @@ -1356,11 +1551,24 @@ def optimizerSettings(self) -> None: if self.installOptimizer: self.printH1("Configure Maximo Optimizer") if self.isSNO(): - self.printDescription(["Using Optimizer 'limited' plan as it is being installed in a single node cluster"]) + self.printDescription( + [ + "Using Optimizer 'limited' plan as it is being installed in a single node cluster" + ] + ) self.setParam("mas_app_plan_optimizer", "limited") else: - self.printDescription(["Customize your Optimizer installation, 'full' and 'limited' install plans are available, refer to the product documentation for more information"]) - self.promptForString("Plan [full/limited]", "mas_app_plan_optimizer", default="full", validator=OptimizerInstallPlanValidator()) + self.printDescription( + [ + "Customize your Optimizer installation, 'full' and 'limited' install plans are available, refer to the product documentation for more information" + ] + ) + self.promptForString( + "Plan [full/limited]", + "mas_app_plan_optimizer", + default="full", + validator=OptimizerInstallPlanValidator(), + ) @logMethodCall def predictSettings(self) -> None: @@ -1372,9 +1580,11 @@ def predictSettings(self) -> None: def assistSettings(self) -> None: if self.installAssist: self.printH1("Configure Maximo Assist") - self.printDescription([ - "Assist requires access to Cloud Object Storage (COS), this install supports automatic setup using either IBMCloud COS or in-cluster COS via OpenShift Data Foundation (ODF)" - ]) + self.printDescription( + [ + "Assist requires access to Cloud Object Storage (COS), this install supports automatic setup using either IBMCloud COS or in-cluster COS via OpenShift Data Foundation (ODF)" + ] + ) self.configCP4D() self.promptForString("COS Provider [ibm/odf]", "cos_type") @@ -1390,103 +1600,241 @@ def assistSettings(self) -> None: def facilitiesSettings(self) -> None: if self.installFacilities: self.printH1("Configure Maximo Real Estate and Facilities") - self.printDescription([ - "Real Estate and Facilities custom configurations" - ]) - self.printDescription([ - "Maximo Real Estate and Facilities Size:", - " 1. Small", - " 2. Medium", - " 3. Large" - ]) - self.promptForListSelect("Select the size:", ["small", "medium", "large"], "mas_ws_facilities_size") + self.printDescription(["Real Estate and Facilities custom configurations"]) + self.printDescription( + [ + "Maximo Real Estate and Facilities Size:", + " 1. Small", + " 2. Medium", + " 3. Large", + ] + ) + self.promptForListSelect( + "Select the size:", + ["small", "medium", "large"], + "mas_ws_facilities_size", + ) if self.showAdvancedOptions: self.printH2("Maximo Real Estate and Facilities Settings - Advanced") - self.printDescription([ - "Advanced configurations for Real Estate and Facilities are added through an additional file called facilities-configs.yaml" - ]) - self.printDescription([ - "Application Object Migration:", - "Warning! Application upgrades can overwrite your custom changes. Do not select Automatic if you have customized your application. Sets the Application upgrades", - " 1. Manual", - " 2. Load Only", - " 3. Automatic (Load and Import)" - ]) - self.promptForListSelect("Select the Application Object Migration Mode:", ["manual", "load-only", "automatic"], "mas_ws_facilities_app_om_upgrade_mode") + self.printDescription( + [ + "Advanced configurations for Real Estate and Facilities are added through an additional file called facilities-configs.yaml" + ] + ) + self.printDescription( + [ + "Application Object Migration:", + "Warning! Application upgrades can overwrite your custom changes. Do not select Automatic if you have customized your application. Sets the Application upgrades", + " 1. Manual", + " 2. Load Only", + " 3. Automatic (Load and Import)", + ] + ) + self.promptForListSelect( + "Select the Application Object Migration Mode:", + ["manual", "load-only", "automatic"], + "mas_ws_facilities_app_om_upgrade_mode", + ) - if self.yesOrNo("Supply extra XML tags for Real Estate and Facilities server.xml"): - self.promptForString("Real Estate and Facilities Liberty Extension Secret Name", "mas_ws_facilities_liberty_extension_XML") + if self.yesOrNo( + "Supply extra XML tags for Real Estate and Facilities server.xml" + ): + self.promptForString( + "Real Estate and Facilities Liberty Extension Secret Name", + "mas_ws_facilities_liberty_extension_XML", + ) if self.yesOrNo("Supply custom AES Encryption Password"): - self.promptForString("Real Estate and Facilities AES Vault Secret Name", "mas_ws_facilities_vault_secret") + self.promptForString( + "Real Estate and Facilities AES Vault Secret Name", + "mas_ws_facilities_vault_secret", + ) - self.promptForString("Set Real Estate and Facilities Routes Timeout:", "mas_ws_facilities_routes_timeout", default="600s") - self.promptForInt("Set Facilities maximum connection poll size:", "mas_ws_facilities_db_maxconnpoolsize", default=200) + self.promptForString( + "Set Real Estate and Facilities Routes Timeout:", + "mas_ws_facilities_routes_timeout", + default="600s", + ) + self.promptForInt( + "Set Facilities maximum connection poll size:", + "mas_ws_facilities_db_maxconnpoolsize", + default=200, + ) - self.printDescription(["Real Estate and Facilities Persistent Volume Storage Configuration"]) + self.printDescription( + [ + "Real Estate and Facilities Persistent Volume Storage Configuration" + ] + ) defaultStorageClasses = getDefaultStorageClasses(self.dynamicClient) notUseAutodetectedStorageClasses = False if defaultStorageClasses.provider is not None: self.storageClassProvider = defaultStorageClasses.provider - print_formatted_text(HTML(f"Storage provider auto-detected: {defaultStorageClasses.providerName}")) - print_formatted_text(HTML(f" - Storage class (ReadWriteMany): {defaultStorageClasses.rwx}")) - print_formatted_text(HTML(f" - Storage class (ReadWriteOnce): {defaultStorageClasses.rwo}")) + print_formatted_text( + HTML( + f"Storage provider auto-detected: {defaultStorageClasses.providerName}" + ) + ) + print_formatted_text( + HTML( + f" - Storage class (ReadWriteMany): {defaultStorageClasses.rwx}" + ) + ) + print_formatted_text( + HTML( + f" - Storage class (ReadWriteOnce): {defaultStorageClasses.rwo}" + ) + ) if self.yesOrNo("Use the auto-detected storage classes"): - self.printDescription([ - "Storage Mode for Userfiles PVC:", - " 1. ReadWriteMany", - " 2. ReadWriteOnce" - ]) - storageMode = self.promptForListSelect("Select the storage mode for user files PVC:", ["ReadWriteMany", "ReadWriteOnce"], "mas_ws_facilities_storage_userfiles_mode", default=1) - _ = self.setParam("mas_ws_facilities_storage_userfiles_class", defaultStorageClasses.rwx) if storageMode == "ReadWriteMany" else self.setParam("mas_ws_facilities_storage_userfiles_class", defaultStorageClasses.rwo) - self.promptForInt("User file PVC size (Gb):", "mas_ws_facilities_storage_userfiles_size", default=50) - storageMode = self.promptForListSelect("Select the storage mode for log PVC:", ["ReadWriteMany", "ReadWriteOnce"], "mas_ws_facilities_storage_log_mode", default=1) - _ = self.setParam("mas_ws_facilities_storage_log_class", defaultStorageClasses.rwx) if storageMode == "ReadWriteMany" else self.setParam("mas_ws_facilities_storage_log_class", defaultStorageClasses.rwo) - self.promptForInt("Log PVC size (Gb):", "mas_ws_facilities_storage_log_size", default=30) + self.printDescription( + [ + "Storage Mode for Userfiles PVC:", + " 1. ReadWriteMany", + " 2. ReadWriteOnce", + ] + ) + storageMode = self.promptForListSelect( + "Select the storage mode for user files PVC:", + ["ReadWriteMany", "ReadWriteOnce"], + "mas_ws_facilities_storage_userfiles_mode", + default=1, + ) + _ = ( + self.setParam( + "mas_ws_facilities_storage_userfiles_class", + defaultStorageClasses.rwx, + ) + if storageMode == "ReadWriteMany" + else self.setParam( + "mas_ws_facilities_storage_userfiles_class", + defaultStorageClasses.rwo, + ) + ) + self.promptForInt( + "User file PVC size (Gb):", + "mas_ws_facilities_storage_userfiles_size", + default=50, + ) + storageMode = self.promptForListSelect( + "Select the storage mode for log PVC:", + ["ReadWriteMany", "ReadWriteOnce"], + "mas_ws_facilities_storage_log_mode", + default=1, + ) + _ = ( + self.setParam( + "mas_ws_facilities_storage_log_class", + defaultStorageClasses.rwx, + ) + if storageMode == "ReadWriteMany" + else self.setParam( + "mas_ws_facilities_storage_log_class", + defaultStorageClasses.rwo, + ) + ) + self.promptForInt( + "Log PVC size (Gb):", + "mas_ws_facilities_storage_log_size", + default=30, + ) else: notUseAutodetectedStorageClasses = True - if defaultStorageClasses.provider is None or notUseAutodetectedStorageClasses: + if ( + defaultStorageClasses.provider is None or + notUseAutodetectedStorageClasses + ): for storageClass in getStorageClasses(self.dynamicClient): - print_formatted_text(HTML(f" - {storageClass.metadata.name}")) - self.promptForString("Select storage class for user files PVC:", "mas_ws_facilities_storage_userfiles_class") - self.promptForString("Select storage class for log PVC:", "mas_ws_facilities_storage_log_class") - self.printDescription([ - "Storage Mode for Userfiles PVC:", - " 1. ReadWriteMany", - " 2. ReadWriteOnce" - ]) - self.promptForListSelect("Select the storage mode for user files PVC:", ["ReadWriteMany", "ReadWriteOnce"], "mas_ws_facilities_storage_userfiles_mode", default=1) - self.promptForListSelect("Select the storage mode for log PVC:", ["ReadWriteMany", "ReadWriteOnce"], "mas_ws_facilities_storage_log_mode", default=1) - self.promptForInt("User file PVC size (Gb):", "mas_ws_facilities_storage_userfiles_size", default=50) - self.promptForInt("Log PVC size (Gb):", "mas_ws_facilities_storage_log_size", default=30) + print_formatted_text( + HTML( + f" - {storageClass.metadata.name}" + ) + ) + self.promptForString( + "Select storage class for user files PVC:", + "mas_ws_facilities_storage_userfiles_class", + ) + self.promptForString( + "Select storage class for log PVC:", + "mas_ws_facilities_storage_log_class", + ) + self.printDescription( + [ + "Storage Mode for Userfiles PVC:", + " 1. ReadWriteMany", + " 2. ReadWriteOnce", + ] + ) + self.promptForListSelect( + "Select the storage mode for user files PVC:", + ["ReadWriteMany", "ReadWriteOnce"], + "mas_ws_facilities_storage_userfiles_mode", + default=1, + ) + self.promptForListSelect( + "Select the storage mode for log PVC:", + ["ReadWriteMany", "ReadWriteOnce"], + "mas_ws_facilities_storage_log_mode", + default=1, + ) + self.promptForInt( + "User file PVC size (Gb):", + "mas_ws_facilities_storage_userfiles_size", + default=50, + ) + self.promptForInt( + "Log PVC size (Gb):", + "mas_ws_facilities_storage_log_size", + default=30, + ) if self.yesOrNo("Supply configuration for dedicated workflow agents"): - print_formatted_text(HTML(" Example: '[{\"name\":\"dwfa1\",\"members\":[{\"name\": \"u1\", \"class\": \"user\"}]}, {\"name\":\"dwfa2\",\"members\":[{\"name\": \"u2\", \"class\": \"user\"},{\"name\":\"g1\", \"class\":\"group\"}]}]' ")) - self.promptForString("Dedicated Workflow Agent JSON:", "mas_ws_facilities_dwfagents", validator=JsonValidator()) + print_formatted_text( + HTML( + ' Example: \'[{"name":"dwfa1","members":[{"name": "u1", "class": "user"}]}, {"name":"dwfa2","members":[{"name": "u2", "class": "user"},{"name":"g1", "class":"group"}]}]\' ' + ) + ) + self.promptForString( + "Dedicated Workflow Agent JSON:", + "mas_ws_facilities_dwfagents", + validator=JsonValidator(), + ) # If advanced options is selected, we need to create a file to add props not supported by Tekton self.selectLocalConfigDir() if self.localConfigDir is not None: - facilitiesConfigsPath = path.join(self.localConfigDir, "facilities-configs.yaml") + facilitiesConfigsPath = path.join( + self.localConfigDir, "facilities-configs.yaml" + ) self.generateFacilitiesCfg(destination=facilitiesConfigsPath) - self.setParam("mas_ws_facilities_config_file", "/workspace/configs/facilities-configs.yaml") + self.setParam( + "mas_ws_facilities_config_file", + "/workspace/configs/facilities-configs.yaml", + ) @logMethodCall def configAIService(self): self.printH1("Configure AI Service Instance") - self.printDescription([ - "Instance ID restrictions:", - " - Must be 3-12 characters long", - " - Must only use lowercase letters, numbers, and hypen (-) symbol", - " - Must start with a lowercase letter", - " - Must end with a lowercase letter or a number" - ]) + self.printDescription( + [ + "Instance ID restrictions:", + " - Must be 3-12 characters long", + " - Must only use lowercase letters, numbers, and hypen (-) symbol", + " - Must start with a lowercase letter", + " - Must end with a lowercase letter or a number", + ] + ) # Install Db2 for AI Service self.setParam("db2_action_aiservice", "install") - self.promptForString("Instance ID", "aiservice_instance_id", validator=InstanceIDFormatValidator()) - self.params["aiservice_channel"] = prompt(HTML('Custom channel for AI Service ')) + self.promptForString( + "Instance ID", + "aiservice_instance_id", + validator=InstanceIDFormatValidator(), + ) + self.params["aiservice_channel"] = prompt( + HTML("Custom channel for AI Service ") + ) @logMethodCall def aiServiceSettings(self) -> None: @@ -1495,32 +1843,54 @@ def aiServiceSettings(self) -> None: # Ask about MinIO installation FIRST (moved from aiServiceDependencies) self.printH2("Storage Configuration") - self.printDescription(["AI Service requires object storage for pipelines, tenants, and templates. You can either install MinIO in-cluster or connect to external storage."]) + self.printDescription( + [ + "AI Service requires object storage for pipelines, tenants, and templates. You can either install MinIO in-cluster or connect to external storage." + ] + ) if self.yesOrNo("Install Minio"): # Only ask for MinIO credentials self.promptForString("minio root username", "minio_root_user") - self.promptForString("minio root password", "minio_root_password", isPassword=True) + self.promptForString( + "minio root password", "minio_root_password", isPassword=True + ) # Auto-set MinIO storage defaults (same as non-interactive mode) self._setMinioStorageDefaults() else: # Ask for external storage configuration - self.printDescription(["Configure your external object storage (S3-compatible) connection details:"]) + self.printDescription( + [ + "Configure your external object storage (S3-compatible) connection details:" + ] + ) self.promptForString("Storage access key", "aiservice_s3_accesskey") - self.promptForString("Storage secret key", "aiservice_s3_secretkey", isPassword=True) + self.promptForString( + "Storage secret key", "aiservice_s3_secretkey", isPassword=True + ) self.promptForString("Storage host", "aiservice_s3_host") self.promptForString("Storage port", "aiservice_s3_port") self.promptForString("Storage ssl", "aiservice_s3_ssl") self.promptForString("Storage region", "aiservice_s3_region") - self.printDescription([ - "", - "Storage bucket prefix restrictions:", - " - Must be 1-4 characters long" - ]) - self.promptForString("Storage bucket prefix", "aiservice_s3_bucket_prefix", validator=BucketPrefixValidator()) - self.promptForString("Storage tenants bucket", "aiservice_s3_tenants_bucket") - self.promptForString("Storage templates bucket", "aiservice_s3_templates_bucket") + self.printDescription( + [ + "", + "Storage bucket prefix restrictions:", + " - Must be 1-4 characters long", + ] + ) + self.promptForString( + "Storage bucket prefix", + "aiservice_s3_bucket_prefix", + validator=BucketPrefixValidator(), + ) + self.promptForString( + "Storage tenants bucket", "aiservice_s3_tenants_bucket" + ) + self.promptForString( + "Storage templates bucket", "aiservice_s3_templates_bucket" + ) # Configure Certificate Issuer self.configAIServiceCertIssuer() @@ -1529,23 +1899,31 @@ def aiServiceSettings(self) -> None: def configAIServiceCertIssuer(self): if self.showAdvancedOptions: self.printH1("Configure Certificate Issuer") - configureCertIssuer = self.yesOrNo('Configure certificate issuer') + configureCertIssuer = self.yesOrNo("Configure certificate issuer") if configureCertIssuer: - self.promptForString("Certificate issuer name", "aiservice_certificate_issuer") + self.promptForString( + "Certificate issuer name", "aiservice_certificate_issuer" + ) @logMethodCall def aiServiceTenantSettings(self) -> None: if self.installAIService: self.printH1("AI Service Tenant Settings") - self.printDescription([ - "AI Service will reserve AppPoints for a fixed period of time based on the values you enter:" - ]) + self.printDescription( + [ + "AI Service will reserve AppPoints for a fixed period of time based on the values you enter:" + ] + ) today = datetime.today() oneyear = datetime.today() + relativedelta(years=1) self.setParam("tenant_entitlement_type", "standard") - self.setParam("tenant_entitlement_start_date", today.strftime('%Y-%m-%d')) - self.promptForString("Entitlement end date (YYYY-MM-DD)", "tenant_entitlement_end_date", default=oneyear.strftime('%Y-%m-%d')) + self.setParam("tenant_entitlement_start_date", today.strftime("%Y-%m-%d")) + self.promptForString( + "Entitlement end date (YYYY-MM-DD)", + "tenant_entitlement_end_date", + default=oneyear.strftime("%Y-%m-%d"), + ) self.aiserviceTenantSchedulingConfigFileLocal = None self.configSchedulingConstraints() @@ -1554,17 +1932,25 @@ def aiServiceTenantSettings(self) -> None: def configSchedulingConstraints(self): if self.showAdvancedOptions: self.printH1("Scheduling configuration for AI Workloads") - self.printDescription(content=[ - "AI Service supports configuring tolerations and nodeSelector per tenant to schedule AI workloads(training pipelines & Inference services) on dedicated nodes.", - "To configure tolerations and nodeSelector, create a YAML configuration file", - "The YAML file must contain `pipeline` and/or `predictor` objects. Each object can have:", - " `tolerations`: List of Kubernetes tolerations (required fields: `key`, `operator`, `effect`)", - " `nodeSelector`: Dictionary of node label key-value pairs", - ]) + self.printDescription( + content=[ + "AI Service supports configuring tolerations and nodeSelector per tenant to schedule AI workloads(training pipelines & Inference services) on dedicated nodes.", + "To configure tolerations and nodeSelector, create a YAML configuration file", + "The YAML file must contain `pipeline` and/or `predictor` objects. Each object can have:", + " `tolerations`: List of Kubernetes tolerations (required fields: `key`, `operator`, `effect`)", + " `nodeSelector`: Dictionary of node label key-value pairs", + ] + ) - configSchedulingConstraints = self.yesOrNo('Configure Scheduling policies for AI Service tenant') + configSchedulingConstraints = self.yesOrNo( + "Configure Scheduling policies for AI Service tenant" + ) if configSchedulingConstraints: - self.aiserviceTenantSchedulingConfigFileLocal = self.promptForFile("Scheduling configuration YAML file", mustExist=True, envVar="AISERVICE_TENANT_SCHEDULING_CONFIG_FILE") + self.aiserviceTenantSchedulingConfigFileLocal = self.promptForFile( + "Scheduling configuration YAML file", + mustExist=True, + envVar="AISERVICE_TENANT_SCHEDULING_CONFIG_FILE", + ) @logMethodCall def _setMinioStorageDefaults(self) -> None: @@ -1589,51 +1975,76 @@ def _setMinioStorageDefaults(self) -> None: def aiServiceIntegrations(self) -> None: if self.installAIService: self.printH1("WatsonX Integration") - self.printDescription([ - "This CLI section configures the integration between the AI Service and IBM watsonx.ai. AI Service", - "uses watsonx for model deployment and inferencing.", - "", - "The WatsonX API key must be a **platform API key** associated with a user that has at least:", - "- **Editor permission** for the project", - "- **Viewer permission** for the space", - "You can generate this key by following IBM's documentation: https://www.ibm.com/docs/en/watsonx/w-and-w/2.2.0?topic=tutorials-generating-api-keys#api-keys__platform__title__1", - "", - "The endpoint URL is your WatsonX Machine Learning service URL. It can be found in the watsonx.ai", - "documentation: https://cloud.ibm.com/apidocs/watsonx-ai-cp/watsonx-ai-cp-2.2.0#endpoint-url", - "", - "The project ID refers to your specific watsonx.ai project where your ML models and assets are stored.", - "", - "Optional identifiers:", - " - DeploymentId: ID of the model deployment in a **dedicated watsonx runtime**", - " (e.g., granite-3-2-8b-instruct deployed in your dedicated runtime).", - " - SpaceId: ID of the **watsonx deployment space** where deployments are managed.", - "Provide these only if you already have them; otherwise AI Service can proceed with defaults/workflows", - "that do not require pre-existing deployment/space identifiers.", - "", - ]) - self.promptForString("Watsonxai api key", "aiservice_watsonxai_apikey", isPassword=True) - watsonxUrl = self.promptForString("Watsonxai machine learning url", "aiservice_watsonxai_url") - self.promptForString("Watsonxai project id", "aiservice_watsonxai_project_id") + self.printDescription( + [ + "This CLI section configures the integration between the AI Service and IBM watsonx.ai. AI Service", + "uses watsonx for model deployment and inferencing.", + "", + "The WatsonX API key must be a **platform API key** associated with a user that has at least:", + "- **Editor permission** for the project", + "- **Viewer permission** for the space", + "You can generate this key by following IBM's documentation: https://www.ibm.com/docs/en/watsonx/w-and-w/2.2.0?topic=tutorials-generating-api-keys#api-keys__platform__title__1", + "", + "The endpoint URL is your WatsonX Machine Learning service URL. It can be found in the watsonx.ai", + "documentation: https://cloud.ibm.com/apidocs/watsonx-ai-cp/watsonx-ai-cp-2.2.0#endpoint-url", + "", + "The project ID refers to your specific watsonx.ai project where your ML models and assets are stored.", + "", + "Optional identifiers:", + " - DeploymentId: ID of the model deployment in a **dedicated watsonx runtime**", + " (e.g., granite-3-2-8b-instruct deployed in your dedicated runtime).", + " - SpaceId: ID of the **watsonx deployment space** where deployments are managed.", + "Provide these only if you already have them; otherwise AI Service can proceed with defaults/workflows", + "that do not require pre-existing deployment/space identifiers.", + "", + ] + ) + self.promptForString( + "Watsonxai api key", "aiservice_watsonxai_apikey", isPassword=True + ) + watsonxUrl = self.promptForString( + "Watsonxai machine learning url", "aiservice_watsonxai_url" + ) + self.promptForString( + "Watsonxai project id", "aiservice_watsonxai_project_id" + ) if self.yesOrNo("Does the Watsonxai AI use a self-signed certificate"): - self.promptForString("Watsonxai CA certificate (PEM format)", "aiservice_watsonxai_ca_crt") - self.promptForString("Watsonxai Deployment ID (optional)", "aiservice_watsonxai_deployment_id") - self.promptForString("Watsonxai Space ID (optional)", "aiservice_watsonxai_space_id") + self.promptForString( + "Watsonxai CA certificate (PEM format)", + "aiservice_watsonxai_ca_crt", + ) + self.promptForString( + "Watsonxai Deployment ID (optional)", + "aiservice_watsonxai_deployment_id", + ) + self.promptForString( + "Watsonxai Space ID (optional)", "aiservice_watsonxai_space_id" + ) if ".ibm.com" not in watsonxUrl: - self.promptForString("Watsonxai Instance ID (optional)", "aiservice_watsonxai_instance_id") - self.promptForString("Watsonxai Username (optional)", "aiservice_watsonxai_username") - self.promptForString("Watsonxai Version (optional)", "aiservice_watsonxai_version") + self.promptForString( + "Watsonxai Instance ID (optional)", + "aiservice_watsonxai_instance_id", + ) + self.promptForString( + "Watsonxai Username (optional)", "aiservice_watsonxai_username" + ) + self.promptForString( + "Watsonxai Version (optional)", "aiservice_watsonxai_version" + ) self.printH1("RSL Integration") - self.printDescription([ - "RSL (Reliable Strategy Library) connects to strategic asset management via STRATEGIZEAPI.", - "", - "RSL URL: https://api.rsl-service.suite.maximo.com (standard for all customers)", - "Org ID: Get from MAS Manage > System Properties > 'mxe.rs.rslorgid'", - "Token: Use your IBM entitlement key (same as MAS installation)", - "", - "Note: Future versions will auto-configure these from MAS Manage.", - "" - ]) + self.printDescription( + [ + "RSL (Reliable Strategy Library) connects to strategic asset management via STRATEGIZEAPI.", + "", + "RSL URL: https://api.rsl-service.suite.maximo.com (standard for all customers)", + "Org ID: Get from MAS Manage > System Properties > 'mxe.rs.rslorgid'", + "Token: Use your IBM entitlement key (same as MAS installation)", + "", + "Note: Future versions will auto-configure these from MAS Manage.", + "", + ] + ) self.promptForString("RSL url", "rsl_url") self.promptForString("ORG Id of RSL", "rsl_org_id") rslToken = self.promptForString("Token for RSL", isPassword=True) @@ -1646,24 +2057,26 @@ def aiServiceIntegrations(self) -> None: @logMethodCall def chooseInstallFlavour(self) -> None: self.printH1("Choose Install Mode") - self.printDescription([ - "There are two flavours of the interactive install to choose from: Simplified and Advanced. The simplified option will present fewer dialogs, but you lose the ability to configure the following aspects of the installation:", - " - Configure installation namespaces", - " - Provide pod templates", - " - Configure Single Sign-On (SSO) settings" - " - Configure whether to trust well-known certificate authorities by default (defaults to enabled)", - " - Configure whether the Guided Tour feature is enabled (defaults to enabled)", - " - Configure whether special characters are allowed in usernames and userids (defaults to disabled)", - " - Configure a custom domain, DNS integrations, routing mode and manual certificates", - " - Customize Maximo Manage database settings (schema, tablespace, indexspace)", - " - Customize Maximo Manage server bundle configuration (defaults to \"all\" configuration)", - " - Enable optional Maximo Manage integration Cognos Analytics and Watson Studio Local", - " - Enable optional Real Estate and Facilities configurations", - " - Customize Db2 node affinity and tolerations, memory, cpu, and storage settings (when using the IBM Db2 Universal Operator)", - " - Choose alternative Apache Kafka providers (default to Strimzi)", - " - Customize Grafana storage settings", - " - Customize Scheduling configuration for AI workloads(Training pipeline & Inference services) for AI Service tenant" - ]) + self.printDescription( + [ + "There are two flavours of the interactive install to choose from: Simplified and Advanced. The simplified option will present fewer dialogs, but you lose the ability to configure the following aspects of the installation:", + " - Configure installation namespaces", + " - Provide pod templates", + " - Configure Single Sign-On (SSO) settings" + " - Configure whether to trust well-known certificate authorities by default (defaults to enabled)", + " - Configure whether the Guided Tour feature is enabled (defaults to enabled)", + " - Configure whether special characters are allowed in usernames and userids (defaults to disabled)", + " - Configure a custom domain, DNS integrations, routing mode and manual certificates", + " - Customize Maximo Manage database settings (schema, tablespace, indexspace)", + ' - Customize Maximo Manage server bundle configuration (defaults to "all" configuration)', + " - Enable optional Maximo Manage integration Cognos Analytics and Watson Studio Local", + " - Enable optional Real Estate and Facilities configurations", + " - Customize Db2 node affinity and tolerations, memory, cpu, and storage settings (when using the IBM Db2 Universal Operator)", + " - Choose alternative Apache Kafka providers (default to Strimzi)", + " - Customize Grafana storage settings", + " - Customize Scheduling configuration for AI workloads(Training pipeline & Inference services) for AI Service tenant", + ] + ) self.showAdvancedOptions = self.yesOrNo("Show advanced installation options") @logMethodCall @@ -1756,16 +2169,36 @@ def nonInteractiveMode(self) -> None: self.aiserviceTenantSchedulingConfigFileLocal = None self.approvals: Dict[str, Dict[str, Any]] = { - "approval_core": {"id": "suite-verify"}, # After Core Platform verification has completed - "approval_assist": {"id": "app-cfg-assist"}, # After Assist workspace has been configured - "approval_iot": {"id": "app-cfg-iot"}, # After IoT workspace has been configured - "approval_manage": {"id": "app-cfg-manage"}, # After Manage workspace has been configured - "approval_monitor": {"id": "app-cfg-monitor"}, # After Monitor workspace has been configured - "approval_optimizer": {"id": "app-cfg-optimizer"}, # After Optimizer workspace has been configured - "approval_predict": {"id": "app-cfg-predict"}, # After Predict workspace has been configured - "approval_visualinspection": {"id": "app-cfg-visualinspection"}, # After Visual Inspection workspace has been configured - "approval_facilities": {"id": "app-cfg-facilities"}, # After Facilities workspace has been configured - "approval_aiservice": {"id": "aiservice"} # After AI Service Tenant has been configured  + "approval_core": { + "id": "suite-verify" + }, # After Core Platform verification has completed + "approval_assist": { + "id": "app-cfg-assist" + }, # After Assist workspace has been configured + "approval_iot": { + "id": "app-cfg-iot" + }, # After IoT workspace has been configured + "approval_manage": { + "id": "app-cfg-manage" + }, # After Manage workspace has been configured + "approval_monitor": { + "id": "app-cfg-monitor" + }, # After Monitor workspace has been configured + "approval_optimizer": { + "id": "app-cfg-optimizer" + }, # After Optimizer workspace has been configured + "approval_predict": { + "id": "app-cfg-predict" + }, # After Predict workspace has been configured + "approval_visualinspection": { + "id": "app-cfg-visualinspection" + }, # After Visual Inspection workspace has been configured + "approval_facilities": { + "id": "app-cfg-facilities" + }, # After Facilities workspace has been configured + "approval_aiservice": { + "id": "aiservice" + }, # After AI Service Tenant has been configured } self.configGrafana() @@ -1806,29 +2239,41 @@ def nonInteractiveMode(self) -> None: self.setParam("aiservice_odh_model_deployment_type", "raw") else: self.operationalMode = 2 - self.setParam("mas_annotations", "mas.ibm.com/operationalMode=nonproduction") + self.setParam( + "mas_annotations", "mas.ibm.com/operationalMode=nonproduction" + ) self.setParam("environment_type", "non-production") self.setParam("aiservice_odh_model_deployment_type", "serverless") elif key == "additional_configs": self.localConfigDir = value # If there is a file named mongodb-system.yaml we will use this as a BYO MongoDB datasource - if self.localConfigDir is not None and path.exists(path.join(self.localConfigDir, "mongodb-system.yaml")): + if self.localConfigDir is not None and path.exists( + path.join(self.localConfigDir, "mongodb-system.yaml") + ): self.setParam("mongodb_action", "byo") - self.setParam("sls_mongodb_cfg_file", "/workspace/additional-configs/mongodb-system.yaml") + self.setParam( + "sls_mongodb_cfg_file", + "/workspace/additional-configs/mongodb-system.yaml", + ) # If there is a file named kafka--system.yaml we will use this as a BYO Kafka datasource if self.localConfigDir is not None: instanceId = self.getParam("mas_instance_id") if instanceId is not None and instanceId != "": - kafkaConfigFile = path.join(self.localConfigDir, f"kafka-{instanceId}-system.yaml") + kafkaConfigFile = path.join( + self.localConfigDir, f"kafka-{instanceId}-system.yaml" + ) if path.exists(kafkaConfigFile): self.setParam("kafka_action_system", "byo") elif key == "pod_templates": # For the named configurations we will convert into the path if value in ["best-effort", "guaranteed"]: - self.setParam("mas_pod_templates_dir", path.join(self.templatesDir, "pod-templates", value)) + self.setParam( + "mas_pod_templates_dir", + path.join(self.templatesDir, "pod-templates", value), + ) else: self.setParam("mas_pod_templates_dir", value) @@ -1878,7 +2323,10 @@ def nonInteractiveMode(self) -> None: self.setParam("db2_action_aiservice", "install") self.installAIService = True # Set manage - bind - AI Service params same as provided AI Service's params - self.setParam("manage_bind_aiservice_instance_id", vars(self.args).get("aiservice_instance_id", "")) + self.setParam( + "manage_bind_aiservice_instance_id", + vars(self.args).get("aiservice_instance_id", ""), + ) self.setParam("manage_bind_aiservice_tenant_id", "user") elif key == "configure_aiassistant": if value is not None and value != "": @@ -1892,11 +2340,19 @@ def nonInteractiveMode(self) -> None: self.setParam("configure_aiassistant", value) elif key == "manage_bind_aiservice_instance_id": # only set if AI Service not being installed - if not vars(self.args).get("aiservice_instance_id") and value is not None and value != "": + if ( + not vars(self.args).get("aiservice_instance_id") and + value is not None and + value != "" + ): self.setParam("manage_bind_aiservice_instance_id", value) elif key == "manage_bind_aiservice_tenant_id": # only set if AI Service not being installed - if not vars(self.args).get("aiservice_instance_id") and value is not None and value != "": + if ( + not vars(self.args).get("aiservice_instance_id") and + value is not None and + value != "" + ): self.setParam("manage_bind_aiservice_tenant_id", value) # ArcGIS settings @@ -1910,7 +2366,9 @@ def nonInteractiveMode(self) -> None: if value is not None: self.setParam(key, value) if value in ["jms", "snojms"]: - self.setParam("mas_app_settings_persistent_volumes_flag", "true") + self.setParam( + "mas_app_settings_persistent_volumes_flag", "true" + ) elif key == "mas_app_settings_base_lang": if value is not None and value != "": self.setParam(key, value.upper()) @@ -1922,7 +2380,9 @@ def nonInteractiveMode(self) -> None: 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") + self.setParam( + "sls_mongodb_cfg_file", f"/workspace/configs/mongo-{value}.yml" + ) # SLS elif key == "license_file": @@ -1934,7 +2394,9 @@ def nonInteractiveMode(self) -> None: self.db2LicenseFileLocal = value elif key == "dedicated_sls": if value: - self.setParam("sls_namespace", f"mas-{self.args.mas_instance_id}-sls") + self.setParam( + "sls_namespace", f"mas-{self.args.mas_instance_id}-sls" + ) elif key == "sls_channel": if self.devMode: if value is not None and value != "": @@ -1952,22 +2414,39 @@ def nonInteractiveMode(self) -> None: elif key.startswith("approval_"): if key not in self.approvals: - raise KeyError(f"{key} is not a supported approval workflow ID: {self.approvals.keys()}") + raise KeyError( + f"{key} is not a supported approval workflow ID: {self.approvals.keys()}" + ) if value != "": valueParts = value.split(":") if len(valueParts) != 3: - self.fatalError(f"Unsupported format for {key} ({value}). Expected MAX_RETRIES:RETRY_DELAY:IGNORE_FAILURE") + self.fatalError( + f"Unsupported format for {key} ({value}). Expected MAX_RETRIES:RETRY_DELAY:IGNORE_FAILURE" + ) else: try: self.approvals[key]["maxRetries"] = int(valueParts[0]) self.approvals[key]["retryDelay"] = int(valueParts[1]) self.approvals[key]["ignoreFailure"] = bool(valueParts[2]) except ValueError: - self.fatalError(f"Unsupported format for {key} ({value}). Expected int:int:boolean") + self.fatalError( + f"Unsupported format for {key} ({value}). Expected int:int:boolean" + ) # Arguments that we don't need to do anything with - elif key in ["accept_license", "dev_mode", "skip_pre_check", "skip_preinstall_rbac", "skip_grafana_install", "no_confirm", "help", "advanced", "simplified", "mas_configure_ingress"]: + elif key in [ + "accept_license", + "dev_mode", + "skip_pre_check", + "skip_preinstall_rbac", + "skip_grafana_install", + "no_confirm", + "help", + "advanced", + "simplified", + "mas_configure_ingress", + ]: pass elif key == "manual_certificates": @@ -1990,44 +2469,62 @@ def nonInteractiveMode(self) -> None: "aiservice_s3_port", "aiservice_s3_ssl", "aiservice_s3_bucket_prefix", - "aiservice_s3_region" + "aiservice_s3_region", ] if value is None: for uKey in incompatibleWithMinioInstall: if vars(self.args)[uKey] is None: - self.fatalError(f"Parameter is required when --install-minio is not set: {uKey}") + self.fatalError( + f"Parameter is required when --install-minio is not set: {uKey}" + ) elif value is not None and value == "true": # If user is installing Minio in-cluster then we know how to connect to it already for uKey in incompatibleWithMinioInstall: if vars(self.args)[uKey] is not None: - self.fatalError(f"Unsupported parameter for --install-minio: {uKey}") + self.fatalError( + f"Unsupported parameter for --install-minio: {uKey}" + ) for rKey in ["minio_root_user", "minio_root_password"]: if vars(self.args)[rKey] is None: - self.fatalError(f"Missing required parameter for --install-minio: {rKey}") + self.fatalError( + f"Missing required parameter for --install-minio: {rKey}" + ) # Extra validation: minio_root_password must be at least 8 characters minio_pass = vars(self.args)["minio_root_password"] if len(minio_pass) < 8: - self.fatalError("minio_root_password must be at least 8 characters long") + self.fatalError( + "minio_root_password must be at least 8 characters long" + ) # self.setParam("aiservice_s3_provider", "minio") - self.setParam("aiservice_s3_accesskey", self.args.minio_root_user) - self.setParam("aiservice_s3_secretkey", self.args.minio_root_password) + self.setParam( + "aiservice_s3_accesskey", self.args.minio_root_user + ) + self.setParam( + "aiservice_s3_secretkey", self.args.minio_root_password + ) # TODO: Duplication -- we already have the URL, why do we need all the individual parts, # especially when we don't need them for the tenant? - self.setParam("aiservice_s3_host", "minio-service.minio.svc.cluster.local") + self.setParam( + "aiservice_s3_host", "minio-service.minio.svc.cluster.local" + ) self.setParam("aiservice_s3_port", "9000") self.setParam("aiservice_s3_ssl", "false") self.setParam("aiservice_s3_region", "none") self.setParam("aiservice_s3_bucket_prefix", "s3-") else: - self.fatalError(f"Unsupported value for --install-minio: {value}") + self.fatalError( + f"Unsupported value for --install-minio: {value}" + ) elif key == "aiservice_s3_bucket_prefix": if len(value) == 0 or len(value) > 4: - self.fatalError(f"Unsupported value for --s3-bucket-prefix(Must be 1-4 characters long): {value}") + self.fatalError( + f"Unsupported value for --s3-bucket-prefix(Must be 1-4 characters long): {value}" + ) elif key == "tenant_scheduling_config_file": # No need to perform validation if file exist here, as it has been already validated by argParser type check. @@ -2046,10 +2543,17 @@ def nonInteractiveMode(self) -> None: if self.installFacilities: # Verifiy if any of the props that needs to be in a file are given - if self.getParam("mas_ws_facilities_storage_log_size") != "" or self.getParam("mas_ws_facilities_storage_userfiles_size") != "" or self.getParam("mas_ws_facilities_db_maxconnpoolsize") or self.getParam("mas_ws_facilities_dwfagents"): + if ( + self.getParam("mas_ws_facilities_storage_log_size") != "" or + self.getParam("mas_ws_facilities_storage_userfiles_size") != "" or + self.getParam("mas_ws_facilities_db_maxconnpoolsize") or + self.getParam("mas_ws_facilities_dwfagents") + ): self.selectLocalConfigDir() assert self.localConfigDir is not None, "localConfigDir is None" - facilitiesConfigsPath = path.join(self.localConfigDir, "facilities-configs.yaml") + facilitiesConfigsPath = path.join( + self.localConfigDir, "facilities-configs.yaml" + ) self.generateFacilitiesCfg(destination=facilitiesConfigsPath) self.setParam("mas_ws_facilities_config_map_name", "facilities-config") @@ -2062,7 +2566,9 @@ def nonInteractiveMode(self) -> None: # License file is only optional for existing SLS instance if self.slsLicenseFileLocal is None: - if findSLSByNamespace(self.getParam("sls_namespace"), dynClient=self.dynamicClient): + if findSLSByNamespace( + self.getParam("sls_namespace"), dynClient=self.dynamicClient + ): self.setParam("sls_action", "gencfg") else: self.fatalError("--license-file must be set for new SLS install") @@ -2073,12 +2579,18 @@ def nonInteractiveMode(self) -> None: self.licensePrompt() self.setParam("db2u_kind", "db2ucluster") - 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')})") + 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')})" + ) if self.getParam("mas_permission_mode") != "": - if not isVersionEqualOrAfter('9.2.0', self.getParam("mas_channel")): - self.fatalError(f"--permission-mode is only supported for MAS 9.2+ (selected channel: {self.getParam('mas_channel')})") + if not isVersionEqualOrAfter("9.2.0", self.getParam("mas_channel")): + self.fatalError( + f"--permission-mode is only supported for MAS 9.2+ (selected channel: {self.getParam('mas_channel')})" + ) else: if self.getParam("mas_issuer_kind") == "": if self.getParam("mas_permission_mode") == "cluster": @@ -2086,22 +2598,32 @@ def nonInteractiveMode(self) -> None: else: self.setParam("mas_issuer_kind", "Issuer") - if self.getParam("mas_issuer_kind") == "ClusterIssuer" and self.getParam("mas_permission_mode") != "cluster": + if ( + self.getParam("mas_issuer_kind") == "ClusterIssuer" and + self.getParam("mas_permission_mode") != "cluster" + ): self.fatalError( - "\n".join([ - "Invalid configuration for certificate issuer kind 'ClusterIssuer'", - "ClusterIssuer can only be used when --permission-mode cluster is selected." - ]) + "\n".join( + [ + "Invalid configuration for certificate issuer kind 'ClusterIssuer'", + "ClusterIssuer can only be used when --permission-mode cluster is selected.", + ] + ) ) if self.getParam("dns_provider") != "": - if self.getParam("mas_permission_mode") in ["namespaced", "minimal"]: + if self.getParam("mas_permission_mode") in [ + "namespaced", + "minimal", + ]: self.fatalError( - "\n".join([ - f"Invalid configuration for permission mode '{self.getParam('mas_permission_mode')}'", - "DNS integration is not available in this mode.", - "Remove DNS integration option --dns-provider, or switch to --permission-mode cluster and use --mas-issuer-kind ClusterIssuer.", - ]) + "\n".join( + [ + f"Invalid configuration for permission mode '{self.getParam('mas_permission_mode')}'", + "DNS integration is not available in this mode.", + "Remove DNS integration option --dns-provider, or switch to --permission-mode cluster and use --mas-issuer-kind ClusterIssuer.", + ] + ) ) if ( @@ -2109,13 +2631,15 @@ def nonInteractiveMode(self) -> None: self.getParam("mas_issuer_kind") == "Issuer" ): self.fatalError( - "\n".join([ - "Invalid configuration for certificate issuer kind 'Issuer'", - "DNS integration is not available when --mas-issuer-kind Issuer is selected.", - "Remove DNS integration option --dns-provider, or use --mas-issuer-kind ClusterIssuer.", - ]) + "\n".join( + [ + "Invalid configuration for certificate issuer kind 'Issuer'", + "DNS integration is not available when --mas-issuer-kind Issuer is selected.", + "Remove DNS integration option --dns-provider, or use --mas-issuer-kind ClusterIssuer.", + ] + ) ) - elif isVersionEqualOrAfter('9.2.0', self.getParam("mas_channel")): + elif isVersionEqualOrAfter("9.2.0", self.getParam("mas_channel")): self.setParam("mas_permission_mode", "cluster") if self.getParam("mas_issuer_kind") == "": self.setParam("mas_issuer_kind", "ClusterIssuer") @@ -2124,63 +2648,76 @@ def nonInteractiveMode(self) -> None: self.setDB2DefaultChannel() # Version before 9.1 cannot have empty components - if (self.getParam("mas_channel").startswith("8.") or self.getParam("mas_channel").startswith("9.0")) and (self.getParam("mas_app_channel_manage") is not None and self.getParam("mas_app_channel_manage") != "") and self.getParam("mas_appws_components") == "": - self.fatalError("--manage-components must be set for versions earlier than 9.1.0") + if ( + ( + self.getParam("mas_channel").startswith("8.") or + self.getParam("mas_channel").startswith("9.0") + ) and + ( + self.getParam("mas_app_channel_manage") is not None and + self.getParam("mas_app_channel_manage") != "" + ) and + self.getParam("mas_appws_components") == "" + ): + self.fatalError( + "--manage-components must be set for versions earlier than 9.1.0" + ) # An error should be raised if "health" is not specified when installing Predict. - if ((self.getParam("mas_app_channel_predict") is not None and self.getParam("mas_app_channel_predict") != "") and 'health' not in self.getParam("mas_appws_components")): - self.fatalError("--manage-components must include 'health' component when installing Predict") + if ( + self.getParam("mas_app_channel_predict") is not None and + self.getParam("mas_app_channel_predict") != "" + ) and "health" not in self.getParam("mas_appws_components"): + self.fatalError( + "--manage-components must include 'health' component when installing Predict" + ) # Validate ArcGIS installation requirements in non-interactive mode if self.installArcgis: - hasSpatial = self.installManage and "spatial=" in self.getParam("mas_appws_components") + hasSpatial = self.installManage and "spatial=" in self.getParam( + "mas_appws_components" + ) hasFacilities = self.installFacilities # ArcGIS requires either Spatial or Facilities to be installed if not hasSpatial and not hasFacilities: - self.fatalError("--arcgis-channel requires either Manage with Spatial component (--manage-components must include 'spatial=') or Facilities (--facilities-channel) to be installed") + self.fatalError( + "--arcgis-channel requires either Manage with Spatial component (--manage-components must include 'spatial=') or Facilities (--facilities-channel) to be installed" + ) # ArcGIS requires channel 9.0 or later arcgis_channel = self.getParam("mas_arcgis_channel") - if arcgis_channel and not isVersionEqualOrAfter('9.0.0', arcgis_channel): - self.fatalError(f"--arcgis-channel must be 9.0 or later (current: {arcgis_channel})") + if arcgis_channel and not isVersionEqualOrAfter("9.0.0", arcgis_channel): + self.fatalError( + f"--arcgis-channel must be 9.0 or later (current: {arcgis_channel})" + ) # Validate Kafka requirements for IoT installation in non-interactive mode if self.installIoT: kafkaAction = self.getParam("kafka_action_system") hasKafkaConfig = kafkaAction in ["install", "byo"] if not hasKafkaConfig: - self.fatalError("--iot-channel requires Kafka configuration. Provide Kafka install arguments such as --kafka-provider, or supply a BYO Kafka config file named kafka--system.yaml using --additional-configs") + self.fatalError( + "--iot-channel requires Kafka configuration. Provide Kafka install arguments such as --kafka-provider, or supply a BYO Kafka config file named kafka--system.yaml using --additional-configs" + ) + + # Validate --manage-kafka parameter requirements + if self.getParam("mas_appws_bindings_kafka_manage") != "": + # Validate Kafka provider is configured + kafkaProvider = self.getParam("kafka_provider") + if not kafkaProvider or kafkaProvider == "": + self.fatalError("--manage-kafka requires --kafka-provider to be set") - # Validate Kafka requirements for CIVIL installation in non-interactive mode - isCivilEnabled = self.installManage and "civil=" in self.getParam("mas_appws_components") - if isCivilEnabled: + # Validate Manage version compatibility manageChannel = self.getParam("mas_app_channel_manage") - if manageChannel and isVersionEqualOrAfter('9.2.0', manageChannel): - kafkaAction = self.getParam("kafka_action_system") - hasKafkaConfig = kafkaAction in ["install", "byo"] - if not hasKafkaConfig: - # Warn user but give option to proceed (Civil will work, but Defect Detection won't) - print_formatted_text(HTML("⚠ Warning: Kafka Configuration Required")) - print_formatted_text(HTML( - f"Installing Manage {manageChannel} with Civil Infrastructure component " - "requires Kafka configuration. Civil versions >= 9.2.0 require a shared system-scope Kafka instance." - )) - print_formatted_text(HTML( - "Without Kafka, the Defect Detection functionality will not work." - )) - print() + if manageChannel and not isVersionEqualOrAfter("9.2.0", manageChannel): + self.fatalError( + f"--manage-kafka requires Manage version 9.2.0 or later. Current version: {manageChannel}" + ) - if self.noConfirm: - # In non-interactive mode, log warning and proceed - logger.warning( - f"Installing Manage {manageChannel} with Civil component without Kafka configuration. " - "Defect Detection functionality will not work." - ) - else: - # In interactive mode, ask user if they want to proceed - if not self.yesOrNo("Do you want to proceed with the installation without Kafka? (Defect Detection functionality will not work)"): - self.fatalError("Installation cancelled. Please configure Kafka before installing.") + # Set enableKafkaImageProcessor flag for non-interactive mode + if self.getParam("mas_appws_bindings_kafka_manage") == "system": + self.enableKafkaImageProcessor = True @logMethodCall def install(self, argv): @@ -2212,10 +2749,10 @@ def install(self, argv): if args.skip_pre_check: self.setParam("skip_pre_check", "true") - if hasattr(args, 'skip_preinstall_rbac') and args.skip_preinstall_rbac: + if hasattr(args, "skip_preinstall_rbac") and args.skip_preinstall_rbac: self.setParam("skip_preinstall_rbac", "true") - if hasattr(args, 'mas_configure_ingress') and args.mas_configure_ingress: + if hasattr(args, "mas_configure_ingress") and args.mas_configure_ingress: self.setParam("mas_configure_ingress", "true") if instanceId is None: @@ -2223,11 +2760,17 @@ def install(self, argv): # Connect to the target cluster self.connect() else: - logger.debug("MAS instance ID is set, so we assume already connected to the desired OCP") + logger.debug( + "MAS instance ID is set, so we assume already connected to the desired OCP" + ) self.lookupTargetArchitecture() if self.dynamicClient is None: - print_formatted_text(HTML("Error: The Kubernetes dynamic Client is not available. See log file for details")) + print_formatted_text( + HTML( + "Error: The Kubernetes dynamic Client is not available. See log file for details" + ) + ) exit(1) # Perform a check whether the cluster is set up for airgap install, this will trigger an early failure if the cluster is using the now @@ -2242,6 +2785,7 @@ def install(self, argv): self.configICR() self.configCertManager() # TODO: I think this is redundant, we should look to remove this and the appropriate params in the install pipeline self.deployCP4D = False + self.enableKafkaImageProcessor = False self.setParam("dro_action", "install") @@ -2267,21 +2811,30 @@ def install(self, argv): # Show a summary of the installation configuration 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.printDescription( + [ + "Save and re-use the following script to re-run this install without needing to answer the interactive prompts again", + "", + self.buildCommand(), + ] + ) # Validate IngressController configuration for path-based routing (non-interactive mode only) if not self.isInteractiveMode and self.getParam("mas_routing_mode") == "path": ingressControllerName = None - if hasattr(self.args, 'mas_ingress_controller_name') and self.args.mas_ingress_controller_name: + if ( + hasattr(self.args, "mas_ingress_controller_name") and + self.args.mas_ingress_controller_name + ): ingressControllerName = self.args.mas_ingress_controller_name - logger.info(f"Using IngressController '{ingressControllerName}' from CLI flag") + logger.info( + f"Using IngressController '{ingressControllerName}' from CLI flag" + ) elif self.getParam("mas_ingress_controller_name"): ingressControllerName = self.getParam("mas_ingress_controller_name") - logger.info(f"Using IngressController '{ingressControllerName}' from existing parameter") + logger.info( + f"Using IngressController '{ingressControllerName}' from existing parameter" + ) else: ingressControllerName = "default" logger.info("No IngressController specified, defaulting to 'default'") @@ -2293,85 +2846,100 @@ def install(self, argv): if not canConfigure: self.fatalError( - "\n".join([ - "IngressController Configuration Requires Administrator Permissions", - "========================================================================", - "You do not have sufficient permissions to check or configure the", - f"IngressController '{ingressControllerName}'.", - "", - "If you wish to configure MAS with path-based routing, contact your OpenShift", - "administrator to apply the following configuration:", - "", - " spec:", - " routeAdmission:", - " namespaceOwnership: InterNamespaceAllowed", - "", - "Alternatively, you can use subdomain routing mode:", - " mas install --routing subdomain ..." - ]) + "\n".join( + [ + "IngressController Configuration Requires Administrator Permissions", + "========================================================================", + "You do not have sufficient permissions to check or configure the", + f"IngressController '{ingressControllerName}'.", + "", + "If you wish to configure MAS with path-based routing, contact your OpenShift", + "administrator to apply the following configuration:", + "", + " spec:", + " routeAdmission:", + " namespaceOwnership: InterNamespaceAllowed", + "", + "Alternatively, you can use subdomain routing mode:", + " mas install --routing subdomain ...", + ] + ) ) - exists, isConfigured = self._checkIngressControllerForPathRouting(ingressControllerName) + exists, isConfigured = self._checkIngressControllerForPathRouting( + ingressControllerName + ) if not exists: self.fatalError( - "\n".join([ - "IngressController Not Found", - "", - "========================================================================", - f"You selected IngressController '{ingressControllerName}', but it does not exist", - "in the openshift-ingress-operator namespace.", - "", - "To fix this issue:", - "", - "1. List available IngressControllers:", - " oc get ingresscontroller -n openshift-ingress-operator", - "", - "2. Use an existing controller name with --ingress-controller-name flag:", - " mas install --routing path --ingress-controller-name [existing-controller] ...", - "", - "3. Or use the default controller (usually named 'default'):", - " mas install --routing path --ingress-controller-name default ...", - "", - "Alternatively, you can use subdomain routing mode:", - " mas install --routing subdomain ..." - ]) - ) - elif not isConfigured: - if hasattr(self.args, 'mas_configure_ingress') and self.args.mas_configure_ingress: - logger.info(f"IngressController '{ingressControllerName}' will be configured for path-based routing before MAS installation") - self.setParam("mas_configure_ingress", "true") - else: - self.fatalError( - "\n".join([ - "IngressController Not Configured for Path-Based Routing", + "\n".join( + [ + "IngressController Not Found", "", "========================================================================", - f"IngressController '{ingressControllerName}' exists but is not properly configured", - "for path-based routing.", + f"You selected IngressController '{ingressControllerName}', but it does not exist", + "in the openshift-ingress-operator namespace.", "", - "Required Configuration:", - " spec:", - " routeAdmission:", - " namespaceOwnership: InterNamespaceAllowed", + "To fix this issue:", "", - "To fix this issue, you have two options:", + "1. List available IngressControllers:", + " oc get ingresscontroller -n openshift-ingress-operator", "", - "1. Add the --configure-ingress flag to configure it during installation:", - f" (Optionally, you can provide your custom IngressController name instead of {ingressControllerName} )", - f" mas install --routing path --ingress-controller-name {ingressControllerName} --configure-ingress ...", + "2. Use an existing controller name with --ingress-controller-name flag:", + " mas install --routing path --ingress-controller-name [existing-controller] ...", "", - "2. Manually configure it before installation by running:", - f" oc patch ingresscontroller {ingressControllerName} -n openshift-ingress-operator \\", - " --type=merge \\", - " --patch='{\"spec\":{\"routeAdmission\":{\"namespaceOwnership\":\"InterNamespaceAllowed\"}}}'", + "3. Or use the default controller (usually named 'default'):", + " mas install --routing path --ingress-controller-name default ...", "", "Alternatively, you can use subdomain routing mode:", - " mas install --routing subdomain ..." - ]) + " mas install --routing subdomain ...", + ] + ) + ) + elif not isConfigured: + if ( + hasattr(self.args, "mas_configure_ingress") and + self.args.mas_configure_ingress + ): + logger.info( + f"IngressController '{ingressControllerName}' will be configured for path-based routing before MAS installation" + ) + self.setParam("mas_configure_ingress", "true") + else: + self.fatalError( + "\n".join( + [ + "IngressController Not Configured for Path-Based Routing", + "", + "========================================================================", + f"IngressController '{ingressControllerName}' exists but is not properly configured", + "for path-based routing.", + "", + "Required Configuration:", + " spec:", + " routeAdmission:", + " namespaceOwnership: InterNamespaceAllowed", + "", + "To fix this issue, you have two options:", + "", + "1. Add the --configure-ingress flag to configure it during installation:", + f" (Optionally, you can provide your custom IngressController name instead of {ingressControllerName} )", + f" mas install --routing path --ingress-controller-name {ingressControllerName} --configure-ingress ...", + "", + "2. Manually configure it before installation by running:", + f" oc patch ingresscontroller {ingressControllerName} -n openshift-ingress-operator \\", + " --type=merge \\", + ' --patch=\'{"spec":{"routeAdmission":{"namespaceOwnership":"InterNamespaceAllowed"}}}\'', + "", + "Alternatively, you can use subdomain routing mode:", + " mas install --routing subdomain ...", + ] + ) ) else: - logger.info(f"IngressController '{ingressControllerName}' is already configured for path-based routing") + logger.info( + f"IngressController '{ingressControllerName}' is already configured for path-based routing" + ) # Based on the parameters set the annotations correctly self.configAnnotations() @@ -2380,9 +2948,11 @@ def install(self, argv): continueWithInstall = True 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" - ]) + 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") # Prepare the namespace and launch the installation pipeline @@ -2392,49 +2962,95 @@ def install(self, argv): self.printH1("Launch Install") pipelinesNamespace = f"mas-{self.getParam('mas_instance_id')}-pipelines" - with Halo(text='Validating OpenShift Pipelines installation', spinner=self.spinner) as h: - if installOpenShiftPipelines(self.dynamicClient, customStorageClassName=self.getParam("storage_class_rwx")): - h.stop_and_persist(symbol=self.successIcon, text="OpenShift Pipelines Operator is installed and ready to use") + with Halo( + text="Validating OpenShift Pipelines installation", spinner=self.spinner + ) as h: + if installOpenShiftPipelines( + self.dynamicClient, + customStorageClassName=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") + h.stop_and_persist( + symbol=self.successIcon, + text="OpenShift Pipelines Operator installation failed", + ) self.fatalError("Installation failed") if self.applyPreInstallMASRBAC: - with Halo(text='Applying pre-install MAS RBAC', spinner=self.spinner) as h: + with Halo( + text="Applying pre-install MAS RBAC", spinner=self.spinner + ) as h: applyPreInstallMASRBAC( dynClient=self.dynamicClient, - masVersion=".".join(self.getParam("mas_channel").split(".")[:2]), + masVersion=".".join( + self.getParam("mas_channel").split(".")[:2] + ), masInstanceId=self.getParam("mas_instance_id"), permissionMode=self.getParam("mas_permission_mode"), - selectedApps=self.getSelectedApps() + selectedApps=self.getSelectedApps(), + ) + h.stop_and_persist( + symbol=self.successIcon, text="Pre-install MAS RBAC applied" ) - h.stop_and_persist(symbol=self.successIcon, text="Pre-install MAS RBAC applied") # Enable console plugin for OCP 4.21+ - with Halo(text='Enabling Pipelines console plugin', spinner=self.spinner) as h: + with Halo( + text="Enabling Pipelines console plugin", spinner=self.spinner + ) as h: if enablePipelinesConsolePlugin(self.dynamicClient): - h.stop_and_persist(symbol=self.successIcon, text="Pipelines console plugin enabled") + h.stop_and_persist( + symbol=self.successIcon, text="Pipelines console plugin enabled" + ) else: - h.stop_and_persist(symbol=self.warningIcon, text="Failed to enable Pipelines console plugin (non-fatal)") + h.stop_and_persist( + symbol=self.warningIcon, + text="Failed to enable Pipelines console plugin (non-fatal)", + ) # Note: This is non-fatal as the plugin can be enabled manually - if self.getParam("mas_routing_mode") == "path" and self.getParam("mas_configure_ingress") == "true": - with Halo(text='Configuring cluster for path-based routing', spinner=self.spinner) as h: - ingressControllerName = self.getParam("mas_ingress_controller_name") if self.getParam("mas_ingress_controller_name") else "default" - if configureIngressForPathBasedRouting(self.dynamicClient, ingressControllerName): - h.stop_and_persist(symbol=self.successIcon, text="Cluster configured for path-based routing") + if ( + self.getParam("mas_routing_mode") == "path" and + self.getParam("mas_configure_ingress") == "true" + ): + with Halo( + text="Configuring cluster for path-based routing", + spinner=self.spinner, + ) as h: + ingressControllerName = ( + self.getParam("mas_ingress_controller_name") + if self.getParam("mas_ingress_controller_name") + else "default" + ) + if configureIngressForPathBasedRouting( + self.dynamicClient, ingressControllerName + ): + h.stop_and_persist( + symbol=self.successIcon, + text="Cluster configured for path-based routing", + ) else: - h.stop_and_persist(symbol=self.failureIcon, text="Failed to configure cluster for path-based routing") - self.fatalError("Installation failed - unable to configure IngressController for path-based routing") + h.stop_and_persist( + symbol=self.failureIcon, + text="Failed to configure cluster for path-based routing", + ) + self.fatalError( + "Installation failed - unable to configure IngressController for path-based routing" + ) - with Halo(text=f'Preparing namespace ({pipelinesNamespace})', spinner=self.spinner) as h: + with Halo( + text=f"Preparing namespace ({pipelinesNamespace})", spinner=self.spinner + ) as h: createNamespace(self.dynamicClient, pipelinesNamespace) preparePipelinesNamespace( dynClient=self.dynamicClient, instanceId=self.getParam("mas_instance_id"), storageClass=self.pipelineStorageClass, accessMode=self.pipelineStorageAccessMode, - configureRBAC=(self.getParam("service_account_name") == "") + configureRBAC=(self.getParam("service_account_name") == ""), ) prepareInstallSecrets( dynClient=self.dynamicClient, @@ -2446,28 +3062,56 @@ def install(self, argv): certs=self.certsSecret, aiserviceConfig=self.aiserviceConfigSecret, slack_token=self.getParam("slack_token"), - slack_channel=self.getParam("slack_channel") + slack_channel=self.getParam("slack_channel"), ) self.setupApprovals(pipelinesNamespace) - h.stop_and_persist(symbol=self.successIcon, text=f"Namespace is ready ({pipelinesNamespace})") + 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: + 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") + 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: + with Halo( + text=f"Installing latest Tekton definitions (v{self.version})", + spinner=self.spinner, + ) as h: updateTektonDefinitions(pipelinesNamespace, self.tektonDefsPath) - h.stop_and_persist(symbol=self.successIcon, text=f"Latest Tekton definitions are installed (v{self.version})") + 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')} install", spinner=self.spinner) as h: - pipelineURL = launchInstallPipeline(dynClient=self.dynamicClient, params=self.params) + with Halo( + text=f"Submitting PipelineRun for {self.getParam('mas_instance_id')} install", + spinner=self.spinner, + ) as h: + pipelineURL = launchInstallPipeline( + 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")) + 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") + 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() @logMethodCall @@ -2480,9 +3124,20 @@ def setupApprovals(self, namespace: str) -> None: for approval in self.approvals.values(): if "maxRetries" in approval: # Enable this approval workload - logger.debug(f"Approval workflow for {approval['id']} will be enabled during install ({approval['maxRetries']} / {approval['retryDelay']}s / {approval['ignoreFailure']})") - self.initializeApprovalConfigMap(namespace, approval['id'], True, approval['maxRetries'], approval['retryDelay'], approval['ignoreFailure']) + logger.debug( + f"Approval workflow for {approval['id']} will be enabled during install ({approval['maxRetries']} / {approval['retryDelay']}s / {approval['ignoreFailure']})" + ) + self.initializeApprovalConfigMap( + namespace, + approval["id"], + True, + approval["maxRetries"], + approval["retryDelay"], + approval["ignoreFailure"], + ) else: # Disable this approval workload - logger.debug(f"Approval workflow for {approval['id']} will be disabled during install") - self.initializeApprovalConfigMap(namespace, approval['id'], False) + logger.debug( + f"Approval workflow for {approval['id']} will be disabled during install" + ) + self.initializeApprovalConfigMap(namespace, approval["id"], False) diff --git a/python/src/mas/cli/install/argBuilder.py b/python/src/mas/cli/install/argBuilder.py index ffd8a4ad0d8..3d1231f1f44 100644 --- a/python/src/mas/cli/install/argBuilder.py +++ b/python/src/mas/cli/install/argBuilder.py @@ -13,43 +13,45 @@ logger = logging.getLogger(__name__) -class installArgBuilderMixin(): +class installArgBuilderMixin: def buildCommand(self) -> str: # MAS Catalog Selection & Entitlement # ----------------------------------------------------------------------------- newline = " \\\n" command = "export IBM_ENTITLEMENT_KEY=x\n" - if self.getParam('ibmcloud_apikey') != "": + if self.getParam("ibmcloud_apikey") != "": command += "export IBMCLOUD_APIKEY=x\n" - if self.getParam('aws_access_key_id') != "": + if self.getParam("aws_access_key_id") != "": command += "export AWS_ACCESS_KEY_ID=x\n" - if self.getParam('secret_access_key') != "": + if self.getParam("secret_access_key") != "": command += "export SECRET_ACCESS_KEY=x\n" - if self.getParam('artifactory_username') != "": + if self.getParam("artifactory_username") != "": command += "export ARTIFACTORY_USERNAME=x\nexport ARTIFACTORY_TOKEN=x\n" - if self.getParam('mas_superuser_password') != "": + if self.getParam("mas_superuser_password") != "": command += "export SUPERUSER_PASSWORD=x\n" - if self.getParam('eck_remote_es_password') != "": + if self.getParam("eck_remote_es_password") != "": command += "export ES_PASSWORD=x\n" - if self.getParam('kafka_password') != "": + if self.getParam("kafka_password") != "": command += "export KAFKA_PASSWORD=x\n" - if self.getParam('mas_app_settings_customization_archive_password') != "": + if self.getParam("mas_app_settings_customization_archive_password") != "": command += "export CUSTOMIZATION_PASSWORD=x\n" - if self.getParam('mas_manage_encryptionsecret_crypto_key') != "": + if self.getParam("mas_manage_encryptionsecret_crypto_key") != "": command += "export CRYPTO_KEY=x\n" - if self.getParam('mas_manage_encryptionsecret_cryptox_key') != "": + if self.getParam("mas_manage_encryptionsecret_cryptox_key") != "": command += "export CRYPTOX_KEY=x\n" - if self.getParam('mas_manage_encryptionsecret_old_crypto_key') != "": + if self.getParam("mas_manage_encryptionsecret_old_crypto_key") != "": command += "export OLD_CRYPTO_KEY=x\n" - if self.getParam('mas_manage_encryptionsecret_old_cryptox_key') != "": + if self.getParam("mas_manage_encryptionsecret_old_cryptox_key") != "": command += "export OLD_CRYTPOX_KEY=x\n" - command += f"mas install --mas-catalog-version {self.getParam('mas_catalog_version')}" + command += ( + f"mas install --mas-catalog-version {self.getParam('mas_catalog_version')}" + ) - if self.getParam('mas_catalog_digest') != "": + 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}" @@ -59,109 +61,130 @@ def buildCommand(self) -> str: command += f" --mas-channel {self.getParam('mas_channel')}" command += f" --mas-instance-id {self.getParam('mas_instance_id')}" command += f" --mas-workspace-id {self.getParam('mas_workspace_id')}" - command += f" --mas-workspace-name \"{self.getParam('mas_workspace_name')}\"{newline}" + command += ( + f" --mas-workspace-name \"{self.getParam('mas_workspace_name')}\"{newline}" + ) - if self.getParam('mas_special_characters') == "true": + if self.getParam("mas_special_characters") == "true": command += f" --allow-special-chars {newline}" # ECK Integration # ----------------------------------------------------------------------------- - if self.getParam('eck_action') == "install": + if self.getParam("eck_action") == "install": command += "--eck" - if self.getParam('eck_enable_logstash') == "true": + if self.getParam("eck_enable_logstash") == "true": command += f"--eck-enable-logstash{newline}" - if self.getParam('eck_remote_es_hosts') != "": + if self.getParam("eck_remote_es_hosts") != "": command += f"--eck-remote-es-hosts \"{self.getParam('eck_remote_es_hosts')}\"{newline}" - if self.getParam('eck_remote_es_username') != "": + if self.getParam("eck_remote_es_username") != "": command += f"--eck-remote-es-username \"{self.getParam('eck_remote_es_username')}\"" - if self.getParam('eck_remote_es_password') != "": + if self.getParam("eck_remote_es_password") != "": command += f"--eck-remote-es-password $ES_PASSWORD{newline}" # MAS Advanced Configuration # ----------------------------------------------------------------------------- - if self.getParam('mas_superuser_username') != "": + if self.getParam("mas_superuser_username") != "": command += f" --mas-superuser-username \"{self.getParam('mas_superuser_username')}\"" - if self.getParam('mas_superuser_password') != "": + if self.getParam("mas_superuser_password") != "": command += f" --mas-superuser-password $SUPERUSER_PASSWORD{newline}" if self.localConfigDir is not None: - command += f" --additional-configs \"{self.localConfigDir}\"{newline}" - if self.getParam('pod_templates') != "": - command += f" --pod-templates \"{self.getParam('pod_templates')}\"{newline}" + command += f' --additional-configs "{self.localConfigDir}"{newline}' + if self.getParam("pod_templates") != "": + command += ( + f" --pod-templates \"{self.getParam('pod_templates')}\"{newline}" + ) if self.operationalMode == 2: command += f" --non-prod{newline}" - if self.getParam('mas_permission_mode') != "": - command += f" --permission-mode {self.getParam('mas_permission_mode')}{newline}" + if self.getParam("mas_permission_mode") != "": + command += ( + f" --permission-mode {self.getParam('mas_permission_mode')}{newline}" + ) - if self.getParam('mas_trust_default_cas').lower() == "false": + if self.getParam("mas_trust_default_cas").lower() == "false": command += f" --disable-ca-trust{newline}" - if self.getParam('mas_manual_cert_mgmt').lower() == "true": - command += f" --manual-certificates \"{self.manualCertsDir}\"{newline}" + if self.getParam("mas_manual_cert_mgmt").lower() == "true": + command += f' --manual-certificates "{self.manualCertsDir}"{newline}' - if self.getParam('mas_routing_mode') != "": + if self.getParam("mas_routing_mode") != "": command += f" --routing \"{self.getParam('mas_routing_mode')}\"{newline}" - if self.getParam('mas_ingress_controller_name') != "": + if self.getParam("mas_ingress_controller_name") != "": command += f" --ingress-controller \"{self.getParam('mas_ingress_controller_name')}\"{newline}" - if self.getParam('mas_configure_ingress').lower() == "true": + if self.getParam("mas_configure_ingress").lower() == "true": command += f" --configure-ingress{newline}" - if self.getParam('mas_use_service_mesh') != "": - command += f" --servicemesh \"{self.getParam('mas_use_service_mesh')}\"{newline}" + if self.getParam("mas_use_service_mesh") != "": + command += ( + f" --servicemesh \"{self.getParam('mas_use_service_mesh')}\"{newline}" + ) - if self.getParam('mas_domain') != "": + if self.getParam("mas_domain") != "": command += f" --domain \"{self.getParam('mas_domain')}\"{newline}" - if self.getParam('dns_provider') == "cis": - command += f" --dns-provider cis --cis-apikey \"{self.getParam('cis_apikey')}\"" + if self.getParam("dns_provider") == "cis": + command += ( + f" --dns-provider cis --cis-apikey \"{self.getParam('cis_apikey')}\"" + ) command += f" --cis-subdomain \"{self.getParam('cis_subdomain')}\"" command += f" --cis-crn \"{self.getParam('cis_crn')}\"" command += f" --cis-email \"{self.getParam('cis_email')}\"{newline}" - if self.getParam('dns_provider') == "cloudflare": + if self.getParam("dns_provider") == "cloudflare": command += f" --dns-provider cloudflare --cloudflare-apitoken \"{self.getParam('cloudflare_apitoken')}\"{newline}" - command += f" --cloudflare-email \"{self.getParam('cloudflare_email')}\"{newline}" - command += f" --cloudflare-zone \"{self.getParam('cloudflare_zone')}\"{newline}" + command += ( + f" --cloudflare-email \"{self.getParam('cloudflare_email')}\"{newline}" + ) + command += ( + f" --cloudflare-zone \"{self.getParam('cloudflare_zone')}\"{newline}" + ) command += f" --cloudflare-subdomain \"{self.getParam('cloudflare_subdomain')}\"{newline}" - if self.getParam('mas_cluster_issuer') != "": + if self.getParam("mas_cluster_issuer") != "": command += f" --mas-cluster-issuer \"{self.getParam('mas_cluster_issuer')}\"{newline}" - if self.getParam('mas_issuer_kind') != "": - command += f" --mas-issuer-kind \"{self.getParam('mas_issuer_kind')}\"{newline}" + if self.getParam("mas_issuer_kind") != "": + command += ( + f" --mas-issuer-kind \"{self.getParam('mas_issuer_kind')}\"{newline}" + ) - if self.getParam('mas_enable_walkme').lower() == "false": + if self.getParam("mas_enable_walkme").lower() == "false": command += f" --disable-walkme{newline}" - if self.getParam('mas_feature_usage').lower() == "false": + if self.getParam("mas_feature_usage").lower() == "false": command += f" --disable-feature-usage{newline}" - if self.getParam('mas_usability_metrics').lower() == "false": + if self.getParam("mas_usability_metrics").lower() == "false": command += f" --disable-usability-metrics{newline}" - if self.getParam('mas_deployment_progression').lower() == "false": + if self.getParam("mas_deployment_progression").lower() == "false": command += f" --disable-deployment-progression{newline}" - if self.getParam('enable_ipv6').lower() == "true": + if self.getParam("enable_ipv6").lower() == "true": command += f" --enable-ipv6{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}" + 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 # ----------------------------------------------------------------------------- addedSLSCmd = False if self.getParam("sls_namespace") != "ibm-sls": if self.getParam("mas_instance_id") != "": - if self.getParam("sls_namespace") == f"mas-{self.getParam('mas_instance_id')}-sls": + if ( + self.getParam("sls_namespace") == + f"mas-{self.getParam('mas_instance_id')}-sls" + ): command += " --dedicated-sls" addedSLSCmd = True else: @@ -171,7 +194,7 @@ def buildCommand(self) -> str: command += f" --sls-channel \"{self.getParam('sls_channel')}\"" addedSLSCmd = True if self.slsLicenseFileLocal: - command += f" --license-file \"{self.slsLicenseFileLocal}\"" + command += f' --license-file "{self.slsLicenseFileLocal}"' addedSLSCmd = True if addedSLSCmd is True: command += newline @@ -180,29 +203,33 @@ def buildCommand(self) -> str: # ----------------------------------------------------------------------------- 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}" + command += ( + f" --contact-lastname \"{self.getParam('dro_contact_lastname')}\"{newline}" + ) + if self.getParam("dro_namespace") != "": + command += ( + f" --dro-namespace \"{self.getParam('dro_namespace')}\"{newline}" + ) # MongoDb Community Operator # ----------------------------------------------------------------------------- - if self.getParam('mongodb_namespace') != "": + if self.getParam("mongodb_namespace") != "": command += f" --mongodb-namespace \"{self.getParam('mongodb_namespace')}\"{newline}" # OCP Configuration # ----------------------------------------------------------------------------- - if self.getParam('ocp_ingress_tls_secret_name') != "": + if self.getParam("ocp_ingress_tls_secret_name") != "": command += f" --ocp-ingress-tls-secret-name \"{self.getParam('ocp_ingress_tls_secret_name')}\"{newline}" - if self.getParam('ocp_ingress') != "": + if self.getParam("ocp_ingress") != "": command += f" --ocp-ingress \"{self.getParam('ocp_ingress')}\"{newline}" # Grafana # ----------------------------------------------------------------------------- - if self.getParam('skip_grafana_install') is True: + if self.getParam("skip_grafana_install") is True: command += f" --skip-grafana-install{newline}" - if self.getParam('grafana_v5_namespace') != "": + if self.getParam("grafana_v5_namespace") != "": command += f" --grafana-v5-namespace \"{self.getParam('grafana_v5_namespace')}\"{newline}" - if self.getParam('grafana_instance_storage_size') != "": + if self.getParam("grafana_instance_storage_size") != "": command += f" --grafana-instance-storage-size \"{self.getParam('grafana_instance_storage_size')}\"{newline}" # MAS Applications @@ -210,7 +237,9 @@ def buildCommand(self) -> str: if self.installAssist: command += f" --assist-channel \"{self.getParam('mas_app_channel_assist')}\"{newline}" if self.installIoT: - command += f" --iot-channel \"{self.getParam('mas_app_channel_iot')}\"{newline}" + command += ( + f" --iot-channel \"{self.getParam('mas_app_channel_iot')}\"{newline}" + ) if self.installMonitor: command += f" --monitor-channel \"{self.getParam('mas_app_channel_monitor')}\"{newline}" if self.installManage: @@ -226,391 +255,464 @@ def buildCommand(self) -> str: command += f" --facilities-channel \"{self.getParam('mas_app_channel_facilities')}\"{newline}" if self.installAIService: command += f" --aiservice-channel \"{self.getParam('aiservice_channel')}\"{newline}" - if self.getParam('configure_aiassistant') != "": + if self.getParam("configure_aiassistant") != "": command += f" --configure-aiassistant \"{self.getParam('configure_aiassistant')}\"{newline}" # Arcgis # ----------------------------------------------------------------------------- if self.installArcgis: - command += f" --arcgis-channel \"{self.getParam('mas_arcgis_channel')}\"{newline}" + command += ( + f" --arcgis-channel \"{self.getParam('mas_arcgis_channel')}\"{newline}" + ) # Manage Advanced Settings # ----------------------------------------------------------------------------- if self.installManage: command += f" --manage-jdbc \"{self.getParam('mas_appws_bindings_jdbc_manage')}\"{newline}" + if self.getParam("mas_appws_bindings_kafka_manage") != "": + command += f" --manage-kafka \"{self.getParam('mas_appws_bindings_kafka_manage')}\"{newline}" command += f" --manage-components \"{self.getParam('mas_appws_components')}\"{newline}" - if self.getParam('mas_app_settings_server_bundles_size') != "": + if self.getParam("mas_app_settings_server_bundles_size") != "": command += f" --manage-server-bundle-size \"{self.getParam('mas_app_settings_server_bundles_size')}\"{newline}" - if self.getParam('mas_app_settings_default_jms') != "": + if self.getParam("mas_app_settings_default_jms") != "": command += f" --manage-jms {newline}" - if self.getParam('mas_app_settings_persistent_volumes_flag') == "true": + if self.getParam("mas_app_settings_persistent_volumes_flag") == "true": command += f" --manage-persistent-volumes{newline}" - if self.getParam('mas_app_settings_demodata') == "true": + if self.getParam("mas_app_settings_demodata") == "true": command += f" --manage-demodata{newline}" - if self.getParam('mas_app_settings_customization_archive_name') != "": + if self.getParam("mas_app_settings_customization_archive_name") != "": command += f" --manage-customization-archive-name \"{self.getParam('mas_app_settings_customization_archive_name')}\"{newline}" - if self.getParam('mas_app_settings_customization_archive_url') != "": + if self.getParam("mas_app_settings_customization_archive_url") != "": command += f" --manage-customization-archive-url \"{self.getParam('mas_app_settings_customization_archive_url')}\"{newline}" - if self.getParam('mas_app_settings_customization_archive_username') != "": + if self.getParam("mas_app_settings_customization_archive_username") != "": command += f" --manage-customization-archive-username \"{self.getParam('mas_app_settings_customization_archive_username')}\"{newline}" - if self.getParam('mas_app_settings_customization_archive_password') != "": + if self.getParam("mas_app_settings_customization_archive_password") != "": command += f" --manage-customization-archive-password $CUSTOMIZATION_PASSWORD{newline}" - if self.getParam('mas_app_settings_tablespace') != "": + if self.getParam("mas_app_settings_tablespace") != "": command += f" --manage-db-tablespace \"{self.getParam('mas_app_settings_tablespace')}\"{newline}" - if self.getParam('mas_app_settings_indexspace') != "": + if self.getParam("mas_app_settings_indexspace") != "": command += f" --manage-db-indexspace \"{self.getParam('mas_app_settings_indexspace')}\"{newline}" - if self.getParam('mas_app_settings_db2_schema') != "": + if self.getParam("mas_app_settings_db2_schema") != "": command += f" --manage-db-schema \"{self.getParam('mas_app_settings_db2_schema')}\"{newline}" - if self.getParam('mas_manage_encryptionsecret_crypto_key') != "": + if self.getParam("mas_manage_encryptionsecret_crypto_key") != "": command += f" --manage-crypto-key $CRYPTO_KEY{newline}" - if self.getParam('mas_manage_encryptionsecret_cryptox_key') != "": + if self.getParam("mas_manage_encryptionsecret_cryptox_key") != "": command += f" --manage-cryptox-key $CRYPTOX_KEY{newline}" - if self.getParam('mas_manage_encryptionsecret_old_crypto_key') != "": + if self.getParam("mas_manage_encryptionsecret_old_crypto_key") != "": command += f" --manage-old-crypto-key $OLD_CRYPTO_KEY{newline}" - if self.getParam('mas_manage_encryptionsecret_old_cryptox_key') != "": + if self.getParam("mas_manage_encryptionsecret_old_cryptox_key") != "": command += f" --manage-old-cryptox-key $OLD_CRYPTOX_KEY{newline}" - if self.getParam('mas_manage_ws_db_encryptionsecret') != "": + if self.getParam("mas_manage_ws_db_encryptionsecret") != "": command += f" --manage-encryption-secret-name \"{self.getParam('mas_manage_ws_db_encryptionsecret')}\"{newline}" - if self.getParam('mas_app_settings_base_lang') != "": + if self.getParam("mas_app_settings_base_lang") != "": command += f" --manage-base-language \"{self.getParam('mas_app_settings_base_lang')}\"{newline}" - if self.getParam('mas_app_settings_secondary_langs') != "": + if self.getParam("mas_app_settings_secondary_langs") != "": command += f" --manage-secondary-languages \"{self.getParam('mas_app_settings_secondary_langs')}\"{newline}" - if self.getParam('mas_app_settings_server_timezone') != "": + if self.getParam("mas_app_settings_server_timezone") != "": command += f" --manage-server-timezone \"{self.getParam('mas_app_settings_server_timezone')}\"{newline}" - if self.getParam('mas_manage_attachments_provider') != "": + if self.getParam("mas_manage_attachments_provider") != "": command += f" --manage-attachments-provider \"{self.getParam('mas_manage_attachments_provider')}\"{newline}" - if self.getParam('mas_manage_attachment_configuration_mode') != "": + if self.getParam("mas_manage_attachment_configuration_mode") != "": command += f" --manage-attachments-mode \"{self.getParam('mas_manage_attachment_configuration_mode')}\"{newline}" - if self.getParam('mas_appws_bindings_health_wsl_flag') == "true": + if self.getParam("mas_appws_bindings_health_wsl_flag") == "true": command += f" --manage-health-wsl{newline}" - if self.getParam('mas_appws_upgrade_type') == "true": + if self.getParam("mas_appws_upgrade_type") == "true": command += f" --manage-upgrade-type \"{self.getParam('mas_appws_upgrade_type')}\"{newline}" - if self.getParam('manage_bind_aiservice_instance_id') != "": + if self.getParam("manage_bind_aiservice_instance_id") != "": command += f" --manage-aiservice-instance-id \"{self.getParam('manage_bind_aiservice_instance_id')}\"{newline}" - if self.getParam('manage_bind_aiservice_tenant_id') != "": + if self.getParam("manage_bind_aiservice_tenant_id") != "": command += f" --manage-aiservice-tenant-id \"{self.getParam('manage_bind_aiservice_tenant_id')}\"{newline}" # Facilities Advanced Settings # ----------------------------------------------------------------------------- # TODO: Fix type for storage sizes and max conn pool size if self.installFacilities: - if self.getParam('mas_ws_facilities_size') != "": + if self.getParam("mas_ws_facilities_size") != "": command += f" --facilities-size \"{self.getParam('mas_ws_facilities_size')}\"{newline}" - if self.getParam('mas_ws_facilities_app_om_upgrade_mode') != "": + if self.getParam("mas_ws_facilities_app_om_upgrade_mode") != "": command += f" --facilities-app-om-upgrade-mode \"{self.getParam('mas_ws_facilities_app_om_upgrade_mode')}\"{newline}" - if self.getParam('mas_ws_facilities_pull_policy') != "": + if self.getParam("mas_ws_facilities_pull_policy") != "": command += f" --facilities-pull-policy \"{self.getParam('mas_ws_facilities_pull_policy')}\"{newline}" - if self.getParam('mas_ws_facilities_routes_timeout') != "": + if self.getParam("mas_ws_facilities_routes_timeout") != "": command += f" --facilities-routes-timeout \"{self.getParam('mas_ws_facilities_routes_timeout')}\"{newline}" - if self.getParam('mas_ws_facilities_liberty_extension_XML') != "": + if self.getParam("mas_ws_facilities_liberty_extension_XML") != "": command += f" --facilities-xml-extension \"{self.getParam('mas_ws_facilities_liberty_extension_XML')}\"{newline}" - if self.getParam('mas_ws_facilities_vault_secret') != "": + if self.getParam("mas_ws_facilities_vault_secret") != "": command += f" --facilities-vault-secret \"{self.getParam('mas_ws_facilities_vault_secret')}\"{newline}" - if self.getParam('mas_ws_facilities_dwfagents') != "": - command += f" --facilities-dwfagent \'{self.getParam('mas_ws_facilities_dwfagents')}\'{newline}" + if self.getParam("mas_ws_facilities_dwfagents") != "": + command += f" --facilities-dwfagent '{self.getParam('mas_ws_facilities_dwfagents')}'{newline}" - if self.getParam('mas_ws_facilities_db_maxconnpoolsize') != "": + if self.getParam("mas_ws_facilities_db_maxconnpoolsize") != "": command += f" --facilities-maxconnpoolsize \"{self.getParam('mas_ws_facilities_db_maxconnpoolsize')}\"{newline}" - if self.getParam('mas_ws_facilities_storage_log_class') != "": + if self.getParam("mas_ws_facilities_storage_log_class") != "": command += f" --facilities-log-storage-class \"{self.getParam('mas_ws_facilities_storage_log_class')}\"{newline}" - if self.getParam('mas_ws_facilities_storage_log_mode') != "": + if self.getParam("mas_ws_facilities_storage_log_mode") != "": command += f" --facilities-log-storage-mode \"{self.getParam('mas_ws_facilities_storage_log_mode')}\"{newline}" - if self.getParam('mas_ws_facilities_storage_log_size') != "": + if self.getParam("mas_ws_facilities_storage_log_size") != "": command += f" --facilities-log-storage-size \"{self.getParam('mas_ws_facilities_storage_log_size')}\"{newline}" - if self.getParam('mas_ws_facilities_storage_userfiles_class') != "": + if self.getParam("mas_ws_facilities_storage_userfiles_class") != "": command += f" --facilities-userfiles-storage-class \"{self.getParam('mas_ws_facilities_storage_userfiles_class')}\"{newline}" - if self.getParam('mas_ws_facilities_storage_userfiles_mode') != "": + if self.getParam("mas_ws_facilities_storage_userfiles_mode") != "": command += f" --facilities-userfiles-storage-mode \"{self.getParam('mas_ws_facilities_storage_userfiles_mode')}\"{newline}" - if self.getParam('mas_ws_facilities_storage_userfiles_size') != "": + if self.getParam("mas_ws_facilities_storage_userfiles_size") != "": command += f" --facilities-userfiles-storage-size \"{self.getParam('mas_ws_facilities_storage_userfiles_size')}\"{newline}" # AI Service Advanced Settings # ----------------------------------------------------------------------------- if self.installAIService: - if self.getParam('aiservice_instance_id') != "": + if self.getParam("aiservice_instance_id") != "": command += f" --aiservice-instance-id \"{self.getParam('aiservice_instance_id')}\"{newline}" - if self.getParam('aiservice_channel') != "": + if self.getParam("aiservice_channel") != "": command += f" --aiservice-channel \"{self.getParam('aiservice_channel')}\"{newline}" # Certificate Issuer for AI Service - if self.getParam('aiservice_certificate_issuer') != "": + if self.getParam("aiservice_certificate_issuer") != "": command += f" --aiservice-certificate-issuer \"{self.getParam('aiservice_certificate_issuer')}\"{newline}" - if self.getParam('aiservice_s3_accesskey') != "" and self.getParam('minio_root_user') == "": + if ( + self.getParam("aiservice_s3_accesskey") != "" and + self.getParam("minio_root_user") == "" + ): command += f" --s3-accesskey \"{self.getParam('aiservice_s3_accesskey')}\"{newline}" - if self.getParam('aiservice_s3_secretkey') != "" and self.getParam('minio_root_user') == "": + if ( + self.getParam("aiservice_s3_secretkey") != "" and + self.getParam("minio_root_user") == "" + ): command += f" --s3-secretkey \"{self.getParam('aiservice_s3_secretkey')}\"{newline}" - if self.getParam('aiservice_s3_host') != "" and self.getParam('minio_root_user') == "": - command += f" --s3-host \"{self.getParam('aiservice_s3_host')}\"{newline}" - if self.getParam('aiservice_s3_port') != "" and self.getParam('minio_root_user') == "": - command += f" --s3-port \"{self.getParam('aiservice_s3_port')}\"{newline}" - if self.getParam('aiservice_s3_ssl') != "" and self.getParam('minio_root_user') == "": - command += f" --s3-ssl \"{self.getParam('aiservice_s3_ssl')}\"{newline}" - if self.getParam('aiservice_s3_region') != "" and self.getParam('minio_root_user') == "": - command += f" --s3-region \"{self.getParam('aiservice_s3_region')}\"{newline}" - if self.getParam('aiservice_s3_bucket_prefix') != "" and self.getParam('minio_root_user') == "": + if ( + self.getParam("aiservice_s3_host") != "" and + self.getParam("minio_root_user") == "" + ): + command += ( + f" --s3-host \"{self.getParam('aiservice_s3_host')}\"{newline}" + ) + if ( + self.getParam("aiservice_s3_port") != "" and + self.getParam("minio_root_user") == "" + ): + command += ( + f" --s3-port \"{self.getParam('aiservice_s3_port')}\"{newline}" + ) + if ( + self.getParam("aiservice_s3_ssl") != "" and + self.getParam("minio_root_user") == "" + ): + command += ( + f" --s3-ssl \"{self.getParam('aiservice_s3_ssl')}\"{newline}" + ) + if ( + self.getParam("aiservice_s3_region") != "" and + self.getParam("minio_root_user") == "" + ): + command += ( + f" --s3-region \"{self.getParam('aiservice_s3_region')}\"{newline}" + ) + if ( + self.getParam("aiservice_s3_bucket_prefix") != "" and + self.getParam("minio_root_user") == "" + ): command += f" --s3-bucket-prefix \"{self.getParam('aiservice_s3_bucket_prefix')}\"{newline}" - if self.getParam('aiservice_s3_tenants_bucket') != "": + if self.getParam("aiservice_s3_tenants_bucket") != "": command += f" --s3-tenants-bucket \"{self.getParam('aiservice_s3_tenants_bucket')}\"{newline}" - if self.getParam('aiservice_s3_templates_bucket') != "": + if self.getParam("aiservice_s3_templates_bucket") != "": command += f" --s3-templates-bucket \"{self.getParam('aiservice_s3_templates_bucket')}\"{newline}" - if self.getParam('aiservice_odh_model_deployment_type') != "": + if self.getParam("aiservice_odh_model_deployment_type") != "": command += f" --odh-model-deployment-type \"{self.getParam('aiservice_odh_model_deployment_type')}\"{newline}" - if self.getParam('aiservice_rhoai_model_deployment_type') != "": + if self.getParam("aiservice_rhoai_model_deployment_type") != "": command += f" --rhoai-model-deployment-type \"{self.getParam('aiservice_rhoai_model_deployment_type')}\"{newline}" - if self.getParam('rhoai') == "true": + if self.getParam("rhoai") == "true": command += f" --rhoai{newline}" - if self.getParam('mas_app_settings_persistent_volumes_flag') == "true": + if self.getParam("mas_app_settings_persistent_volumes_flag") == "true": command += f" --manage-persistent-volumes{newline}" - if self.getParam('aiservice_watsonxai_apikey') != "": + if self.getParam("aiservice_watsonxai_apikey") != "": command += f" --watsonxai-apikey \"{self.getParam('aiservice_watsonxai_apikey')}\"{newline}" - if self.getParam('aiservice_watsonxai_url') != "": + if self.getParam("aiservice_watsonxai_url") != "": command += f" --watsonxai-url \"{self.getParam('aiservice_watsonxai_url')}\"{newline}" - if self.getParam('aiservice_watsonxai_project_id') != "": + if self.getParam("aiservice_watsonxai_project_id") != "": command += f" --watsonxai-project-id \"{self.getParam('aiservice_watsonxai_project_id')}\"{newline}" - if self.getParam('aiservice_watsonx_action') != "": + if self.getParam("aiservice_watsonx_action") != "": command += f" --watsonx-action \"{self.getParam('aiservice_watsonx_action')}\"{newline}" - if self.getParam('aiservice_watsonxai_ca_crt') != "": + if self.getParam("aiservice_watsonxai_ca_crt") != "": command += f" --watsonxai-ca-crt \"{self.getParam('aiservice_watsonxai_ca_crt')}\"{newline}" - if self.getParam('aiservice_watsonxai_deployment_id') != "": + if self.getParam("aiservice_watsonxai_deployment_id") != "": command += f" --watsonxai-deployment-id \"{self.getParam('aiservice_watsonxai_deployment_id')}\"{newline}" - if self.getParam('aiservice_watsonxai_space_id') != "": + if self.getParam("aiservice_watsonxai_space_id") != "": command += f" --watsonxai-space-id \"{self.getParam('aiservice_watsonxai_space_id')}\"{newline}" - if self.getParam('aiservice_watsonxai_instance_id') != "": + if self.getParam("aiservice_watsonxai_instance_id") != "": command += f" --watsonxai-instance-id \"{self.getParam('aiservice_watsonxai_instance_id')}\"{newline}" - if self.getParam('aiservice_watsonxai_username') != "": + if self.getParam("aiservice_watsonxai_username") != "": command += f" --watsonxai-username \"{self.getParam('aiservice_watsonxai_username')}\"{newline}" - if self.getParam('aiservice_watsonxai_version') != "": + if self.getParam("aiservice_watsonxai_version") != "": command += f" --watsonxai-version \"{self.getParam('aiservice_watsonxai_version')}\"{newline}" - if self.getParam('aiservice_watsonxai_on_prem') != "": + if self.getParam("aiservice_watsonxai_on_prem") != "": command += f" --watsonxai-onprem \"{self.getParam('aiservice_watsonxai_on_prem')}\"{newline}" - if self.getParam('minio_root_user') != "": + if self.getParam("minio_root_user") != "": command += f" --install-minio {newline}" command += f" --minio-root-user \"{self.getParam('minio_root_user')}\"{newline}" - if self.getParam('minio_root_password') != "": + if self.getParam("minio_root_password") != "": command += f" --minio-root-password \"{self.getParam('minio_root_password')}\"{newline}" - if self.getParam('tenant_entitlement_type') != "": + if self.getParam("tenant_entitlement_type") != "": command += f" --tenant-entitlement-type \"{self.getParam('tenant_entitlement_type')}\"{newline}" - if self.getParam('tenant_entitlement_start_date') != "": + if self.getParam("tenant_entitlement_start_date") != "": command += f" --tenant-entitlement-start-date \"{self.getParam('tenant_entitlement_start_date')}\"{newline}" - if self.getParam('tenant_entitlement_end_date') != "": + if self.getParam("tenant_entitlement_end_date") != "": command += f" --tenant-entitlement-end-date \"{self.getParam('tenant_entitlement_end_date')}\"{newline}" if self.aiserviceTenantSchedulingConfigFileLocal: - command += f" --tenant-scheduling-config-file \"{self.aiserviceTenantSchedulingConfigFileLocal}\"{newline}" + command += f' --tenant-scheduling-config-file "{self.aiserviceTenantSchedulingConfigFileLocal}"{newline}' - if self.getParam('rsl_url') != "": + if self.getParam("rsl_url") != "": command += f" --rsl-url \"{self.getParam('rsl_url')}\"{newline}" - if self.getParam('rsl_org_id') != "": + if self.getParam("rsl_org_id") != "": command += f" --rsl-org-id \"{self.getParam('rsl_org_id')}\"{newline}" - if self.getParam('rsl_token') != "": + if self.getParam("rsl_token") != "": command += f" --rsl-token \"{self.getParam('rsl_token')}\"{newline}" - if self.getParam('rsl_ca_crt') != "": + if self.getParam("rsl_ca_crt") != "": command += f" --rsl-ca-crt \"{self.getParam('rsl_ca_crt')}\"{newline}" # IBM Cloud Pak for Data # ----------------------------------------------------------------------------- - if self.getParam('cpd_product_version') != "": + if self.getParam("cpd_product_version") != "": command += f" --cp4d-version \"{self.getParam('cpd_product_version')}\"" - if self.getParam('cpd_install_cognos') == "true": + if self.getParam("cpd_install_cognos") == "true": command += " --cp4d-install-cognos" - if self.getParam('cpd_install_ws') == "true": + if self.getParam("cpd_install_ws") == "true": command += " --cp4d-install-ws" - if self.getParam('cpd_install_wml') == "true": + if self.getParam("cpd_install_wml") == "true": command += " --cp4d-install-wml" - if self.getParam('cpd_install_ae') == "true": + if self.getParam("cpd_install_ae") == "true": command += " --cp4d-install-ae" command += newline # IBM Db2 Universal Operator # ----------------------------------------------------------------------------- - if self.getParam('db2_action_system') == "install" or self.getParam('db2_action_manage') == "install" or self.getParam('db2_action_facilities') == "install": - if self.getParam('db2_action_system') == "install": + if ( + self.getParam("db2_action_system") == "install" or + self.getParam("db2_action_manage") == "install" or + self.getParam("db2_action_facilities") == "install" + ): + if self.getParam("db2_action_system") == "install": command += f" --db2-system{newline}" - if self.getParam('db2_action_manage') == "install": + if self.getParam("db2_action_manage") == "install": command += f" --db2-manage{newline}" - if self.getParam('db2_action_facilities') == "install": + if self.getParam("db2_action_facilities") == "install": command += f" --db2-facilities{newline}" - if self.getParam('db2_channel') != "": - command += f" --db2-channel \"{self.getParam('db2_channel')}\"{newline}" - if self.getParam('db2_namespace') != "": - command += f" --db2-namespace \"{self.getParam('db2_namespace')}\"{newline}" + if self.getParam("db2_channel") != "": + command += ( + f" --db2-channel \"{self.getParam('db2_channel')}\"{newline}" + ) + if self.getParam("db2_namespace") != "": + command += ( + f" --db2-namespace \"{self.getParam('db2_namespace')}\"{newline}" + ) - if self.getParam('db2_type') != "": + if self.getParam("db2_type") != "": command += f" --db2-type \"{self.getParam('db2_type')}\"{newline}" - if self.getParam('db2_timezone') != "": - command += f" --db2-timezone \"{self.getParam('db2_timezone')}\"{newline}" + if self.getParam("db2_timezone") != "": + command += ( + f" --db2-timezone \"{self.getParam('db2_timezone')}\"{newline}" + ) if self.db2LicenseFileLocal != "": - command += f" --db2-license-file \"{self.db2LicenseFileLocal}\"{newline}" + command += f' --db2-license-file "{self.db2LicenseFileLocal}"{newline}' - if self.getParam('db2_affinity_key') != "": + if self.getParam("db2_affinity_key") != "": command += f" --db2-affinity-key \"{self.getParam('db2_affinity_key')}\"{newline}" - if self.getParam('db2_affinity_value') != "": + if self.getParam("db2_affinity_value") != "": command += f" --db2-affinity-value \"{self.getParam('db2_affinity_value')}\"{newline}" - if self.getParam('db2_tolerate_key') != "": + if self.getParam("db2_tolerate_key") != "": command += f" --db2-tolerate-key \"{self.getParam('db2_tolerate_key')}\"{newline}" - if self.getParam('db2_tolerate_value') != "": + if self.getParam("db2_tolerate_value") != "": command += f" --db2-tolerate-value \"{self.getParam('db2_tolerate_value')}\"{newline}" - if self.getParam('db2_tolerate_effect') != "": + if self.getParam("db2_tolerate_effect") != "": command += f" --db2-tolerate-effect \"{self.getParam('db2_tolerate_effect')}\"{newline}" - if self.getParam('db2_cpu_requests') != "": + if self.getParam("db2_cpu_requests") != "": command += f" --db2-cpu-requests \"{self.getParam('db2_cpu_requests')}\"{newline}" - if self.getParam('db2_cpu_limits') != "": - command += f" --db2-cpu-limits \"{self.getParam('db2_cpu_limits')}\"{newline}" + if self.getParam("db2_cpu_limits") != "": + command += ( + f" --db2-cpu-limits \"{self.getParam('db2_cpu_limits')}\"{newline}" + ) - if self.getParam('db2_memory_requests') != "": + if self.getParam("db2_memory_requests") != "": command += f" --db2-memory-requests \"{self.getParam('db2_memory_requests')}\"{newline}" - if self.getParam('db2_memory_limits') != "": + if self.getParam("db2_memory_limits") != "": command += f" --db2-memory-limits \"{self.getParam('db2_memory_limits')}\"{newline}" - if self.getParam('db2_backup_storage_size') != "": + if self.getParam("db2_backup_storage_size") != "": command += f" --db2-backup-storage \"{self.getParam('db2_backup_storage_size')}\"{newline}" - if self.getParam('db2_data_storage_size') != "": + if self.getParam("db2_data_storage_size") != "": command += f" --db2-data-storage \"{self.getParam('db2_data_storage_size')}\"{newline}" - if self.getParam('db2_logs_storage_size') != "": + if self.getParam("db2_logs_storage_size") != "": command += f" --db2-logs-storage \"{self.getParam('db2_logs_storage_size')}\"{newline}" - if self.getParam('db2_meta_storage_size') != "": + if self.getParam("db2_meta_storage_size") != "": command += f" --db2-meta-storage \"{self.getParam('db2_meta_storage_size')}\"{newline}" - if self.getParam('db2_temp_storage_size') != "": + if self.getParam("db2_temp_storage_size") != "": command += f" --db2-temp-storage \"{self.getParam('db2_temp_storage_size')}\"{newline}" - if self.getParam('db2u_kind') != "": + if self.getParam("db2u_kind") != "": command += f" --db2u-kind \"{self.getParam('db2u_kind')}\"{newline}" # Kafka - Common # ----------------------------------------------------------------------------- - if self.getParam('kafka_provider') != "": - command += f" --kafka-provider \"{self.getParam('kafka_provider')}\"{newline}" - - if self.getParam('kafka_username') != "": - command += f" --kafka-username \"{self.getParam('kafka_username')}\"{newline}" - if self.getParam('kafka_password') != "": + if self.getParam("kafka_provider") != "": + command += ( + f" --kafka-provider \"{self.getParam('kafka_provider')}\"{newline}" + ) + + if self.getParam("kafka_username") != "": + command += ( + f" --kafka-username \"{self.getParam('kafka_username')}\"{newline}" + ) + if self.getParam("kafka_password") != "": command += f" --kafka-password $KAFKA_PASSWORD{newline}" # Kafka - Strimzi & AMQ Streams # ----------------------------------------------------------------------------- - if self.getParam('kafka_namespace') != "": + if self.getParam("kafka_namespace") != "": command += f" --kafka-namespace \"{self.getParam('kafka_namespace')}\"{newline}" - if self.getParam('kafka_version') != "": - command += f" --kafka-version \"{self.getParam('kafka_version')}\"{newline}" + if self.getParam("kafka_version") != "": + command += ( + f" --kafka-version \"{self.getParam('kafka_version')}\"{newline}" + ) # Kafka - MSK # ----------------------------------------------------------------------------- - if self.getParam('aws_msk_instance_type') != "": + if self.getParam("aws_msk_instance_type") != "": command += f" --msk-instance-type \"{self.getParam('aws_msk_instance_type')}\"" command += f" --msk-instance-nodes \"{self.getParam('aws_msk_instance_nodes')}\"" command += f" --msk-instance-volume-size \"{self.getParam('aws_msk_instance_volume_size')}\"{newline}" command += f" --msk-cidr-az1 \"{self.getParam('aws_msk_cidr_az1')}\"" command += f" --msk-cidr-az2 \"{self.getParam('aws_msk_cidr_az1')}\"" - command += f" --msk-cidr-az3 \"{self.getParam('aws_msk_cidr_az1')}\"{newline}" + command += ( + f" --msk-cidr-az3 \"{self.getParam('aws_msk_cidr_az1')}\"{newline}" + ) - command += f" --msk-cidr-egress \"{self.getParam('aws_msk_egress_cidr')}\"" + command += ( + f" --msk-cidr-egress \"{self.getParam('aws_msk_egress_cidr')}\"" + ) command += f" --msk-cidr-ingress \"{self.getParam('aws_msk_ingress_cidr')}\"{newline}" # Kafka - Event Streams # ----------------------------------------------------------------------------- - if self.getParam('eventstreams_instance_name') != "": + if self.getParam("eventstreams_instance_name") != "": command += f" --eventstreams-resource-group \"{self.getParam('eventstreams_resourcegroup')}\"" command += f" --eventstreams-instance-name \"{self.getParam('eventstreams_name')}\"" command += f" --eventstreams-instance-location \"{self.getParam('eventstreams_location')}\"{newline}" # COS # ----------------------------------------------------------------------------- - if self.getParam('cos_type') != "": + if self.getParam("cos_type") != "": command += f" --cos \"{self.getParam('cos_type')}\"" - if self.getParam('cos_resourcegroup') != "": - command += f" --cos-resourcegroup \"{self.getParam('cos_resourcegroup')}\"" - if self.getParam('cos_apikey') != "": + if self.getParam("cos_resourcegroup") != "": + command += ( + f" --cos-resourcegroup \"{self.getParam('cos_resourcegroup')}\"" + ) + if self.getParam("cos_apikey") != "": command += f" --cos-apikey \"{self.getParam('cos_apikey')}\"" - if self.getParam('cos_instance_name') != "": - command += f" --cos-instance-name \"{self.getParam('cos_instance_name')}\"" - if self.getParam('cos_bucket_name') != "": + if self.getParam("cos_instance_name") != "": + command += ( + f" --cos-instance-name \"{self.getParam('cos_instance_name')}\"" + ) + if self.getParam("cos_bucket_name") != "": command += f" --cos-bucket-name \"{self.getParam('cos_bucket_name')}\"{newline}" command += newline # Cloud Providers # ----------------------------------------------------------------------------- - if self.getParam('ibmcloud_apikey') != "": + if self.getParam("ibmcloud_apikey") != "": command += f" --ibmcloud-apikey $IBMCLOUD_APIKEY{newline}" - if self.getParam('aws_access_key_id') != "": + if self.getParam("aws_access_key_id") != "": command += f" --aws-access-key-id $AWS_ACCESS_KEY_ID{newline}" - if self.getParam('secret_access_key') != "": + if self.getParam("secret_access_key") != "": command += f" --secret-access-key $SECRET_ACCESS_KEY{newline}" command += f" --aws-region \"{self.getParam('aws_region')}\"" command += f" --aws-vpc-id \"{self.getParam('aws_vpc_id')}\"" # Development Mode # ----------------------------------------------------------------------------- - if self.getParam('artifactory_username') != "": + if self.getParam("artifactory_username") != "": command += f" --artifactory-username $ARTIFACTORY_USERNAME --artifactory-token $ARTIFACTORY_TOKEN{newline}" # Approvals # ----------------------------------------------------------------------------- - if self.getParam('approval_core') != "": - command += f" --approval-core \"{self.getParam('approval_core')}\"{newline}" - if self.getParam('approval_assist') != "": - command += f" --approval-assist \"{self.getParam('approval_assist')}\"{newline}" - if self.getParam('approval_iot') != "": + if self.getParam("approval_core") != "": + command += ( + f" --approval-core \"{self.getParam('approval_core')}\"{newline}" + ) + if self.getParam("approval_assist") != "": + command += ( + f" --approval-assist \"{self.getParam('approval_assist')}\"{newline}" + ) + if self.getParam("approval_iot") != "": command += f" --approval-iot \"{self.getParam('approval_iot')}\"{newline}" - if self.getParam('approval_manage') != "": - command += f" --approval-manage \"{self.getParam('approval_manage')}\"{newline}" - if self.getParam('approval_monitor') != "": - command += f" --approval-monitor \"{self.getParam('approval_monitor')}\"{newline}" - if self.getParam('approval_optimizer') != "": + if self.getParam("approval_manage") != "": + command += ( + f" --approval-manage \"{self.getParam('approval_manage')}\"{newline}" + ) + if self.getParam("approval_monitor") != "": + command += ( + f" --approval-monitor \"{self.getParam('approval_monitor')}\"{newline}" + ) + if self.getParam("approval_optimizer") != "": command += f" --approval-optimizer \"{self.getParam('approval_optimizer')}\"{newline}" - if self.getParam('approval_predict') != "": - command += f" --approval-predict \"{self.getParam('approval_predict')}\"{newline}" - if self.getParam('approval_visualinspection') != "": + if self.getParam("approval_predict") != "": + command += ( + f" --approval-predict \"{self.getParam('approval_predict')}\"{newline}" + ) + if self.getParam("approval_visualinspection") != "": command += f" --approval-visualinspection \"{self.getParam('approval_visualinspection')}\"{newline}" - if self.getParam('approval_facilities') != "": + if self.getParam("approval_facilities") != "": command += f" --approval-facilities \"{self.getParam('approval_facilities')}\"{newline}" - if self.getParam('approval_aiservice') != "": + if self.getParam("approval_aiservice") != "": command += f" --approval-aiservice \"{self.getParam('approval_aiservice')}\"{newline}" # Slack # ----------------------------------------------------------------------------- - if self.getParam('slack_channel') != "" and self.getParam('slack_token'): + if self.getParam("slack_channel") != "" and self.getParam("slack_token"): command += f" --slack-channel \"{self.getParam('slack_channel')}\" --slack-token $SLACK_TOKEN{newline}" # More Options # ----------------------------------------------------------------------------- if self.devMode: command += f" --dev-mode{newline}" - if self.getParam('skip_pre_check') is True: + if self.getParam("skip_pre_check") is True: command += f" --skip-pre-check{newline}" - if self.getParam('skip_preinstall_rbac') == "true": + if self.getParam("skip_preinstall_rbac") == "true": command += f" --skip-preinstall-rbac{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}" + 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/install/argParser.py b/python/src/mas/cli/install/argParser.py index f74266c5ddd..75c8fc89f55 100644 --- a/python/src/mas/cli/install/argParser.py +++ b/python/src/mas/cli/install/argParser.py @@ -50,117 +50,102 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: installArgParser = argparse.ArgumentParser( prog="mas install", - description="\n".join([ - f"IBM Maximo Application Suite Admin CLI v{packageVersion}", - "Install MAS by configuring and launching the MAS Uninstall Tekton Pipeline.\n", - "Interactive Mode:", - "Omitting the --instance-id option will trigger an interactive prompt" - ]), + description="\n".join( + [ + f"IBM Maximo Application Suite Admin CLI v{packageVersion}", + "Install MAS by configuring and launching the MAS Uninstall Tekton Pipeline.\n", + "Interactive Mode:", + "Omitting the --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 + add_help=False, ) # MAS Catalog Selection & Entitlement # ----------------------------------------------------------------------------- catArgGroup = installArgParser.add_argument_group( "MAS Catalog Selection & Entitlement", - "Configure which IBM Maximo Operator Catalog to install and provide your IBM entitlement key for access to container images." + "Configure which IBM Maximo Operator Catalog to install and provide your IBM entitlement key for access to container images.", ) catArgGroup.add_argument( - "-c", "--mas-catalog-version", + "-c", + "--mas-catalog-version", required=False, - help="IBM Maximo Operator Catalog to install" + 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" + 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" + "--ibm-entitlement-key", required=False, help="IBM entitlement key" ) # Basic Configuration # ----------------------------------------------------------------------------- masArgGroup = installArgParser.add_argument_group( "Basic Configuration", - "Core configuration options for your MAS instance including instance ID, workspace settings, subscription channels, and user settings." + "Core configuration options for your MAS instance including instance ID, workspace settings, subscription channels, and user settings.", ) masArgGroup.add_argument( - "-i", "--mas-instance-id", - required=False, - help="MAS Instance ID" + "-i", "--mas-instance-id", required=False, help="MAS Instance ID" ) masArgGroup.add_argument( - "-w", "--mas-workspace-id", - required=False, - help="MAS Workspace ID" + "-w", "--mas-workspace-id", required=False, help="MAS Workspace ID" ) masArgGroup.add_argument( - "-W", "--mas-workspace-name", - required=False, - help="MAS Workspace Name" + "-W", "--mas-workspace-name", required=False, help="MAS Workspace Name" ) masArgGroup.add_argument( - "--mas-channel", - required=False, - help="Subscription channel for the Core Platform" + "--mas-channel", required=False, help="Subscription channel for the Core Platform" ) masArgGroup.add_argument( - "--aiservice-instance-id", - required=False, - help="AI Service Instance ID" + "--aiservice-instance-id", required=False, help="AI Service Instance ID" ) masArgGroup.add_argument( "--configure-ai-assistant", dest="configure_aiassistant", required=False, - help="Configure AI Assistant in silent mode (for example: pipeline, configure, none)" + help="Configure AI Assistant in silent mode (for example: pipeline, configure, none)", ) masArgGroup.add_argument( "--allow-special-chars", dest="mas_special_characters", required=False, help="Allow special characters for user username/ID", - action="store_true" + action="store_true", ) # Advanced Configuration # ----------------------------------------------------------------------------- masAdvancedArgGroup = installArgParser.add_argument_group( "Advanced Configuration", - "Advanced configuration options for MAS including DNS providers, certificates, domain settings, and IPv6 support." + "Advanced configuration options for MAS including DNS providers, certificates, domain settings, and IPv6 support.", ) masAdvancedArgGroup.add_argument( - "--superuser-username", - dest="mas_superuser_username", - required=False, - help="" + "--superuser-username", dest="mas_superuser_username", required=False, help="" ) masAdvancedArgGroup.add_argument( - "--superuser-password", - dest="mas_superuser_password", - required=False, - help="" + "--superuser-password", dest="mas_superuser_password", required=False, help="" ) masAdvancedArgGroup.add_argument( "--additional-configs", required=False, - help="Path to a directory containing additional configuration files to be applied" + help="Path to a directory containing additional configuration files to be applied", ) masAdvancedArgGroup.add_argument( "--pod-templates", required=False, - help="Path to directory containing custom podTemplates configuration files to be applied" + help="Path to directory containing custom podTemplates configuration files to be applied", ) masAdvancedArgGroup.add_argument( "--non-prod", required=False, help="Install MAS in non-production mode", - action="store_true" + action="store_true", ) masAdvancedArgGroup.add_argument( "--disable-ca-trust", @@ -168,45 +153,45 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Disable built-in trust of well-known CAs", action="store_const", - const="false" + const="false", ) masAdvancedArgGroup.add_argument( "--routing", dest="mas_routing_mode", required=False, help="Configure MAS with path or subdomain routing", - choices=["path", "subdomain"] + choices=["path", "subdomain"], ) masAdvancedArgGroup.add_argument( "--configure-ingress", dest="mas_configure_ingress", required=False, action="store_true", - help="Automatically configure IngressController to allow InterNamespaceAllowed for path-based routing" + help="Automatically configure IngressController to allow InterNamespaceAllowed for path-based routing", ) masAdvancedArgGroup.add_argument( "--ingress-controller-name", dest="mas_ingress_controller_name", required=False, - help="Name of the IngressController to use for path-based routing (default: 'default')" + help="Name of the IngressController to use for path-based routing (default: 'default')", ) masAdvancedArgGroup.add_argument( "--servicemesh", dest="mas_use_service_mesh", required=False, help="Configure MAS to use Service Mesh networking (default: false)", - choices=["true", "false"] + choices=["true", "false"], ) masAdvancedArgGroup.add_argument( "--manual-certificates", required=False, - help="Path to directory containing the certificates to be applied" + help="Path to directory containing the certificates to be applied", ) masAdvancedArgGroup.add_argument( "--domain", dest="mas_domain", required=False, - help="Configure MAS with a custom domain" + help="Configure MAS with a custom domain", ) masAdvancedArgGroup.add_argument( "--disable-walkme", @@ -214,7 +199,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Disable MAS guided tour", action="store_const", - const="false" + const="false", ) masAdvancedArgGroup.add_argument( "--disable-feature-usage", @@ -222,7 +207,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Disable feature adoption metrics reporting", action="store_const", - const="false" + const="false", ) masAdvancedArgGroup.add_argument( "--disable-deployment-progression", @@ -230,7 +215,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Disable deployment progression metrics reporting", action="store_const", - const="false" + const="false", ) masAdvancedArgGroup.add_argument( "--disable-usability-metrics", @@ -238,7 +223,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Disable usability metrics reporting", action="store_const", - const="false" + const="false", ) masAdvancedArgGroup.add_argument( @@ -247,14 +232,14 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Enable automatic DNS management (see DNS Configuration options)", choices=DNS_PROVIDERS, - metavar="{cloudflare,cis,route53}" + metavar="{cloudflare,cis,route53}", ) masAdvancedArgGroup.add_argument( "--ocp-ingress", dest="ocp_ingress", required=False, - help="Overwrites Ingress Domain" + help="Overwrites Ingress Domain", ) masAdvancedArgGroup.add_argument( @@ -278,7 +263,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Configure MAS to run in IP version 6. Before setting this option, be sure your cluster is configured in IP version 6", action="store_const", - const="true" + const="true", ) masAdvancedArgGroup.add_argument( @@ -287,7 +272,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Permission mode for MAS installation: 'cluster' (with ClusterRoles, default), 'namespaced' (without ClusterRoles, limited to pre-created namespaces), 'minimal' (essential roles only, no app lifecycle management)", choices=["cluster", "namespaced", "minimal"], - default=None + default=None, ) # DNS Integration - IBM CIS @@ -297,110 +282,110 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: "--cis-email", dest="cis_email", required=False, - help="Required when DNS provider is CIS and you want to use a Let's Encrypt ClusterIssuer" + help="Required when DNS provider is CIS and you want to use a Let's Encrypt ClusterIssuer", ) cisArgGroup.add_argument( "--cis-apikey", dest="cis_apikey", required=False, - help="Required when DNS provider is CIS" + help="Required when DNS provider is CIS", ) cisArgGroup.add_argument( "--cis-crn", dest="cis_crn", required=False, - help="Required when DNS provider is CIS" + help="Required when DNS provider is CIS", ) cisArgGroup.add_argument( "--cis-subdomain", dest="cis_subdomain", required=False, - help="Optionally setup MAS instance as a subdomain under a multi-tenant CIS DNS record" + help="Optionally setup MAS instance as a subdomain under a multi-tenant CIS DNS record", ) # DNS Integration - CloudFlare # ----------------------------------------------------------------------------- cloudFlareArgGroup = installArgParser.add_argument_group( "DNS Integration - CloudFlare", - "Configuration options for Cloudflare DNS provider, including API credentials, zone, and subdomain settings." + "Configuration options for Cloudflare DNS provider, including API credentials, zone, and subdomain settings.", ) cloudFlareArgGroup.add_argument( "--cloudflare-email", dest="cloudflare_email", required=False, - help="Required when DNS provider is Cloudflare" + help="Required when DNS provider is Cloudflare", ) cloudFlareArgGroup.add_argument( "--cloudflare-apitoken", dest="cloudflare_apitoken", required=False, - help="Required when DNS provider is Cloudflare" + help="Required when DNS provider is Cloudflare", ) cloudFlareArgGroup.add_argument( "--cloudflare-zone", dest="cloudflare_zone", required=False, - help="Required when DNS provider is Cloudflare" + help="Required when DNS provider is Cloudflare", ) cloudFlareArgGroup.add_argument( "--cloudflare-subdomain", dest="cloudflare_subdomain", required=False, - help="Required when DNS provider is Cloudflare" + help="Required when DNS provider is Cloudflare", ) # Storage # ----------------------------------------------------------------------------- storageArgGroup = installArgParser.add_argument_group( "Storage", - "Configure storage classes for ReadWriteOnce (RWO) and ReadWriteMany (RWX) volumes, and pipeline storage settings." + "Configure storage classes for ReadWriteOnce (RWO) and ReadWriteMany (RWX) volumes, and pipeline storage settings.", ) storageArgGroup.add_argument( "--storage-class-rwo", required=False, - help="ReadWriteOnce (RWO) storage class (e.g. ibmc-block-gold)" + 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)" + 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)" + 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", choices=STORAGE_ACCESS_MODES, - metavar="{ReadWriteMany,ReadWriteOnce}" + metavar="{ReadWriteMany,ReadWriteOnce}", ) # IBM Suite License Service # ----------------------------------------------------------------------------- slsArgGroup = installArgParser.add_argument_group( "IBM Suite License Service", - "Configure IBM Suite License Service (SLS) including license file location, namespace, and subscription channel." + "Configure IBM Suite License Service (SLS) including license file location, namespace, and subscription channel.", ) slsArgGroup.add_argument( "--license-file", required=False, help="Path to MAS license file", - type=lambda x: isValidFile(installArgParser, x) + type=lambda x: isValidFile(installArgParser, x), ) slsArgGroup.add_argument( "--sls-namespace", required=False, help="Customize the SLS install namespace", - default="ibm-sls" + default="ibm-sls", ) slsArgGroup.add_argument( "--dedicated-sls", action="store_true", default=False, - help="Set the SLS namespace to mas--sls" + help="Set the SLS namespace to mas--sls", ) slsArgGroup.add_argument( "--sls-channel", @@ -412,150 +397,137 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: # ----------------------------------------------------------------------------- droArgGroup = installArgParser.add_argument_group( "IBM Data Reporting Operator", - "Configure IBM Data Reporting Operator (DRO) with contact information and namespace settings for usage data collection." + "Configure IBM Data Reporting Operator (DRO) with contact information and namespace settings for usage data collection.", ) droArgGroup.add_argument( "--contact-email", "--uds-email", dest="dro_contact_email", required=False, - help="Contact e-mail address" + help="Contact e-mail address", ) droArgGroup.add_argument( "--contact-firstname", "--uds-firstname", dest="dro_contact_firstname", required=False, - help="Contact first name" + 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 DRO" + help="Contact last name", ) +droArgGroup.add_argument("--dro-namespace", required=False, help="Namespace for DRO") # MongoDB Community Operator # ----------------------------------------------------------------------------- mongoArgGroup = installArgParser.add_argument_group( "MongoDB Community Operator", - "Configure the namespace for MongoDB Community Operator deployment." + "Configure the namespace for MongoDB Community Operator deployment.", ) mongoArgGroup.add_argument( "--mongodb-namespace", required=False, - help="Namespace for MongoDB Community Operator" + help="Namespace for MongoDB Community Operator", ) # OCP Configuration # ----------------------------------------------------------------------------- ocpArgGroup = installArgParser.add_argument_group( "OCP Configuration", - "OpenShift Container Platform specific configuration including ingress certificate settings." + "OpenShift Container Platform specific configuration including ingress certificate settings.", ) ocpArgGroup.add_argument( "--ocp-ingress-tls-secret-name", required=False, - help="Name of the secret holding the cluster's ingress certificates" + help="Name of the secret holding the cluster's ingress certificates", ) # Grafana # ----------------------------------------------------------------------------- grafanaArgGroup = installArgParser.add_argument_group( - "Grafana", - "Configure Grafana installation, namespace and storage size." + "Grafana", "Configure Grafana installation, namespace and storage size." ) grafanaArgGroup.add_argument( "--skip-grafana-install", required=False, action="store_true", - help="Skip Grafana installation" + help="Skip Grafana installation", ) grafanaArgGroup.add_argument( "--grafana-v5-namespace", required=False, help="Customize the Grafana namespace", - default="grafana5" + default="grafana5", ) grafanaArgGroup.add_argument( "--grafana-instance-storage-size", required=False, help="Customize the Grafana storage size", - default="10Gi" + default="10Gi", ) # MAS Applications # ----------------------------------------------------------------------------- masAppsArgGroup = installArgParser.add_argument_group( "MAS Applications", - "Configure subscription channels for MAS applications including Assist, IoT, Monitor, Optimizer, Predict, and Visual Inspection." + "Configure subscription channels for MAS applications including Assist, IoT, Monitor, Optimizer, Predict, and Visual Inspection.", ) masAppsArgGroup.add_argument( - "--assist-channel", - required=False, - help="Subscription channel for Maximo Assist" + "--assist-channel", required=False, help="Subscription channel for Maximo Assist" ) masAppsArgGroup.add_argument( - "--iot-channel", - required=False, - help="Subscription channel for Maximo IoT" + "--iot-channel", required=False, help="Subscription channel for Maximo IoT" ) masAppsArgGroup.add_argument( - "--monitor-channel", - required=False, - help="Subscription channel for Maximo Monitor" + "--monitor-channel", required=False, help="Subscription channel for Maximo Monitor" ) masAppsArgGroup.add_argument( - "--manage-channel", - required=False, - help="Subscription channel for Maximo Manage" + "--manage-channel", required=False, help="Subscription channel for Maximo Manage" ) masAppsArgGroup.add_argument( - "--predict-channel", - required=False, - help="Subscription channel for Maximo Predict" + "--predict-channel", required=False, help="Subscription channel for Maximo Predict" ) masAppsArgGroup.add_argument( "--visualinspection-channel", required=False, - help="Subscription channel for Maximo Visual Inspection" + help="Subscription channel for Maximo Visual Inspection", ) masAppsArgGroup.add_argument( "--optimizer-channel", required=False, - help="Subscription channel for Maximo optimizer" + help="Subscription channel for Maximo optimizer", ) masAppsArgGroup.add_argument( "--optimizer-plan", required=False, choices=["full", "limited"], - help="Install plan for Maximo Optimizer" + help="Install plan for Maximo Optimizer", ) masAppsArgGroup.add_argument( "--facilities-channel", required=False, - help="Subscription channel for Maximo Real Estate and Facilities" + help="Subscription channel for Maximo Real Estate and Facilities", ) masAppsArgGroup.add_argument( "--aiservice-channel", required=False, - help="Subscription channel for Maximo AI Service" + help="Subscription channel for Maximo AI Service", ) # Arcgis # ----------------------------------------------------------------------------- -arcgisArgGroup = installArgParser.add_argument_group("Maximo Location Services for Esri (arcgis)") +arcgisArgGroup = installArgParser.add_argument_group( + "Maximo Location Services for Esri (arcgis)" +) arcgisArgGroup.add_argument( "--arcgis-channel", dest="mas_arcgis_channel", required=False, - help="Subscription channel for IBM Maximo Location Services for Esri. Only applicable if installing Manage with Spatial or Facilities" + help="Subscription channel for IBM Maximo Location Services for Esri. Only applicable if installing Manage with Spatial or Facilities", ) # Manage Advanced Settings @@ -566,7 +538,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: dest="mas_app_settings_server_bundles_size", required=False, help="Set Manage server bundle size configuration", - choices=["dev", "snojms", "small", "jms"] + choices=["dev", "snojms", "small", "jms"], ) manageArgGroup.add_argument( "--manage-jms", @@ -574,7 +546,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Set JMS configuration", action="store_const", - const="true" + const="true", ) manageArgGroup.add_argument( "--manage-persistent-volumes", @@ -582,7 +554,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="", action="store_const", - const="true" + const="true", ) manageArgGroup.add_argument( @@ -590,7 +562,14 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: dest="mas_appws_bindings_jdbc_manage", required=False, help="", - choices=["system", "workspace-application"] + choices=["system", "workspace-application"], +) +manageArgGroup.add_argument( + "--manage-kafka", + dest="mas_appws_bindings_kafka_manage", + required=False, + help="Select the Kafka configuration to bind to Manage (e.g., 'system'). Required for Civil Infrastructure Kafka Image Processor.", + default="", ) manageArgGroup.add_argument( "--manage-demodata", @@ -598,14 +577,14 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="", action="store_const", - const="true" + const="true", ) manageArgGroup.add_argument( "--manage-components", dest="mas_appws_components", required=False, help="Set Manage Components to be installed (e.g 'base=latest,health=latest,civil=latest')", - default="" + default="", ) manageArgGroup.add_argument( @@ -614,101 +593,101 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Set boolean value indicating if Watson Studio must be bound to Manage. It is expected a system level WatsonStudioCfg applied in the cluster.", action="store_const", - const="true" + const="true", ) manageArgGroup.add_argument( "--manage-customization-archive-name", dest="mas_app_settings_customization_archive_name", required=False, - help="Manage Archive name" + help="Manage Archive name", ) manageArgGroup.add_argument( "--manage-customization-archive-url", dest="mas_app_settings_customization_archive_url", required=False, - help="Manage Archive url" + help="Manage Archive url", ) manageArgGroup.add_argument( "--manage-customization-archive-username", dest="mas_app_settings_customization_archive_username", required=False, - help="Manage Archive username (HTTP basic auth)" + help="Manage Archive username (HTTP basic auth)", ) manageArgGroup.add_argument( "--manage-customization-archive-password", dest="mas_app_settings_customization_archive_password", required=False, - help="Manage Archive password (HTTP basic auth)" + help="Manage Archive password (HTTP basic auth)", ) manageArgGroup.add_argument( "--manage-db-tablespace", dest="mas_app_settings_tablespace", required=False, - help="Database tablespace name that Manage will use to be installed. Default is 'MAXDATA'" + help="Database tablespace name that Manage will use to be installed. Default is 'MAXDATA'", ) manageArgGroup.add_argument( "--manage-db-indexspace", dest="mas_app_settings_indexspace", required=False, - help="Database indexspace name that Manage will use to be installed. Default is 'MAXINDEX'" + help="Database indexspace name that Manage will use to be installed. Default is 'MAXINDEX'", ) manageArgGroup.add_argument( "--manage-db-schema", dest="mas_app_settings_db2_schema", required=False, - help="Database schema name that Manage will use to be installed. Default is 'maximo'" + help="Database schema name that Manage will use to be installed. Default is 'maximo'", ) manageArgGroup.add_argument( "--manage-crypto-key", dest="mas_manage_encryptionsecret_crypto_key", required=False, - help="Customize Manage database encryption keys" + help="Customize Manage database encryption keys", ) manageArgGroup.add_argument( "--manage-cryptox-key", dest="mas_manage_encryptionsecret_cryptox_key", required=False, - help="Customize Manage database encryption keys" + help="Customize Manage database encryption keys", ) manageArgGroup.add_argument( "--manage-old-crypto-key", dest="mas_manage_encryptionsecret_old_crypto_key", required=False, - help="Customize Manage database encryption keys" + help="Customize Manage database encryption keys", ) manageArgGroup.add_argument( "--manage-old-cryptox-key", dest="mas_manage_encryptionsecret_old_cryptox_key", required=False, - help="Customize Manage database encryption keys" + help="Customize Manage database encryption keys", ) manageArgGroup.add_argument( "--manage-encryption-secret-name", dest="mas_manage_ws_db_encryptionsecret", required=False, - help="Name of the Manage database encryption secret" + help="Name of the Manage database encryption secret", ) manageArgGroup.add_argument( "--manage-base-language", dest="mas_app_settings_base_lang", required=False, - help="Manage base language to be installed. Default is `EN` (English)" + help="Manage base language to be installed. Default is `EN` (English)", ) manageArgGroup.add_argument( "--manage-secondary-languages", dest="mas_app_settings_secondary_langs", required=False, - help="Comma-separated list of Manage secondary languages to be installed (e.g. 'JA,DE,AR')" + help="Comma-separated list of Manage secondary languages to be installed (e.g. 'JA,DE,AR')", ) manageArgGroup.add_argument( "--manage-server-timezone", dest="mas_app_settings_server_timezone", required=False, - help="Manage server timezone. Default is `GMT`" + help="Manage server timezone. Default is `GMT`", ) manageArgGroup.add_argument( @@ -718,7 +697,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: help="Set Manage upgrade type (default: regularUpgrade)", default="regularUpgrade", choices=UPGRADE_TYPES, - metavar="{regularUpgrade,onlineUpgrade}" + metavar="{regularUpgrade,onlineUpgrade}", ) # Manage Attachments @@ -729,7 +708,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Storage provider type for Maximo Manage attachments", choices=ATTACHMENT_PROVIDERS, - metavar="{filestorage,ibm,aws}" + metavar="{filestorage,ibm,aws}", ) manageArgGroup.add_argument( "--manage-attachments-mode", @@ -737,27 +716,27 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="How attachment properties will be configured in Manage", choices=ATTACHMENT_MODES, - metavar="{cr,db}" + metavar="{cr,db}", ) manageArgGroup.add_argument( "--manage-aiservice-instance-id", dest="manage_bind_aiservice_instance_id", required=False, - help="AI Service Instance ID to bind with Manage" + help="AI Service Instance ID to bind with Manage", ) manageArgGroup.add_argument( "--manage-aiservice-tenant-id", dest="manage_bind_aiservice_tenant_id", required=False, - help="AI Service Tenant ID to bind with Manage" + help="AI Service Tenant ID to bind with Manage", ) # Facilities Advanced Settings # ----------------------------------------------------------------------------- facilitiesArgGroup = installArgParser.add_argument_group( "Advanced Settings - Facilities", - "Advanced configuration for Maximo Real Estate and Facilities including deployment size, image pull policy, routes timeout, Liberty extensions, vault secrets, workflow agents, connection pool size, and storage settings." + "Advanced configuration for Maximo Real Estate and Facilities including deployment size, image pull policy, routes timeout, Liberty extensions, vault secrets, workflow agents, connection pool size, and storage settings.", ) facilitiesArgGroup.add_argument( "--facilities-app-om-upgrade-mode", @@ -766,7 +745,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: help="Sets the Application Object Migration Mode", default="manual", choices=FACILITIES_APPOMUPGRADEMODE, - metavar="{manual,load-only,automatic}" + metavar="{manual,load-only,automatic}", ) facilitiesArgGroup.add_argument( "--facilities-size", @@ -775,7 +754,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: help="Size of Facilities deployment", default="small", choices=FACILITIES_SIZES, - metavar="{small,medium,large}" + metavar="{small,medium,large}", ) facilitiesArgGroup.add_argument( "--facilities-pull-policy", @@ -784,33 +763,33 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: help="Image pull policy for Facilities", default="IfNotPresent", choices=IMAGE_PULL_POLICIES, - metavar="{IfNotPresent,Always}" + metavar="{IfNotPresent,Always}", ) facilitiesArgGroup.add_argument( "--facilities-routes-timeout", dest="mas_ws_facilities_routes_timeout", required=False, help="Timeout for Facilities routes (default: 600s)", - default="600s" + default="600s", ) facilitiesArgGroup.add_argument( "--facilities-xml-extension", dest="mas_ws_facilities_liberty_extension_XML", required=False, - help="Secret name containing Liberty server extensions" + help="Secret name containing Liberty server extensions", ) facilitiesArgGroup.add_argument( "--facilities-vault-secret", dest="mas_ws_facilities_vault_secret", required=False, - help="Secret name containing AES encryption password" + help="Secret name containing AES encryption password", ) facilitiesArgGroup.add_argument( "--facilities-dwfagent", dest="mas_ws_facilities_dwfagents", required=False, help="List of dedicated workflow agents", - type=str + type=str, ) facilitiesArgGroup.add_argument( "--facilities-maxconnpoolsize", @@ -818,13 +797,13 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Maximum database connection pool size (default: 200)", type=int, - default=200 + default=200, ) facilitiesArgGroup.add_argument( "--facilities-log-storage-class", dest="mas_ws_facilities_storage_log_class", required=False, - help="Storage class for Facilities logs" + help="Storage class for Facilities logs", ) facilitiesArgGroup.add_argument( "--facilities-log-storage-mode", @@ -838,13 +817,13 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: dest="mas_ws_facilities_storage_log_size", required=False, help="Storage size for Facilities logs", - default=30 + default=30, ) facilitiesArgGroup.add_argument( "--facilities-userfiles-storage-class", dest="mas_ws_facilities_storage_userfiles_class", required=False, - help="Storage class for Facilities user files" + help="Storage class for Facilities user files", ) facilitiesArgGroup.add_argument( "--facilities-userfiles-storage-mode", @@ -858,7 +837,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: dest="mas_ws_facilities_storage_userfiles_size", required=False, help="Storage size for Facilities user files", - default=50 + default=50, ) # Open Data Hub @@ -870,7 +849,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: dest="aiservice_odh_model_deployment_type", required=False, default="raw", - help="Model deployment type for ODH" + help="Model deployment type for ODH", ) # Red Hat Openshift AI @@ -882,7 +861,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: dest="aiservice_rhoai_model_deployment_type", required=False, default="raw", - help="Model deployment type for RedHat Openshift AI" + help="Model deployment type for RedHat Openshift AI", ) rhoaiArgGroup.add_argument( @@ -891,13 +870,13 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="temporary flag to install Redhat Openshift AI instead of Opendatahub", action="store_const", - const="true" + const="true", ) # S3 Storage # ----------------------------------------------------------------------------- aiServiceS3ArgGroup = installArgParser.add_argument_group( "S3 Storage", - "Configure S3-compatible object storage for AI Service including Minio installation or external S3 connection details (host, port, SSL, credentials, bucket, and region)." + "Configure S3-compatible object storage for AI Service including Minio installation or external S3 connection details (host, port, SSL, credentials, bucket, and region).", ) aiServiceS3ArgGroup.add_argument( "--install-minio", @@ -905,7 +884,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Install Minio and configure it as the S3 provider for AI Service", action="store_const", - const="true" + const="true", ) # S3 - Minio @@ -914,13 +893,13 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: "--minio-root-user", dest="minio_root_user", required=False, - help="Root user for minio" + help="Root user for minio", ) aiServiceS3ArgGroup.add_argument( "--minio-root-password", dest="minio_root_password", required=False, - help="Password for minio root user" + help="Password for minio root user", ) # S3 - External Connection @@ -929,43 +908,43 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: "--s3-host", dest="aiservice_s3_host", required=False, - help="Hostname or IP address of the S3 storage service" + help="Hostname or IP address of the S3 storage service", ) aiServiceS3ArgGroup.add_argument( "--s3-port", dest="aiservice_s3_port", required=False, - help="Port number for the S3 storage service" + help="Port number for the S3 storage service", ) aiServiceS3ArgGroup.add_argument( "--s3-ssl", dest="aiservice_s3_ssl", required=False, - help="Enable or disable SSL for S3 connection (true/false)" + help="Enable or disable SSL for S3 connection (true/false)", ) aiServiceS3ArgGroup.add_argument( "--s3-accesskey", dest="aiservice_s3_accesskey", required=False, - help="Access key for authenticating with the S3 storage service" + help="Access key for authenticating with the S3 storage service", ) aiServiceS3ArgGroup.add_argument( "--s3-secretkey", dest="aiservice_s3_secretkey", required=False, - help="Secret key for authenticating with the S3 storage service" + help="Secret key for authenticating with the S3 storage service", ) aiServiceS3ArgGroup.add_argument( "--s3-region", dest="aiservice_s3_region", required=False, - help="Region for the S3 storage service" + help="Region for the S3 storage service", ) aiServiceS3ArgGroup.add_argument( "--s3-bucket-prefix", dest="aiservice_s3_bucket_prefix", required=False, - help="Bucket prefix configured with S3 storage service" + help="Bucket prefix configured with S3 storage service", ) # S3 - Bucket Naming @@ -975,150 +954,143 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: dest="aiservice_s3_tenants_bucket", required=False, default="km-tenants", - help="Name of the S3 bucket for tenants storage" + help="Name of the S3 bucket for tenants storage", ) aiServiceS3ArgGroup.add_argument( "--s3-templates-bucket", dest="aiservice_s3_templates_bucket", required=False, default="km-templates", - help="Name of the S3 bucket for templates storage" + help="Name of the S3 bucket for templates storage", ) # Watsonx # ----------------------------------------------------------------------------- aiServiceWatsonxArgGroup = installArgParser.add_argument_group( "Watsonx", - "Configure IBM Watsonx integration for AI Service including API key, instance ID, project ID, and service URL." + "Configure IBM Watsonx integration for AI Service including API key, instance ID, project ID, and service URL.", ) aiServiceWatsonxArgGroup.add_argument( "--watsonxai-apikey", dest="aiservice_watsonxai_apikey", required=False, - help="API key for WatsonX" + help="API key for WatsonX", ) aiServiceWatsonxArgGroup.add_argument( "--watsonxai-url", dest="aiservice_watsonxai_url", required=False, - help="URL endpoint for WatsonX" + help="URL endpoint for WatsonX", ) aiServiceWatsonxArgGroup.add_argument( "--watsonxai-project-id", dest="aiservice_watsonxai_project_id", required=False, - help="Project ID for WatsonX" + help="Project ID for WatsonX", ) aiServiceWatsonxArgGroup.add_argument( "--watsonx-action", dest="aiservice_watsonx_action", required=False, - help="Action to perform with WatsonX (install/remove)" + help="Action to perform with WatsonX (install/remove)", ) aiServiceWatsonxArgGroup.add_argument( "--watsonxai-ca-crt", dest="aiservice_watsonxai_ca_crt", required=False, - help="CA certificate for WatsonX AI (PEM format, optional, only if using self-signed certs)" + help="CA certificate for WatsonX AI (PEM format, optional, only if using self-signed certs)", ) aiServiceWatsonxArgGroup.add_argument( "--watsonxai-deployment-id", dest="aiservice_watsonxai_deployment_id", required=False, - help="WatsonX deployment ID" + help="WatsonX deployment ID", ) aiServiceWatsonxArgGroup.add_argument( "--watsonxai-space-id", dest="aiservice_watsonxai_space_id", required=False, - help="WatsonX space ID" + help="WatsonX space ID", ) aiServiceWatsonxArgGroup.add_argument( "--watsonxai-instance-id", dest="aiservice_watsonxai_instance_id", required=False, - help="WatsonX instance ID" + help="WatsonX instance ID", ) aiServiceWatsonxArgGroup.add_argument( "--watsonxai-username", dest="aiservice_watsonxai_username", required=False, - help="WatsonX username" + help="WatsonX username", ) aiServiceWatsonxArgGroup.add_argument( "--watsonxai-version", dest="aiservice_watsonxai_version", required=False, - help="WatsonX version" + help="WatsonX version", ) aiServiceWatsonxArgGroup.add_argument( "--watsonxai-onprem", dest="aiservice_watsonxai_on_prem", required=False, - help="WatsonX deployed on prem" + help="WatsonX deployed on prem", ) # AI Service Tenant # ----------------------------------------------------------------------------- -aiServiceTenantArgGroup = installArgParser.add_argument_group("Maximo AI Service Tenant") +aiServiceTenantArgGroup = installArgParser.add_argument_group( + "Maximo AI Service Tenant" +) aiServiceTenantArgGroup.add_argument( "--tenant-entitlement-type", dest="tenant_entitlement_type", required=False, default="standard", - help="Entitlement type for AI Service tenant" + help="Entitlement type for AI Service tenant", ) aiServiceTenantArgGroup.add_argument( "--tenant-entitlement-start-date", dest="tenant_entitlement_start_date", required=False, - help="Start date for AI Service tenant" + help="Start date for AI Service tenant", ) aiServiceTenantArgGroup.add_argument( "--tenant-entitlement-end-date", dest="tenant_entitlement_end_date", required=False, - help="End date for AI Service tenant" + help="End date for AI Service tenant", ) aiServiceTenantArgGroup.add_argument( - "--rsl-url", - dest="rsl_url", - required=False, - help="rsl url" + "--rsl-url", dest="rsl_url", required=False, help="rsl url" ) aiServiceTenantArgGroup.add_argument( - "--rsl-org-id", - dest="rsl_org_id", - required=False, - help="org id for rsl" + "--rsl-org-id", dest="rsl_org_id", required=False, help="org id for rsl" ) aiServiceTenantArgGroup.add_argument( - "--rsl-token", - dest="rsl_token", - required=False, - help="token for rsl" + "--rsl-token", dest="rsl_token", required=False, help="token for rsl" ) aiServiceTenantArgGroup.add_argument( "--rsl-ca-crt", dest="rsl_ca_crt", required=False, - help="CA certificate for RSL API (PEM format, optional, only if using self-signed certs)" + help="CA certificate for RSL API (PEM format, optional, only if using self-signed certs)", ) # AI Service Configuration # ----------------------------------------------------------------------------- aiServiceArgGroup = installArgParser.add_argument_group( "Maximo AI Service", - "Maximo AI Service configuration such as certificate Issuer, environment type" + "Maximo AI Service configuration such as certificate Issuer, environment type", ) aiServiceArgGroup.add_argument( "--environment-type", dest="environment_type", required=False, default="non-production", - help="Environment type (default: non-production)" + help="Environment type (default: non-production)", ) aiServiceArgGroup.add_argument( "--aiservice-certificate-issuer", @@ -1131,20 +1103,20 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: dest="tenant_scheduling_config_file", required=False, help="Path to the YAML file that contains the scheduling configuration for tenant", - type=lambda x: isValidFile(installArgParser, x) + type=lambda x: isValidFile(installArgParser, x), ) # IBM Cloud Pak for Data # ----------------------------------------------------------------------------- cpdAppsArgGroup = installArgParser.add_argument_group( "IBM Cloud Pak for Data", - "Configure IBM Cloud Pak for Data applications including Watson Studio, Watson Machine Learning, Watson Discovery, Analytics Engine (Spark), Cognos Analytics, SPSS Modeler, and Canvas Base." + "Configure IBM Cloud Pak for Data applications including Watson Studio, Watson Machine Learning, Watson Discovery, Analytics Engine (Spark), Cognos Analytics, SPSS Modeler, and Canvas Base.", ) cpdAppsArgGroup.add_argument( "--cp4d-version", dest="cpd_product_version", required=False, - help="Product version of CP4D to use" + help="Product version of CP4D to use", ) cpdAppsArgGroup.add_argument( "--cp4d-install-cognos", @@ -1152,7 +1124,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Add Cognos as part of Cloud Pak for Data", action="store_const", - const="install" + const="install", ) cpdAppsArgGroup.add_argument( "--cp4d-install-ws", @@ -1160,7 +1132,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Add Watson Studio as part of Cloud Pak for Data", action="store_const", - const="install" + const="install", ) cpdAppsArgGroup.add_argument( "--cp4d-install-wml", @@ -1168,7 +1140,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Add Watson Machine Learning as part of Cloud Pak for Data", action="store_const", - const="install" + const="install", ) cpdAppsArgGroup.add_argument( "--cp4d-install-ae", @@ -1176,24 +1148,22 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Add Spark Analytics Engine as part of Cloud Pak for Data", action="store_const", - const="install" + const="install", ) # IBM Db2 Universal Operator # ----------------------------------------------------------------------------- db2ArgGroup = installArgParser.add_argument_group( "IBM Db2 Universal Operator", - "Configure IBM Db2 instances including namespace, channel, installation options for system/manage/facilities databases, database type, timezone, affinity, tolerations, resource limits, and storage capacity." + "Configure IBM Db2 instances including namespace, channel, installation options for system/manage/facilities databases, database type, timezone, affinity, tolerations, resource limits, and storage capacity.", ) db2ArgGroup.add_argument( "--db2-namespace", required=False, - help="Change namespace where Db2u instances will be created" + help="Change namespace where Db2u instances will be created", ) db2ArgGroup.add_argument( - "--db2-channel", - required=False, - help="Subscription channel for Db2u" + "--db2-channel", required=False, help="Subscription channel for Db2u" ) db2ArgGroup.add_argument( "--db2-system", @@ -1201,7 +1171,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Install a shared Db2u instance for MAS (required by IoT & Monitor, supported by Manage)", action="store_const", - const="install" + const="install", ) db2ArgGroup.add_argument( "--db2-manage", @@ -1209,7 +1179,7 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Install a dedicated Db2u instance for Maximo Manage (supported by Manage)", action="store_const", - const="install" + const="install", ) db2ArgGroup.add_argument( "--db2-facilities", @@ -1217,114 +1187,98 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Install a dedicated Db2u instance for Maximo Real Estate and Facilities (supported by Facilities)", action="store_const", - const="install" + const="install", ) db2ArgGroup.add_argument( "--db2-type", required=False, help="Type of Manage dedicated Db2u instance (default: db2wh)", choices=DB2_TYPES, - metavar="{db2wh,db2oltp}" + metavar="{db2wh,db2oltp}", ) db2ArgGroup.add_argument( - "--db2-timezone", - required=False, - help="Timezone for Db2 instance" + "--db2-timezone", required=False, help="Timezone for Db2 instance" ) db2ArgGroup.add_argument( - "--db2-license-file", - required=False, - help="Db2 License File for Db2" + "--db2-license-file", required=False, help="Db2 License File for Db2" ) db2ArgGroup.add_argument( - "--db2-affinity-key", - required=False, - help="Set a node label to declare affinity to" + "--db2-affinity-key", required=False, help="Set a node label to declare affinity to" ) db2ArgGroup.add_argument( "--db2-affinity-value", required=False, - help="Set the value of the node label to affine with" + help="Set the value of the node label to affine with", ) db2ArgGroup.add_argument( - "--db2-tolerate-key", - required=False, - help="Set a node taint to tolerate" + "--db2-tolerate-key", required=False, help="Set a node taint to tolerate" ) db2ArgGroup.add_argument( "--db2-tolerate-value", required=False, - help="Set the value of the taint to tolerate" + help="Set the value of the taint to tolerate", ) db2ArgGroup.add_argument( "--db2-tolerate-effect", required=False, help="Taint effect to tolerate", choices=TAINT_EFFECTS, - metavar="{NoSchedule,PreferNoSchedule,NoExecute}" + metavar="{NoSchedule,PreferNoSchedule,NoExecute}", ) db2ArgGroup.add_argument( - "--db2-cpu-requests", - required=False, - help="Customize Db2 CPU request" + "--db2-cpu-requests", required=False, help="Customize Db2 CPU request" ) db2ArgGroup.add_argument( - "--db2-cpu-limits", - required=False, - help="Customize Db2 CPU limit" + "--db2-cpu-limits", required=False, help="Customize Db2 CPU limit" ) db2ArgGroup.add_argument( - "--db2-memory-requests", - required=False, - help="Customize Db2 memory request" + "--db2-memory-requests", required=False, help="Customize Db2 memory request" ) db2ArgGroup.add_argument( - "--db2-memory-limits", - required=False, - help="Customize Db2 memory limit" + "--db2-memory-limits", required=False, help="Customize Db2 memory limit" ) db2ArgGroup.add_argument( "--db2-backup-storage", dest="db2_backup_storage_size", required=False, - help="Db2 backup storage capacity" + help="Db2 backup storage capacity", ) db2ArgGroup.add_argument( "--db2-data-storage", dest="db2_data_storage_size", required=False, - help="Db2 data storage capacity" + help="Db2 data storage capacity", ) db2ArgGroup.add_argument( "--db2-logs-storage", dest="db2_logs_storage_size", required=False, - help="Db2 logs storage capacity" + help="Db2 logs storage capacity", ) db2ArgGroup.add_argument( "--db2-meta-storage", dest="db2_meta_storage_size", required=False, - help="Db2 metadata storage capacity" + help="Db2 metadata storage capacity", ) db2ArgGroup.add_argument( "--db2-temp-storage", dest="db2_temp_storage_size", required=False, - help="Db2 temporary storage capacity" + help="Db2 temporary storage capacity", ) db2ArgGroup.add_argument( "--db2u-kind", dest="db2u_kind", required=False, - help="Db2 resource kind in the cluster" + help="Db2 resource kind in the cluster", ) # ECK Integration # ----------------------------------------------------------------------------- eckArgGroup = installArgParser.add_argument_group( "ECK Integration", - "Configure Elastic Cloud on Kubernetes (ECK) integration for logging and monitoring capabilities." + "Configure Elastic Cloud on Kubernetes (ECK) integration for logging and monitoring capabilities.", ) eckArgGroup.add_argument( "--eck", @@ -1332,149 +1286,134 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="", action="store_const", - const="install" + const="install", ) eckArgGroup.add_argument( - "--eck-enable-logstash", - required=False, - help="", - action="store_true" -) -eckArgGroup.add_argument( - "--eck-remote-es-hosts", - required=False, - help="" -) -eckArgGroup.add_argument( - "--eck-remote-es-username", - required=False, - help="" -) -eckArgGroup.add_argument( - "--eck-remote-es-password", - required=False, - help="" + "--eck-enable-logstash", required=False, help="", action="store_true" ) +eckArgGroup.add_argument("--eck-remote-es-hosts", required=False, help="") +eckArgGroup.add_argument("--eck-remote-es-username", required=False, help="") +eckArgGroup.add_argument("--eck-remote-es-password", required=False, help="") # Kafka - Common # ----------------------------------------------------------------------------- kafkaCommonArgGroup = installArgParser.add_argument_group( "Kafka - Common", - "Common Kafka configuration options including provider selection (Strimzi, Red Hat AMQ Streams, IBM Event Streams, or AWS MSK) and authentication credentials." + "Common Kafka configuration options including provider selection (Strimzi, Red Hat AMQ Streams, IBM Event Streams, or AWS MSK) and authentication credentials.", ) kafkaCommonArgGroup.add_argument( "--kafka-provider", required=False, help="Kafka provider: redhat (Red Hat AMQ Streams), strimzi, ibm (IBM Event Streams), or aws (AWS MSK)", choices=KAFKA_PROVIDERS, - metavar="{strimzi,redhat,ibm,aws}" + metavar="{strimzi,redhat,ibm,aws}", ) kafkaCommonArgGroup.add_argument( "--kafka-username", required=False, - help="Kafka instance username (applicable for redhat, strimzi, or aws providers)" + help="Kafka instance username (applicable for redhat, strimzi, or aws providers)", ) kafkaCommonArgGroup.add_argument( "--kafka-password", required=False, - help="Kafka instance password (applicable for redhat, strimzi, or aws providers)" + help="Kafka instance password (applicable for redhat, strimzi, or aws providers)", ) # Kafka - Strimzi & AMQ Streams # ----------------------------------------------------------------------------- kafkaOCPArgGroup = installArgParser.add_argument_group( "Kafka - Strimzi and AMQ Streams", - "Configuration options specific to Strimzi and Red Hat AMQ Streams Kafka deployments including namespace and cluster version." + "Configuration options specific to Strimzi and Red Hat AMQ Streams Kafka deployments including namespace and cluster version.", ) kafkaCommonArgGroup.add_argument( "--kafka-namespace", required=False, - help="Set Kafka namespace. Only applicable if installing `redhat` (Red Hat AMQ Streams) or `strimzi`" + help="Set Kafka namespace. Only applicable if installing `redhat` (Red Hat AMQ Streams) or `strimzi`", ) kafkaOCPArgGroup.add_argument( "--kafka-version", required=False, - help="Set version of the Kafka cluster that the Strimzi or AMQ Streams operator will create" + help="Set version of the Kafka cluster that the Strimzi or AMQ Streams operator will create", ) # Kafka - AWS MSK # ----------------------------------------------------------------------------- mskArgGroup = installArgParser.add_argument_group( "Kafka - AWS MSK", - "Configuration options for Amazon Managed Streaming for Apache Kafka (MSK) including instance type, node count, volume size, CIDR subnets for availability zones, and egress settings." + "Configuration options for Amazon Managed Streaming for Apache Kafka (MSK) including instance type, node count, volume size, CIDR subnets for availability zones, and egress settings.", ) mskArgGroup.add_argument( "--msk-instance-type", dest="aws_msk_instance_type", required=False, - help="Set the MSK instance type" + help="Set the MSK instance type", ) mskArgGroup.add_argument( "--msk-instance-nodes", dest="aws_msk_instance_number", required=False, - help="Set total number of MSK instance nodes" + help="Set total number of MSK instance nodes", ) mskArgGroup.add_argument( "--msk-instance-volume-size", dest="aws_msk_volume_size", required=False, - help="Set storage/volume size for the MSK instance" + help="Set storage/volume size for the MSK instance", ) mskArgGroup.add_argument( "--msk-cidr-az1", dest="aws_msk_cidr_az1", required=False, - help="Set the CIDR subnet for availability zone 1 for the MSK instance" + help="Set the CIDR subnet for availability zone 1 for the MSK instance", ) mskArgGroup.add_argument( "--msk-cidr-az2", dest="aws_msk_cidr_az2", required=False, - help="Set the CIDR subnet for availability zone 2 for the MSK instance" + help="Set the CIDR subnet for availability zone 2 for the MSK instance", ) mskArgGroup.add_argument( "--msk-cidr-az3", dest="aws_msk_cidr_az3", required=False, - help="Set the CIDR subnet for availability zone 3 for the MSK instance" + help="Set the CIDR subnet for availability zone 3 for the MSK instance", ) mskArgGroup.add_argument( "--msk-cidr-egress", dest="aws_msk_egress_cidr", required=False, - help="Set the CIDR for egress connectivity" + help="Set the CIDR for egress connectivity", ) mskArgGroup.add_argument( "--msk-cidr-ingress", dest="aws_msk_ingress_cidr", required=False, - help="Set the CIDR for ingress connectivity" + help="Set the CIDR for ingress connectivity", ) # Kafka - Event Streams # ----------------------------------------------------------------------------- eventstreamsArgGroup = installArgParser.add_argument_group( "Kafka - Event Streams", - "Configuration options for IBM Event Streams including resource group, instance name, and location." + "Configuration options for IBM Event Streams including resource group, instance name, and location.", ) eventstreamsArgGroup.add_argument( "--eventstreams-resource-group", dest="eventstreams_resourcegroup", required=False, - help="Set IBM Cloud resource group to target the Event Streams instance provisioning" + help="Set IBM Cloud resource group to target the Event Streams instance provisioning", ) eventstreamsArgGroup.add_argument( "--eventstreams-instance-name", dest="eventstreams_name", required=False, - help="Set IBM Event Streams instance name" + help="Set IBM Event Streams instance name", ) eventstreamsArgGroup.add_argument( "--eventstreams-instance-location", dest="eventstreams_location", required=False, - help="Set IBM Event Streams instance location" + help="Set IBM Event Streams instance location", ) # COS @@ -1485,31 +1424,31 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: dest="cos_type", required=False, help="Set cloud object storage provider. Supported options are `ibm` and `ocs`", - choices=["ibm", "ocs"] + choices=["ibm", "ocs"], ) cosArgGroup.add_argument( "--cos-resourcegroup", dest="cos_resourcegroup", required=False, - help="When using IBM COS, set the resource group where the instance will run" + help="When using IBM COS, set the resource group where the instance will run", ) cosArgGroup.add_argument( "--cos-apikey", dest="cos_apikey", required=False, - help="When using IBM COS, set COS priviledged apikey for IBM Cloud" + help="When using IBM COS, set COS priviledged apikey for IBM Cloud", ) cosArgGroup.add_argument( "--cos-instance-name", dest="cos_instance_name", required=False, - help="When using IBM COS, set COS instance name to be used/created" + help="When using IBM COS, set COS instance name to be used/created", ) cosArgGroup.add_argument( "--cos-bucket-name", dest="cos_bucket_name", required=False, - help="When using IBM COS, set COS bucket name to be used/created" + help="When using IBM COS, set COS bucket name to be used/created", ) @@ -1517,150 +1456,146 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: # ----------------------------------------------------------------------------- cloudArgGroup = installArgParser.add_argument_group( "Cloud Providers", - "Configure cloud provider settings including AWS region, availability zones, and IBM Cloud API key." + "Configure cloud provider settings including AWS region, availability zones, and IBM Cloud API key.", ) cloudArgGroup.add_argument( - "--ibmcloud-apikey", - required=False, - help="Set IBM Cloud API Key" + "--ibmcloud-apikey", required=False, help="Set IBM Cloud API Key" ) cloudArgGroup.add_argument( - "--aws-region", - required=False, - help="Set target AWS region for the MSK instance" + "--aws-region", required=False, help="Set target AWS region for the MSK instance" ) cloudArgGroup.add_argument( "--aws-access-key-id", required=False, - help="Set AWS access key ID for the target AWS account" + help="Set AWS access key ID for the target AWS account", ) cloudArgGroup.add_argument( "--secret-access-key", required=False, - help="Set AWS secret access key for the target AWS account" + help="Set AWS secret access key for the target AWS account", ) cloudArgGroup.add_argument( "--aws-vpc-id", required=False, - help="Set target Virtual Private Cloud ID for the MSK instance" + help="Set target Virtual Private Cloud ID for the MSK instance", ) # Approvals # ----------------------------------------------------------------------------- approvalsGroup = installArgParser.add_argument_group( "Integrated Approval Workflow", - "Configure approval checkpoints during installation for Core Platform and each MAS application workspace (Assist, IoT, Manage, Monitor, Optimizer, Predict, Visual Inspection, Facilities, and AI Service). Format: MAX_RETRIES:RETRY_DELAY:IGNORE_FAILURE" + "Configure approval checkpoints during installation for Core Platform and each MAS application workspace (Assist, IoT, Manage, Monitor, Optimizer, Predict, Visual Inspection, Facilities, and AI Service). Format: MAX_RETRIES:RETRY_DELAY:IGNORE_FAILURE", ) approvalsGroup.add_argument( "--approval-core", default="", - help="Require approval after the Core Platform has been configured" + help="Require approval after the Core Platform has been configured", ) approvalsGroup.add_argument( "--approval-assist", default="", - help="Require approval after the Maximo Assist workspace has been configured" + help="Require approval after the Maximo Assist workspace has been configured", ) approvalsGroup.add_argument( "--approval-iot", default="", - help="Require approval after the Maximo IoT workspace has been configured" + help="Require approval after the Maximo IoT workspace has been configured", ) approvalsGroup.add_argument( "--approval-manage", default="", - help="Require approval after the Maximo Manage workspace has been configured" + help="Require approval after the Maximo Manage workspace has been configured", ) approvalsGroup.add_argument( "--approval-monitor", default="", - help="Require approval after the Maximo Monitor workspace has been configured" + help="Require approval after the Maximo Monitor workspace has been configured", ) approvalsGroup.add_argument( "--approval-optimizer", default="", - help="Require approval after the Maximo Optimizer workspace has been configured" + help="Require approval after the Maximo Optimizer workspace has been configured", ) approvalsGroup.add_argument( "--approval-predict", default="", - help="Require approval after the Maximo Predict workspace has been configured" + help="Require approval after the Maximo Predict workspace has been configured", ) approvalsGroup.add_argument( "--approval-visualinspection", default="", - help="Require approval after the Maximo Visual Inspection workspace has been configured" + help="Require approval after the Maximo Visual Inspection workspace has been configured", ) approvalsGroup.add_argument( "--approval-facilities", default="", - help="Require approval after the Maximo Real Estate and Facilities workspace has been configured" + help="Require approval after the Maximo Real Estate and Facilities workspace has been configured", ) approvalsGroup.add_argument( "--approval-aiservice", default="", - help="Require approval after the AI Service has been configured" + help="Require approval after the AI Service has been configured", ) # More Options # ----------------------------------------------------------------------------- otherArgGroup = installArgParser.add_argument_group( "More", - "Additional options including advanced/simplified mode toggles, license acceptance, development mode, Artifactory credentials, PVC wait control, pre-check skip, confirmation prompts, image pull policy, and custom service account." + "Additional options including advanced/simplified mode toggles, license acceptance, development mode, Artifactory credentials, PVC wait control, pre-check skip, confirmation prompts, image pull policy, and custom service account.", ) otherArgGroup.add_argument( "--artifactory-username", required=False, - help="Username for access to development builds on Artifactory" + help="Username for access to development builds on Artifactory", ) otherArgGroup.add_argument( "--artifactory-token", required=False, - help="API Token for access to development builds on Artifactory" + help="API Token for access to development builds on Artifactory", ) otherArgGroup.add_argument( "--advanced", action="store_true", default=False, - help="Show advanced install options (in interactive mode)" + 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)" + help="Don't show advanced install options (in interactive mode)", ) otherArgGroup.add_argument( "--slack-token", dest="slack_token", required=False, - help="Slack bot token for sending pipeline notifications" + 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)" + help="Slack channel(s) for notifications (comma-separated for multiple channels)", ) otherArgGroup.add_argument( "--accept-license", action="store_true", default=False, - help="Accept all license terms without prompting" + 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" + 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" + help="Disable the 'pre-install-check' at the start of the install pipeline", ) otherArgGroup.add_argument( @@ -1668,14 +1603,14 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, action="store_true", default=False, - help="Skip CLI application of pre-install MAS RBAC. Use this when an OpenShift administrator has already applied the required RBAC." + help="Skip CLI application of pre-install MAS RBAC. Use this when an OpenShift administrator has already applied the required RBAC.", ) otherArgGroup.add_argument( "--no-confirm", required=False, action="store_true", default=False, - help="Launch the installation without prompting for confirmation" + help="Launch the installation without prompting for confirmation", ) otherArgGroup.add_argument( "--image-pull-policy", @@ -1683,18 +1618,15 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Image pull policy for Tekton Pipeline", choices=IMAGE_PULL_POLICIES, - metavar="{IfNotPresent,Always}" + metavar="{IfNotPresent,Always}", ) otherArgGroup.add_argument( "--service-account", dest="service_account_name", required=False, - help="Custom service account for install pipeline (disables default 'pipeline' service account creation)" + help="Custom service account for install pipeline (disables default 'pipeline' service account creation)", ) otherArgGroup.add_argument( - "-h", "--help", - action="help", - default=False, - help="Show this help message and exit" + "-h", "--help", action="help", default=False, help="Show this help message and exit" ) diff --git a/python/src/mas/cli/install/params.py b/python/src/mas/cli/install/params.py index 9bbc033b751..fd36b28a537 100644 --- a/python/src/mas/cli/install/params.py +++ b/python/src/mas/cli/install/params.py @@ -23,7 +23,7 @@ # DRO "dro_contact_email", "dro_contact_firstname", - "dro_contact_lastname" + "dro_contact_lastname", ] optionalParams = [ @@ -60,6 +60,7 @@ "mas_manage_ws_db_encryptionsecret", "mas_app_settings_server_timezone", "mas_appws_bindings_jdbc_manage", + "mas_appws_bindings_kafka_manage", "mas_appws_components", "mas_appws_bindings_health_wsl_flag", "mas_domain", @@ -189,7 +190,6 @@ "aiservice_s3_bucket_prefix", "aiservice_s3_tenants_bucket", "aiservice_s3_templates_bucket", - "aiservice_watsonxai_apikey", "aiservice_watsonxai_url", "aiservice_watsonxai_project_id", @@ -204,30 +204,24 @@ "aiservice_instance_id", "aiservice_watsonxai_instance_id", "aiservice_watsonxai_verify", - "minio_root_user", "minio_root_password", - "tenant_entitlement_type", "tenant_entitlement_start_date", "tenant_entitlement_end_date", - "rsl_url", "rsl_org_id", "rsl_token", "rsl_ca_crt", "environment_type", "configure_aiassistant", - # Certificate Issuer "aiservice_certificate_issuer", - # Grafana "skip_grafana_install", "grafana_v5_namespace", "grafana_instance_storage_size", - # Slack Integration "slack_token", - "slack_channel" + "slack_channel", ] diff --git a/python/src/mas/cli/install/settings/kafkaSettings.py b/python/src/mas/cli/install/settings/kafkaSettings.py index 28c21741348..d50e7cb9098 100644 --- a/python/src/mas/cli/install/settings/kafkaSettings.py +++ b/python/src/mas/cli/install/settings/kafkaSettings.py @@ -11,15 +11,13 @@ from typing import TYPE_CHECKING, Dict, List, NoReturn from os import path from prompt_toolkit import print_formatted_text -from mas.devops.utils import isVersionEqualOrAfter - if TYPE_CHECKING: from prompt_toolkit.completion import WordCompleter from prompt_toolkit.validation import Validator -class KafkaSettingsMixin(): +class KafkaSettingsMixin: if TYPE_CHECKING: # Attributes from BaseApp and other mixins params: Dict[str, str] @@ -27,6 +25,7 @@ class KafkaSettingsMixin(): installManage: bool showAdvancedOptions: bool localConfigDir: str | None + enableKafkaImageProcessor: bool # Methods from BaseApp def setParam(self, param: str, value: str) -> None: @@ -76,20 +75,14 @@ def selectLocalConfigDir(self) -> None: def _requiresKafkaIoT(self) -> bool: return self.installIoT - def _requiresKafkaCivil(self) -> bool: - isCivilEnabled = self.installManage and "civil=" in self.getParam("mas_appws_components") - if isCivilEnabled: - manageChannel = self.getParam("mas_app_channel_manage") - if manageChannel and isVersionEqualOrAfter('9.2.0', manageChannel): - return True - return False - def _getKafkaRequirements(self) -> List[str]: requirements = [] if self._requiresKafkaIoT(): requirements.append("Maximo IoT") - if self._requiresKafkaCivil(): - requirements.append("Manage Civil Infrastructure (9.2+) Defect Detection") + if self.enableKafkaImageProcessor: + requirements.append( + "Manage Civil Infrastructure (9.2+) Kafka Image Processor" + ) return requirements def configKafka(self) -> None: @@ -100,20 +93,28 @@ def configKafka(self) -> None: # Build description based on what requires Kafka hasIoT = self._requiresKafkaIoT() - hasCivil = self._requiresKafkaCivil() + hasImageProcessor = self.enableKafkaImageProcessor description = [] - if hasIoT and hasCivil: - description.append("Maximo IoT and Manage Civil Infrastructure (9.2+) Defect Detection require a shared system-scope Kafka instance") + if hasIoT and hasImageProcessor: + description.append( + "Maximo IoT and Manage Civil Infrastructure Kafka Image Processor require a shared system-scope Kafka instance" + ) elif hasIoT: - description.append("Maximo IoT requires a shared system-scope Kafka instance") - elif hasCivil: - description.append("Manage Civil Infrastructure (9.2+) Defect Detection functionality requires a shared system-scope Kafka instance") - - description.extend([ - "Supported Kafka providers: Strimzi, Red Hat AMQ Streams, IBM Cloud Event Streams and AWS MSK", - "You may also choose to configure MAS to use an existing Kafka instance by providing a pre-existing configuration file" - ]) + description.append( + "Maximo IoT requires a shared system-scope Kafka instance" + ) + elif hasImageProcessor: + description.append( + "Manage Civil Infrastructure Kafka Image Processor requires a shared system-scope Kafka instance" + ) + + description.extend( + [ + "Supported Kafka providers: Strimzi, Red Hat AMQ Streams, IBM Cloud Event Streams and AWS MSK", + "You may also choose to configure MAS to use an existing Kafka instance by providing a pre-existing configuration file", + ] + ) self.printDescription(description) if self.yesOrNo("Create system Kafka instance using one of the supported providers"): @@ -187,7 +188,7 @@ def configKafka(self) -> None: else: self.setParam("kafka_action_system", "byo") self.selectLocalConfigDir() - instanceId = self.getParam('mas_instance_id') + instanceId = self.getParam("mas_instance_id") # Check if a configuration already exists assert self.localConfigDir is not None, "localConfigDir must be set" diff --git a/python/src/mas/cli/install/settings/manageSettings.py b/python/src/mas/cli/install/settings/manageSettings.py index 36dd3f069c1..b8f8e61afd3 100644 --- a/python/src/mas/cli/install/settings/manageSettings.py +++ b/python/src/mas/cli/install/settings/manageSettings.py @@ -18,6 +18,7 @@ from mas.devops.utils import isVersionEqualOrAfter import logging + logger = logging.getLogger(__name__) @@ -26,7 +27,7 @@ from prompt_toolkit.validation import Validator -class ManageSettingsMixin(): +class ManageSettingsMixin: if TYPE_CHECKING: # Attributes from BaseApp and other mixins params: Dict[str, str] @@ -143,6 +144,23 @@ def manageSettingsComponents(self) -> None: self.params["mas_appws_components"] += ",aviation=latest" if self.yesOrNo(" - Civil Infrastructure"): self.params["mas_appws_components"] += ",civil=latest" + + # Check if Manage version supports Kafka Image Processor (9.2+) + manageChannel = self.getParam("mas_app_channel_manage") + if manageChannel and isVersionEqualOrAfter("9.2.0", manageChannel): + self.printDescription( + [ + "", + "Civil Infrastructure Defect Detection with Kafka Image Processor:", + "The Kafka Image Processor enables advanced defect detection capabilities.", + "This requires a Kafka instance and uses 10GB of storage for image processing.", + ] + ) + + if self.yesOrNo("Enable Kafka Image Processor for Civil Infrastructure"): + self.enableKafkaImageProcessor = True + # Bind Manage to system Kafka (similar to JDBC binding pattern) + self.setParam("mas_appws_bindings_kafka_manage", "system") if self.yesOrNo(" - Envizi"): self.params["mas_appws_components"] += ",envizi=latest" if self.yesOrNo(" - Health"): @@ -179,7 +197,7 @@ def manageSettingsComponents(self) -> None: self.params["mas_appws_components"] += ",vegm=latest" # Collaborate is only available in Manage 9.2 or higher manageChannel = self.getParam("mas_app_channel_manage") - if manageChannel and isVersionEqualOrAfter('9.2.0', manageChannel): + if manageChannel and isVersionEqualOrAfter("9.2.0", manageChannel): if self.yesOrNo(" - Collaborate"): self.params["mas_appws_components"] += ",collaborate=latest" logger.debug(f"Generated mas_appws_components = {self.params['mas_appws_components']}") @@ -214,21 +232,25 @@ def manageSettingsServerBundleConfig(self) -> None: if not self.isManageFoundation: if self.showAdvancedOptions: self.printH2(f"Maximo {self.manageAppName} Settings - Server Bundles") - self.printDescription([ - f"Define how you want to configure {self.manageAppName} servers:", - f" - You can have one or multiple {self.manageAppName} servers distributing workload", - " - Additionally, you can choose to include JMS server for messaging queues", - "", - "Configurations:", - " 1. Deploy the 'all' server pod only (workload is concentrated in just one server pod but consumes less resource)", - " 2. Deploy the 'all' and 'jms' bundle pods (workload is concentrated in just one server pod and includes jms server)" - ]) + self.printDescription( + [ + f"Define how you want to configure {self.manageAppName} servers:", + f" - You can have one or multiple {self.manageAppName} servers distributing workload", + " - Additionally, you can choose to include JMS server for messaging queues", + "", + "Configurations:", + " 1. Deploy the 'all' server pod only (workload is concentrated in just one server pod but consumes less resource)", + " 2. Deploy the 'all' and 'jms' bundle pods (workload is concentrated in just one server pod and includes jms server)", + ] + ) if not self.isSNO(): - self.printDescription([ - " 3. Deploy the 'mea', 'report', 'ui' and 'cron' bundle pods (workload is distributed across multiple server pods)", - " 4. Deploy the 'mea', 'report', 'ui', 'cron' and 'jms' bundle pods (workload is distributed across multiple server pods and includes jms server)" - ]) + self.printDescription( + [ + " 3. Deploy the 'mea', 'report', 'ui' and 'cron' bundle pods (workload is distributed across multiple server pods)", + " 4. Deploy the 'mea', 'report', 'ui', 'cron' and 'jms' bundle pods (workload is distributed across multiple server pods and includes jms server)", + ] + ) manageServerBundleSelection = self.promptForString("Select a server bundle configuration") @@ -249,60 +271,77 @@ def manageSettingsServerBundleConfig(self) -> None: def manageSettingsJMS(self) -> None: if self.getParam("mas_app_settings_server_bundles_size") in ["jms", "snojms"]: - self.printDescription([ - f"Only {self.manageAppName} JMS sequential queues (sqin and sqout) are enabled by default.", - "However, you can enable both sequential (sqin and sqout) and continuous queues (cqin and cqout)" - ]) - - self.yesOrNo(f"Enable both {self.manageAppName} JMS sequential and continuous queues", "mas_app_settings_default_jms") + self.printDescription( + [ + f"Only {self.manageAppName} JMS sequential queues (sqin and sqout) are enabled by default.", + "However, you can enable both sequential (sqin and sqout) and continuous queues (cqin and cqout)", + ] + ) + + self.yesOrNo( + f"Enable both {self.manageAppName} JMS sequential and continuous queues", + "mas_app_settings_default_jms", + ) def manageSettingsCustomizationArchive(self) -> None: # Only ask about customization archive in full Manage installation if not self.isManageFoundation: self.printH2(f"Maximo {self.manageAppName} Settings - Customization") - self.printDescription([ - f"Provide a customization archive to be used in the {self.manageAppName} build process" - ]) + self.printDescription([f"Provide a customization archive to be used in the {self.manageAppName} build process"]) if self.yesOrNo("Include customization archive"): self.promptForString("Customization archive name", "mas_app_settings_customization_archive_name") self.promptForString("Customization archive path/url", "mas_app_settings_customization_archive_url") if self.yesOrNo("Provide authentication to access customization archive URL"): self.promptForString("Username", "mas_app_settings_customization_archive_username") - self.promptForString("Password", "mas_app_settings_customization_archive_password", isPassword=True) # pragma: allowlist secret + self.promptForString("Password", "mas_app_settings_customization_archive_password", isPassword=True) def manageSettingsDemodata(self) -> None: self.yesOrNo("Create demo data", "mas_app_settings_demodata") def manageSettingsTimezone(self) -> None: - self.promptForString(f"{self.manageAppName} server timezone", "mas_app_settings_server_timezone", default="GMT") + self.promptForString( + f"{self.manageAppName} server timezone", + "mas_app_settings_server_timezone", + default="GMT", + ) # Set Manage dedicated Db2 instance timezone to be same as Manage server timezone self.setParam("db2_timezone", self.getParam("mas_app_settings_server_timezone")) def manageSettingsLanguages(self) -> None: self.printH2(f"Maximo {self.manageAppName} Settings - Languages") - self.printDescription([ - f"Define the base language for Maximo {self.manageAppName}" - ]) - baseLanguage = self.promptForString("Base language", validator=LanguageValidator(self.supportedLanguages), completer=WordCompleter(self.supportedLanguages)) + self.printDescription([f"Define the base language for Maximo {self.manageAppName}"]) + baseLanguage = self.promptForString( + "Base language", + validator=LanguageValidator(self.supportedLanguages), + completer=WordCompleter(self.supportedLanguages), + ) self.setParam("mas_app_settings_base_lang", baseLanguage.upper()) - self.printDescription([ - f"Define the additional languages to be configured in Maximo {self.manageAppName}. Provide a comma-separated list of the supported languages indexes, for example: 'DA,EN,ZH-TW'", - "A complete list of available language codes is available online:", - " https://www.ibm.com/docs/en/mas-cd/mhmpmh-and-p-u/continuous-delivery?topic=deploy-language-support" - ]) - - secondaryLanguages = self.promptForString("Secondary language", validator=LanguageValidator(self.supportedLanguages), completer=WordCompleter(self.supportedLanguages)) + self.printDescription( + [ + f"Define the additional languages to be configured in Maximo {self.manageAppName}. Provide a comma-separated list of the supported languages indexes, for example: 'DA,EN,ZH-TW'", + "A complete list of available language codes is available online:", + " https://www.ibm.com/docs/en/mas-cd/mhmpmh-and-p-u/continuous-delivery?topic=deploy-language-support", + ] + ) + + secondaryLanguages = self.promptForString( + "Secondary language", + validator=LanguageValidator(self.supportedLanguages), + completer=WordCompleter(self.supportedLanguages), + ) self.setParam("mas_app_settings_secondary_langs", secondaryLanguages.upper()) def manageSettingsCP4D(self) -> None: if self.getParam("mas_app_channel_manage") in ["8.7.x", "9.0.x"] and self.showAdvancedOptions: - self.printDescription([ - f"Integration with Cognos Analytics provides additional support for reporting features in Maximo {self.manageAppName}, for more information refer to the documentation online: ", - " - https://ibm.biz/BdMuxs" - ]) + self.printDescription( + [ + f"Integration with Cognos Analytics provides additional support for reporting features in Maximo {self.manageAppName}, for more information refer to the documentation online: ", + " - https://ibm.biz/BdMuxs", + ] + ) self.yesOrNo("Enable integration with Cognos Analytics", "cpd_install_cognos") self.yesOrNo("Enable integration with Watson Studio Local", "mas_appws_bindings_health_flag") @@ -311,13 +350,40 @@ def manageSettingsCP4D(self) -> None: def manageSettingsOther(self) -> None: self.printH2(f"Maximo {self.manageAppName} Settings - Other") - self.supportedLanguages = ["AR", "CS", "DA", "DE", "EN", "ES", "FI", "FR", "HE", "HR", "HU", "IT", "JA", "KO", "NL", "NO", "PL", "PT-BR", "RU", "SK", "SL", "SV", "TR", "UK", "ZH-CN", "ZH-TW"] + self.supportedLanguages = [ + "AR", + "CS", + "DA", + "DE", + "EN", + "ES", + "FI", + "FR", + "HE", + "HR", + "HU", + "IT", + "JA", + "KO", + "NL", + "NO", + "PL", + "PT-BR", + "RU", + "SK", + "SL", + "SV", + "TR", + "UK", + "ZH-CN", + "ZH-TW", + ] if self.isManageFoundation: if self.showAdvancedOptions: self.printDescription([ "Configure additional settings:", " - Base and additional languages", - " - Server timezone" + " - Server timezone", ]) self.manageSettingsTimezone() self.manageSettingsLanguages() @@ -329,7 +395,7 @@ def manageSettingsOther(self) -> None: " - Base and additional languages", " - Server timezone", " - Cognos integration (install Cloud Pak for Data)", - " - Watson Studio Local integration (install Cloud Pak for Data)" + " - Watson Studio Local integration (install Cloud Pak for Data)", ]) self.manageSettingsDemodata() self.manageSettingsTimezone() @@ -354,13 +420,13 @@ def manageSettingsAiService(self) -> None: return else: # Set aiservice instance id from the first instance fetched from cluster - self.setParam("manage_bind_aiservice_instance_id", aiserviceInstances[0]['metadata']['name']) + self.setParam("manage_bind_aiservice_instance_id", aiserviceInstances[0]["metadata"]["name"]) self.printH2(f"Maximo {self.manageAppName} Settings - AI Service Tenant Configuration") self.printDescription([ "Select an AI Service Tenant ID to bind with Manage:", - " - The selected AI Service Tenant will be used in Manage AI Config Application" + " - The selected AI Service Tenant will be used in Manage AI Config Application", ]) if self.installAIService: @@ -375,10 +441,17 @@ def manageSettingsAiService(self) -> None: aiserviceTenantOptions = [] for aiserviceTenant in aiserviceTenantInstances: print_formatted_text(HTML(f"- {aiserviceTenant['metadata']['name'].split('-')[-1]}")) - aiserviceTenantOptions.append(aiserviceTenant['metadata']['name'].split('-')[-1]) + aiserviceTenantOptions.append(aiserviceTenant["metadata"]["name"].split("-")[-1]) aiserviceTenantCompleter = WordCompleter(aiserviceTenantOptions) print() - aiserviceTenantInstanceId = self.promptForString('Enter AI Service Tenant ID to bind with Manage: ', completer=aiserviceTenantCompleter, validator=AiserviceTeanantIDValidator(self.getParam("manage_bind_aiservice_instance_id"), self.installAIService)) + aiserviceTenantInstanceId = self.promptForString( + "Enter AI Service Tenant ID to bind with Manage: ", + completer=aiserviceTenantCompleter, + validator=AiserviceTeanantIDValidator( + self.getParam("manage_bind_aiservice_instance_id"), + self.installAIService, + ), + ) self.setParam("manage_bind_aiservice_tenant_id", aiserviceTenantInstanceId) diff --git a/python/src/mas/cli/install/summarizer.py b/python/src/mas/cli/install/summarizer.py index a109dacbda7..5b375331ad5 100644 --- a/python/src/mas/cli/install/summarizer.py +++ b/python/src/mas/cli/install/summarizer.py @@ -10,18 +10,81 @@ import logging import yaml +from typing import TYPE_CHECKING from prompt_toolkit import print_formatted_text, HTML from mas.devops.ocp import getConsoleURL +if TYPE_CHECKING: + from typing import Dict + logger = logging.getLogger(__name__) -class InstallSummarizerMixin(): +class InstallSummarizerMixin: + if TYPE_CHECKING: + from typing import List, NoReturn + from openshift.dynamic import DynamicClient + + # Attributes from BaseApp and other mixins + params: Dict[str, str] + enableKafkaImageProcessor: bool + architecture: str + storageClassProvider: str + operationalMode: int + manualCertsDir: str | None + localConfigDir: str | None + slsLicenseFileLocal: str | None + aiserviceTenantSchedulingConfigFileLocal: str | None + deployCP4D: bool + installAssist: bool + installIoT: bool + installMonitor: bool + installManage: bool + installPredict: bool + installInspection: bool + installOptimizer: bool + installFacilities: bool + installAIService: bool + installArcgis: bool + dynamicClient: DynamicClient + + # Methods from BaseApp + def getParam(self, param: str) -> str: + ... + + def isSNO(self) -> bool: + ... + + def isAirgap(self) -> bool: + ... + + def fatalError(self, message: str, exception: Exception | None = None) -> NoReturn: + ... + + # Methods from PrintMixin + def printH1(self, message: str) -> None: + ... + + def printH2(self, message: str) -> None: + ... + + def printDescription(self, content: List[str]) -> None: + ... + + def printSummary(self, label: str, value: str | None) -> None: + ... + + def printParamSummary(self, label: str, param: str) -> None: + ... + def ocpSummary(self) -> None: 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.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) @@ -53,7 +116,7 @@ def masSummary(self) -> None: self.printParamSummary("Mas Certificate Issuer Kind", "mas_issuer_kind") self.printSummary( "Apply Pre-Install MAS RBAC", - "No" if self.getParam("skip_preinstall_rbac") == "true" else "Yes" + "No" if self.getParam("skip_preinstall_rbac") == "true" else "Yes", ) if self.isAirgap(): self.printSummary("Install Mode", "Disconnected Install") @@ -66,21 +129,21 @@ def masSummary(self) -> None: self.printParamSummary("DNS Provider", "dns_provider") self.printParamSummary("Certificate Issuer", "mas_cluster_issuer") - if self.getParam('ocp_ingress') != "": + if self.getParam("ocp_ingress") != "": self.printParamSummary("OCP Ingress", "ocp_ingress") - if self.getParam('dns_provider') == "cloudflare": + if self.getParam("dns_provider") == "cloudflare": self.printParamSummary("CloudFlare e-mail", "cloudflare_email") self.printParamSummary("CloudFlare API token", "cloudflare_apitoken") self.printParamSummary("CloudFlare zone", "cloudflare_zone") self.printParamSummary("CloudFlare subdomain", "cloudflare_subdomain") - elif self.getParam('dns_provider') == "cis": + elif self.getParam("dns_provider") == "cis": self.printParamSummary("CIS e-mail", "cis_email") self.printParamSummary("CIS API Key", "cis_apikey") self.printParamSummary("CIS CRN", "cis_crn") self.printParamSummary("CIS subdomain", "cis_subdomain") - elif self.getParam('dns_provider') == "route53": + elif self.getParam("dns_provider") == "route53": pass - elif self.getParam('dns_provider') == "": + elif self.getParam("dns_provider") == "": pass print() @@ -108,7 +171,7 @@ def masSummary(self) -> None: print() self.printParamSummary("Catalog Version", "mas_catalog_version") # We only list the digest if it's specified (primary use case is when running development builds in airgap environments) - if self.getParam("mas_catalog_digest" != ""): + if self.getParam("mas_catalog_digest") != "": self.printParamSummary("Catalog Digest", "mas_catalog_digest") self.printParamSummary("Subscription Channel", "mas_channel") @@ -141,8 +204,14 @@ def masSummary(self) -> None: def iotSummary(self) -> None: if self.installIoT: self.printSummary("IoT", self.params["mas_app_channel_iot"]) - self.printSummary("+ MQTT Broker Storage Class", self.params["mas_app_settings_iot_mqttbroker_pvc_storage_class"]) - self.printSummary("+ FPL Storage Class", self.params["mas_app_settings_iot_fpl_pvc_storage_class"]) + self.printSummary( + "+ MQTT Broker Storage Class", + self.params["mas_app_settings_iot_mqttbroker_pvc_storage_class"], + ) + self.printSummary( + "+ FPL Storage Class", + self.params["mas_app_settings_iot_fpl_pvc_storage_class"], + ) else: self.printSummary("IoT", "Do Not Install") @@ -186,29 +255,50 @@ def inspectionSummary(self) -> None: def manageSummary(self) -> None: if self.installManage: - self.printSummary(f"{'Manage foundation' if self.getParam('mas_appws_components') == '' else 'Manage'}", self.params["mas_app_channel_manage"]) + self.printSummary( + f"{'Manage foundation' if self.getParam('mas_appws_components') == '' else 'Manage'}", + self.params["mas_app_channel_manage"], + ) if self.getParam("mas_appws_components") != "": print_formatted_text(HTML(" + Components")) - self.printSummary(" + ACM", "Enabled" if "acm=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + Aviation", "Enabled" if "aviation=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + Civil Infrastructure", "Enabled" if "civil=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + Envizi", "Enabled" if "envizi=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + Health", "Enabled" if "health=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + HSE", "Enabled" if "hse=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + Maximo IT", "Enabled" if "icd=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + Nuclear", "Enabled" if "nuclear=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + Oil & Gas", "Enabled" if "oilandgas=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + Connector for Oracle", "Enabled" if "oracleadapter=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + Connector for SAP", "Enabled" if "sapadapter=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + Service Provider", "Enabled" if "serviceprovider=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + Spatial", "Enabled" if "spatial=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + Strategize", "Enabled" if "strategize=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + Transportation", "Enabled" if "transportation=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + Tririga", "Enabled" if "tririga=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + Utilities", "Enabled" if "utilities=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + Workday Applications", "Enabled" if "workday=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + AIP", "Enabled" if "aip=" in self.getParam("mas_appws_components") else "Disabled") - self.printSummary(" + Vegetation Management", "Enabled" if "vegm=" in self.getParam("mas_appws_components") else "Disabled") + + # Define components with their display names and component IDs + components = [ + ("ACM", "acm"), + ("Aviation", "aviation"), + ("Civil Infrastructure", "civil"), + ("Envizi", "envizi"), + ("Health", "health"), + ("HSE", "hse"), + ("Maximo IT", "icd"), + ("Nuclear", "nuclear"), + ("Oil & Gas", "oilandgas"), + ("Connector for Oracle", "oracleadapter"), + ("Connector for SAP", "sapadapter"), + ("Service Provider", "serviceprovider"), + ("Spatial", "spatial"), + ("Strategize", "strategize"), + ("Transportation", "transportation"), + ("Tririga", "tririga"), + ("Utilities", "utilities"), + ("Workday Applications", "workday"), + ("AIP", "aip"), + ("Vegetation Management", "vegm"), + ] + + componentsStr = self.getParam("mas_appws_components") + for displayName, componentId in components: + isEnabled = f"{componentId}=" in componentsStr + self.printSummary(f" + {displayName}", "Enabled" if isEnabled else "Disabled") + + # Special handling for Civil Infrastructure Kafka Image Processor + if componentId == "civil" and isEnabled: + self.printSummary( + " + Kafka Image Processor", + "Enabled" if self.enableKafkaImageProcessor else "Disabled" + ) + if self.enableKafkaImageProcessor: + self.printParamSummary(" + Kafka Binding", "mas_appws_bindings_kafka_manage") self.printParamSummary("+ Upgrade Type", "mas_appws_upgrade_type") @@ -225,7 +315,10 @@ def manageSummary(self) -> None: if self.getParam("manage_bind_aiservice_tenant_id") != "": print_formatted_text(HTML(" + AI Service Binding (for Manage)")) - self.printParamSummary(" + Bound AI Service Instance ID", "manage_bind_aiservice_instance_id") + self.printParamSummary( + " + Bound AI Service Instance ID", + "manage_bind_aiservice_instance_id", + ) self.printParamSummary(" + Bound AI Service Tenant ID", "manage_bind_aiservice_tenant_id") else: self.printSummary("Manage", "Do Not Install") @@ -236,7 +329,10 @@ def facilitiesSummary(self) -> None: self.printSummary("Facilities", self.params["mas_app_channel_facilities"]) print_formatted_text(HTML(" + Maximo Real Estate and Facilities Settings")) self.printParamSummary(" + Size", "mas_ws_facilities_size") - self.printParamSummary(" + Application Object Migration", "mas_ws_facilities_app_om_upgrade_mode") + self.printParamSummary( + " + Application Object Migration", + "mas_ws_facilities_app_om_upgrade_mode", + ) self.printParamSummary(" + Routes Timeout", "mas_ws_facilities_routes_timeout") self.printParamSummary(" + XML Extension", "mas_ws_facilities_liberty_extension_XML") self.printParamSummary(" + AES vault secret name", "mas_ws_facilities_vault_secret") @@ -245,10 +341,16 @@ def facilitiesSummary(self) -> None: self.printParamSummary(" + Log Storage Class ", "mas_ws_facilities_storage_log_class") self.printParamSummary(" + Log Storage Mode", "mas_ws_facilities_storage_log_mode") # self.printParamSummary(" + Log Storage Size", "mas_ws_facilities_storage_log_size") - self.printParamSummary(" + Userfiles Storage Class ", "mas_ws_facilities_storage_userfiles_class") - self.printParamSummary(" + User files Storage Mode", "mas_ws_facilities_storage_userfiles_mode") + self.printParamSummary( + " + Userfiles Storage Class ", + "mas_ws_facilities_storage_userfiles_class", + ) + self.printParamSummary( + " + User files Storage Mode", + "mas_ws_facilities_storage_userfiles_mode", + ) # self.printParamSummary(" + User files Storage Size", "mas_ws_facilities_storage_userfiles_size") - if self.getParam("db2_action_facilities") == 'none': + if self.getParam("db2_action_facilities") == "none": self.printParamSummary(" + Dedicated DB2 Database", "No") else: self.printParamSummary(" + Dedicated DB2 Database", "db2_action_facilities") @@ -270,7 +372,10 @@ def aiServiceSummary(self) -> None: self.printParamSummary("Start Date", "tenant_entitlement_start_date") self.printParamSummary("End Date", "tenant_entitlement_end_date") if self.aiserviceTenantSchedulingConfigFileLocal: - self.printSummary("Scheduling configuration file", self.aiserviceTenantSchedulingConfigFileLocal) + self.printSummary( + "Scheduling configuration file", + self.aiserviceTenantSchedulingConfigFileLocal, + ) self.printH2("S3 Configuration") # self.printParamSummary("Storage provider", "aiservice_s3_provider") @@ -296,8 +401,14 @@ def aiServiceSummary(self) -> None: def db2Summary(self) -> None: if self.getParam("db2_action_system") == "install" or self.getParam("db2_action_manage") == "install": self.printH2("IBM Db2 Univeral Operator Configuration") - self.printSummary("System Instance", "Install" if self.getParam("db2_action_system") == "install" else "Do Not Install") - self.printSummary("Dedicated Manage Instance", "Install" if self.getParam("db2_action_manage") == "install" else "Do Not Install") + self.printSummary( + "System Instance", + ("Install" if self.getParam("db2_action_system") == "install" else "Do Not Install"), + ) + self.printSummary( + "Dedicated Manage Instance", + ("Install" if self.getParam("db2_action_manage") == "install" else "Do Not Install"), + ) self.printParamSummary(" - Type", "db2_type") self.printParamSummary(" - Timezone", "db2_timezone") print() @@ -315,13 +426,19 @@ def db2Summary(self) -> None: self.printParamSummary("Temp Storage", "db2_temp_storage_size") self.printParamSummary("Transaction Logs Storage", "db2_logs_storage_size") print() - if self.getParam('db2_affinity_key') != "": - self.printSummary("Node Affinity", f"{self.getParam('db2_affinity_key')}={self.getParam('db2_affinity_value')}") + if self.getParam("db2_affinity_key") != "": + self.printSummary( + "Node Affinity", + f"{self.getParam('db2_affinity_key')}={self.getParam('db2_affinity_value')}", + ) else: self.printSummary("Node Affinity", "None") - if self.getParam('db2_tolerate_key') != "": - self.printSummary("Node Tolerations", f"{self.getParam('db2_tolerate_key')}={self.getParam('db2_tolerate_value')} @ {self.getParam('db2_tolerate_effect')}") + if self.getParam("db2_tolerate_key") != "": + self.printSummary( + "Node Tolerations", + f"{self.getParam('db2_tolerate_key')}={self.getParam('db2_tolerate_value')} @ {self.getParam('db2_tolerate_effect')}", + ) else: self.printSummary("Node Tolerations", "None") @@ -334,11 +451,23 @@ def cp4dSummary(self) -> None: self.printSummary("Watson Machine Learning", "Install (Required by Maximo Predict)") self.printSummary("Analytics Engine", "Install (Required by Maximo Predict)") else: - self.printSummary("Watson Studio Local", "Install" if self.getParam("cpd_install_ws") == "true" else "Do Not Install") - self.printSummary("Watson Machine Learning", "Install" if self.getParam("cpd_install_wml") == "true" else "Do Not Install") - self.printSummary("Analytics Engine", "Install" if self.getParam("cpd_install_ae") == "true" else "Do Not Install") - - self.printSummary("Cognos Analytics", "Install" if self.getParam("cpd_install_cognos") == "true" else "Do Not Install") + self.printSummary( + "Watson Studio Local", + ("Install" if self.getParam("cpd_install_ws") == "true" else "Do Not Install"), + ) + self.printSummary( + "Watson Machine Learning", + ("Install" if self.getParam("cpd_install_wml") == "true" else "Do Not Install"), + ) + self.printSummary( + "Analytics Engine", + ("Install" if self.getParam("cpd_install_ae") == "true" else "Do Not Install"), + ) + + self.printSummary( + "Cognos Analytics", + ("Install" if self.getParam("cpd_install_cognos") == "true" else "Do Not Install"), + ) def droSummary(self) -> None: self.printH2("IBM Data Reporter Operator (DRO) Configuration") @@ -437,10 +566,7 @@ def installSummary(self) -> None: def displayInstallSummary(self) -> None: self.printH1("Review Settings") - self.printDescription([ - "Connected to:", - f" - {getConsoleURL(self.dynamicClient)}" - ]) + self.printDescription(["Connected to:", f" - {getConsoleURL(self.dynamicClient)}"]) logger.debug("PipelineRun parameters:") logger.debug(yaml.dump(self.params, default_flow_style=False)) diff --git a/tekton/src/params/install.yml.j2 b/tekton/src/params/install.yml.j2 index 41a9426daea..7333624293a 100644 --- a/tekton/src/params/install.yml.j2 +++ b/tekton/src/params/install.yml.j2 @@ -558,6 +558,10 @@ type: string description: Select the JDBC configuration to bind to default: system +- name: mas_appws_bindings_kafka_manage + type: string + description: Select the Kafka configuration to bind to Manage + default: "" - name: mas_appws_components type: string description: Manage components to configure in the workspace diff --git a/tekton/src/pipelines/taskdefs/apps/manage-app.yml.j2 b/tekton/src/pipelines/taskdefs/apps/manage-app.yml.j2 index 03978daf35e..c6226f022ea 100644 --- a/tekton/src/pipelines/taskdefs/apps/manage-app.yml.j2 +++ b/tekton/src/pipelines/taskdefs/apps/manage-app.yml.j2 @@ -36,6 +36,8 @@ value: "$(params.mas_appws_components)" - name: mas_appws_bindings_jdbc value: "$(params.mas_appws_bindings_jdbc_manage)" + - name: mas_appws_bindings_kafka + value: "$(params.mas_appws_bindings_kafka_manage)" - name: mas_appws_bindings_health_wsl_flag value: $(params.mas_appws_bindings_health_wsl_flag) - name: mas_app_settings_aio_flag From 4cb79d1deaee08ede51395c0f7a5b581eb7e5998 Mon Sep 17 00:00:00 2001 From: David Parker Date: Fri, 15 May 2026 12:58:09 +0100 Subject: [PATCH 2/2] Add missing MAS_APPWS_BINDINGS_KAFKA env var --- .../2026-05-14-kafka-civil-image-processor.md | 30 +++++-------------- tekton/src/tasks/suite-app-install.yml.j2 | 6 ++++ 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/.bob/plans/2026-05-14-kafka-civil-image-processor.md b/.bob/plans/2026-05-14-kafka-civil-image-processor.md index a23b8bf0a0b..1c2d7e5832b 100644 --- a/.bob/plans/2026-05-14-kafka-civil-image-processor.md +++ b/.bob/plans/2026-05-14-kafka-civil-image-processor.md @@ -294,29 +294,13 @@ Add after the `mas_appws_bindings_jdbc` parameter: - [x] Update [`summarizer.py`](python/src/mas/cli/install/summarizer.py) to display Image Processor status and Kafka binding - [x] Validate all Python changes with `autopep8` and `flake8` -- [ ] **Phase 7: Automated Testing** - - [ ] Create `python/test/install/test_manage92_civil_kafka.py` with two test functions: - - [ ] `test_manage92_civil_no_kafka_interactive()` - Interactive test: Manage 9.2 with Civil, decline Kafka Image Processor - - Prompt handlers for Civil component selection - - Decline Kafka Image Processor when prompted - - Verify no Kafka configuration required - - Verify `mas_appws_bindings_kafka_manage` is empty - - [ ] `test_manage92_civil_with_kafka_interactive()` - Interactive test: Manage 9.2 with Civil, enable Kafka Image Processor - - Prompt handlers for Civil component selection - - Accept Kafka Image Processor when prompted - - Prompt handlers for Kafka configuration - - Verify `mas_appws_bindings_kafka_manage = "system"` - - Verify Kafka provider is configured - - [ ] `test_manage92_civil_with_kafka_non_interactive()` - Non-interactive test: Manage 9.2 with Civil and `--manage-kafka system` - - Use argv similar to [`test_install_master_dev_mode_non_interactive()`](python/test/install/test_dev_mode.py:372) - - Include `--manage-channel 9.2.x-dev` - - Include `--manage-components "base=latest,civil=latest"` - - Include `--manage-kafka system` - - Include `--kafka-provider strimzi` - - Verify installation completes successfully - - [ ] `test_manage92_civil_kafka_validation_errors()` - Non-interactive validation tests: - - Test `--manage-kafka` without `--kafka-provider` → expect error - - Test `--manage-kafka` with Manage < 9.2.0 → expect error +- [x] **Phase 7: Automated Testing** + - [x] Create `python/test/install/test_manage92_civil_kafka.py` with four test functions: + - [x] `test_manage92_civil_no_kafka_interactive()` - Interactive test: Manage 9.2 with Civil, decline Kafka Image Processor + - [x] `test_manage92_civil_with_kafka_interactive()` - Interactive test: Manage 9.2 with Civil, enable Kafka Image Processor + - [x] `test_manage92_civil_with_kafka_non_interactive()` - Non-interactive test: Manage 9.2 with Civil and `--manage-kafka system` + - [x] `test_manage92_civil_kafka_validation_errors()` - Non-interactive validation tests for error conditions + - [x] All tests validated with flake8 (no errors) ## Validation diff --git a/tekton/src/tasks/suite-app-install.yml.j2 b/tekton/src/tasks/suite-app-install.yml.j2 index e8985bcb1d5..9a822ba61ab 100644 --- a/tekton/src/tasks/suite-app-install.yml.j2 +++ b/tekton/src/tasks/suite-app-install.yml.j2 @@ -113,6 +113,10 @@ spec: type: string description: Components to configure in the workspace default: "" + - name: mas_appws_bindings_kafka + type: string + description: Components to configure in the workspace + default: "" # Application Workspace - Operand (Manage) - name: mas_appws_bindings_health_wsl_flag @@ -439,6 +443,8 @@ spec: # Application Workspace - Operand - name: MAS_APPWS_BINDINGS_JDBC value: $(params.mas_appws_bindings_jdbc) + - name: MAS_APPWS_BINDINGS_KAFKA + value: $(params.mas_appws_bindings_kafka) - name: MAS_APPWS_COMPONENTS value: $(params.mas_appws_components)