mas-cli 5.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mas-cli might be problematic. Click here for more details.

Files changed (114) hide show
  1. mas/cli/__init__.py +11 -0
  2. mas/cli/aiservice/install/__init__.py +11 -0
  3. mas/cli/aiservice/install/app.py +894 -0
  4. mas/cli/aiservice/install/argBuilder.py +180 -0
  5. mas/cli/aiservice/install/argParser.py +507 -0
  6. mas/cli/aiservice/install/params.py +100 -0
  7. mas/cli/aiservice/install/summarizer.py +134 -0
  8. mas/cli/cli.py +432 -0
  9. mas/cli/displayMixins.py +132 -0
  10. mas/cli/gencfg.py +113 -0
  11. mas/cli/install/__init__.py +11 -0
  12. mas/cli/install/app.py +1316 -0
  13. mas/cli/install/argBuilder.py +465 -0
  14. mas/cli/install/argParser.py +1176 -0
  15. mas/cli/install/catalogs.py +27 -0
  16. mas/cli/install/params.py +172 -0
  17. mas/cli/install/settings/__init__.py +23 -0
  18. mas/cli/install/settings/additionalConfigs.py +227 -0
  19. mas/cli/install/settings/db2Settings.py +252 -0
  20. mas/cli/install/settings/kafkaSettings.py +103 -0
  21. mas/cli/install/settings/manageSettings.py +273 -0
  22. mas/cli/install/settings/mongodbSettings.py +46 -0
  23. mas/cli/install/settings/turbonomicSettings.py +29 -0
  24. mas/cli/install/summarizer.py +398 -0
  25. mas/cli/templates/facilities-configs.yml.j2 +25 -0
  26. mas/cli/templates/ibm-mas-tekton.yaml +49772 -0
  27. mas/cli/templates/jdbccfg.yml.j2 +52 -0
  28. mas/cli/templates/pod-templates/best-effort/ibm-data-dictionary-assetdatadictionary.yml +26 -0
  29. mas/cli/templates/pod-templates/best-effort/ibm-mas-bascfg.yml +56 -0
  30. mas/cli/templates/pod-templates/best-effort/ibm-mas-coreidp.yml +21 -0
  31. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-actions.yml +28 -0
  32. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-auth.yml +32 -0
  33. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-datapower.yml +12 -0
  34. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-devops.yml +14 -0
  35. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-dm.yml +22 -0
  36. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-dsc.yml +40 -0
  37. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-edgeconfig.yml +10 -0
  38. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-fpl.yml +24 -0
  39. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-guardian.yml +20 -0
  40. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-iot.yml +10 -0
  41. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-mbgx.yml +18 -0
  42. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-mfgx.yml +14 -0
  43. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-monitor.yml +18 -0
  44. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-orgmgmt.yml +48 -0
  45. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-provision.yml +28 -0
  46. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-registry.yml +26 -0
  47. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-state.yml +40 -0
  48. mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-webui.yml +22 -0
  49. mas/cli/templates/pod-templates/best-effort/ibm-mas-manage-healthextaccelerator.yml +13 -0
  50. mas/cli/templates/pod-templates/best-effort/ibm-mas-manage-healthextworkspace.yml +10 -0
  51. mas/cli/templates/pod-templates/best-effort/ibm-mas-manage-imagestitching.yml +10 -0
  52. mas/cli/templates/pod-templates/best-effort/ibm-mas-manage-manageaccelerators.yml +10 -0
  53. mas/cli/templates/pod-templates/best-effort/ibm-mas-manage-manageapp.yml +46 -0
  54. mas/cli/templates/pod-templates/best-effort/ibm-mas-manage-manageworkspace.yml +48 -0
  55. mas/cli/templates/pod-templates/best-effort/ibm-mas-manage-slackproxy.yml +10 -0
  56. mas/cli/templates/pod-templates/best-effort/ibm-mas-pushnotificationcfg.yml +13 -0
  57. mas/cli/templates/pod-templates/best-effort/ibm-mas-scimcfg.yml +14 -0
  58. mas/cli/templates/pod-templates/best-effort/ibm-mas-slscfg.yml +10 -0
  59. mas/cli/templates/pod-templates/best-effort/ibm-mas-smtpcfg.yml +10 -0
  60. mas/cli/templates/pod-templates/best-effort/ibm-mas-suite.yml +136 -0
  61. mas/cli/templates/pod-templates/best-effort/ibm-mas-visualinspection.yml +34 -0
  62. mas/cli/templates/pod-templates/best-effort/ibm-sls-licenseservice.yml +10 -0
  63. mas/cli/templates/pod-templates/guaranteed/ibm-data-dictionary-assetdatadictionary.yml +56 -0
  64. mas/cli/templates/pod-templates/guaranteed/ibm-mas-bascfg.yml +140 -0
  65. mas/cli/templates/pod-templates/guaranteed/ibm-mas-coreidp.yml +45 -0
  66. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-actions.yml +70 -0
  67. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-auth.yml +80 -0
  68. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-datapower.yml +24 -0
  69. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-devops.yml +26 -0
  70. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-dm.yml +52 -0
  71. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-dsc.yml +106 -0
  72. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-edgeconfig.yml +16 -0
  73. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-fpl.yml +62 -0
  74. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-guardian.yml +44 -0
  75. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-iot.yml +16 -0
  76. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-mbgx.yml +42 -0
  77. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-mfgx.yml +32 -0
  78. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-monitor.yml +42 -0
  79. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-orgmgmt.yml +126 -0
  80. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-provision.yml +70 -0
  81. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-registry.yml +62 -0
  82. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-state.yml +106 -0
  83. mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-webui.yml +52 -0
  84. mas/cli/templates/pod-templates/guaranteed/ibm-mas-manage-healthextaccelerator.yml +28 -0
  85. mas/cli/templates/pod-templates/guaranteed/ibm-mas-manage-healthextworkspace.yml +18 -0
  86. mas/cli/templates/pod-templates/guaranteed/ibm-mas-manage-imagestitching.yml +16 -0
  87. mas/cli/templates/pod-templates/guaranteed/ibm-mas-manage-manageaccelerators.yml +16 -0
  88. mas/cli/templates/pod-templates/guaranteed/ibm-mas-manage-manageapp.yml +106 -0
  89. mas/cli/templates/pod-templates/guaranteed/ibm-mas-manage-manageworkspace.yml +126 -0
  90. mas/cli/templates/pod-templates/guaranteed/ibm-mas-manage-slackproxy.yml +16 -0
  91. mas/cli/templates/pod-templates/guaranteed/ibm-mas-pushnotificationcfg.yml +25 -0
  92. mas/cli/templates/pod-templates/guaranteed/ibm-mas-scimcfg.yml +26 -0
  93. mas/cli/templates/pod-templates/guaranteed/ibm-mas-slscfg.yml +16 -0
  94. mas/cli/templates/pod-templates/guaranteed/ibm-mas-smtpcfg.yml +16 -0
  95. mas/cli/templates/pod-templates/guaranteed/ibm-mas-suite.yml +340 -0
  96. mas/cli/templates/pod-templates/guaranteed/ibm-mas-visualinspection.yml +76 -0
  97. mas/cli/templates/pod-templates/guaranteed/ibm-sls-licenseservice.yml +16 -0
  98. mas/cli/templates/suite_mongocfg.yml.j2 +55 -0
  99. mas/cli/uninstall/__init__.py +11 -0
  100. mas/cli/uninstall/app.py +197 -0
  101. mas/cli/uninstall/argParser.py +115 -0
  102. mas/cli/update/__init__.py +11 -0
  103. mas/cli/update/app.py +673 -0
  104. mas/cli/update/argParser.py +156 -0
  105. mas/cli/upgrade/__init__.py +11 -0
  106. mas/cli/upgrade/app.py +164 -0
  107. mas/cli/upgrade/argParser.py +68 -0
  108. mas/cli/upgrade/settings/__init__.py +19 -0
  109. mas/cli/validators.py +151 -0
  110. mas_cli-5.1.4.data/scripts/mas-cli +87 -0
  111. mas_cli-5.1.4.dist-info/METADATA +73 -0
  112. mas_cli-5.1.4.dist-info/RECORD +114 -0
  113. mas_cli-5.1.4.dist-info/WHEEL +5 -0
  114. mas_cli-5.1.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,894 @@
1
+ #!/usr/bin/env python
2
+ # *****************************************************************************
3
+ # Copyright (c) 2024, 2025 IBM Corporation and other Contributors.
4
+ #
5
+ # All rights reserved. This program and the accompanying materials
6
+ # are made available under the terms of the Eclipse Public License v1.0
7
+ # which accompanies this distribution, and is available at
8
+ # http://www.eclipse.org/legal/epl-v10.html
9
+ #
10
+ # *****************************************************************************
11
+
12
+ import logging
13
+ from sys import exit
14
+ from os import path, getenv
15
+ import re
16
+ import calendar
17
+
18
+ from openshift.dynamic.exceptions import NotFoundError
19
+
20
+ from prompt_toolkit import prompt, print_formatted_text, HTML
21
+ from prompt_toolkit.completion import WordCompleter
22
+
23
+ from tabulate import tabulate
24
+
25
+ from halo import Halo
26
+
27
+ from ...cli import BaseApp
28
+ from .argBuilder import aiServiceInstallArgBuilderMixin
29
+ from .argParser import aiServiceinstallArgParser
30
+ from .summarizer import aiServiceInstallSummarizerMixin
31
+ from .params import requiredParams, optionalParams
32
+
33
+ from ...install.catalogs import supportedCatalogs
34
+
35
+ # AI Service relies on SLS, which in turn depends on MongoDB.
36
+ # SLS will utilize the shared MongoDB resource that would be used by MAS if it were deployed within the same OpenShift cluster.
37
+ # AI Service utilizes two distinct databases: DB2 is employed by the AiBroker component.
38
+ # By default, AiService will deploy DB2 within the same namespace as MAS (db2u), but it will be configured as a separate DB2 instance.
39
+
40
+ from ...install.settings.mongodbSettings import MongoDbSettingsMixin
41
+ from ...install.settings.db2Settings import Db2SettingsMixin
42
+ from ...install.settings.additionalConfigs import AdditionalConfigsMixin
43
+
44
+ from mas.cli.validators import (
45
+ InstanceIDFormatValidator,
46
+ StorageClassValidator
47
+ )
48
+
49
+ from mas.devops.ocp import createNamespace, getStorageClasses
50
+ from mas.devops.mas import (
51
+ getCurrentCatalog,
52
+ getDefaultStorageClasses
53
+ )
54
+ from mas.devops.sls import findSLSByNamespace
55
+ from mas.devops.data import getCatalog
56
+ from mas.devops.tekton import (
57
+ installOpenShiftPipelines,
58
+ updateTektonDefinitions,
59
+ prepareAiServicePipelinesNamespace,
60
+ prepareInstallSecrets,
61
+ testCLI,
62
+ launchAiServiceInstallPipeline
63
+ )
64
+
65
+ logger = logging.getLogger(__name__)
66
+
67
+
68
+ def logMethodCall(func):
69
+ def wrapper(self, *args, **kwargs):
70
+ logger.debug(f">>> InstallApp.{func.__name__}")
71
+ result = func(self, *args, **kwargs)
72
+ logger.debug(f"<<< InstallApp.{func.__name__}")
73
+ return result
74
+ return wrapper
75
+
76
+
77
+ class AiServiceInstallApp(BaseApp, aiServiceInstallArgBuilderMixin, aiServiceInstallSummarizerMixin, MongoDbSettingsMixin, Db2SettingsMixin, AdditionalConfigsMixin):
78
+ @logMethodCall
79
+ def processCatalogChoice(self) -> list:
80
+ self.catalogDigest = self.chosenCatalog["catalog_digest"]
81
+ self.catalogMongoDbVersion = self.chosenCatalog["mongo_extras_version_default"]
82
+ applications = {
83
+ "Aibroker": "aiservice_version",
84
+ }
85
+
86
+ self.catalogReleases = {}
87
+ self.catalogTable = []
88
+
89
+ # Dynamically fetch the channels from the chosen catalog
90
+ # based on mas core
91
+ for channel in self.chosenCatalog["mas_core_version"]:
92
+ # {"9.1-feature": "9.1.x-feature"}
93
+ self.catalogReleases.update({channel.replace('.x', ''): channel})
94
+
95
+ # Generate catalogTable
96
+ for application, key in applications.items():
97
+ # Add 9.1-feature channel based off 9.0 to those apps that have not onboarded yet
98
+ tempChosenCatalog = self.chosenCatalog[key].copy()
99
+ self.catalogTable.append({"": application} | {key.replace(".x", ""): value for key, value in sorted(tempChosenCatalog.items(), reverse=True)})
100
+
101
+ if self.architecture == "s390x":
102
+ summary = [
103
+ "",
104
+ "<u>Catalog Details</u>",
105
+ f"Catalog Image: icr.io/cpopen/ibm-maximo-operator-catalog:{self.getParam('mas_catalog_version')}",
106
+ f"Catalog Digest: {self.catalogDigest}",
107
+ f"MAS Releases: {', '.join(sorted(self.catalogReleases, reverse=True))}",
108
+ f"MongoDb: {self.catalogMongoDbVersion}",
109
+ ]
110
+ else:
111
+ summary = [
112
+ "",
113
+ "<u>Catalog Details</u>",
114
+ f"Catalog Image: icr.io/cpopen/ibm-maximo-operator-catalog:{self.getParam('mas_catalog_version')}",
115
+ f"Catalog Digest: {self.catalogDigest}",
116
+ f"MAS Releases: {', '.join(sorted(self.catalogReleases, reverse=True))}",
117
+ f"MongoDb: {self.catalogMongoDbVersion}",
118
+ ]
119
+
120
+ return summary
121
+
122
+ @logMethodCall
123
+ def configAibroker(self):
124
+ self.printH1("Configure Aibroker Instance")
125
+ self.printDescription([
126
+ "Instance ID restrictions:",
127
+ " - Must be 3-12 characters long",
128
+ " - Must only use lowercase letters, numbers, and hypen (-) symbol",
129
+ " - Must start with a lowercase letter",
130
+ " - Must end with a lowercase letter or a number"
131
+ ])
132
+ self.promptForString("Instance ID", "aiservice_instance_id", validator=InstanceIDFormatValidator())
133
+
134
+ if self.slsMode == 2 and not self.getParam("sls_namespace"):
135
+ self.setParam("sls_namespace", f"mas-{self.getParam('aiservice_instance_id')}-sls")
136
+
137
+ self.configOperationMode()
138
+
139
+ @logMethodCall
140
+ def interactiveMode(self, simplified: bool, advanced: bool) -> None:
141
+ # Interactive mode
142
+ self.interactiveMode = True
143
+
144
+ self.storageClassProvider = "custom"
145
+ self.slsLicenseFileLocal = None
146
+
147
+ # Catalog
148
+ self.configCatalog()
149
+ if not self.devMode:
150
+ self.validateCatalogSource()
151
+ self.licensePrompt()
152
+
153
+ # Storage Classes
154
+ self.configStorageClasses()
155
+
156
+ # Licensing (SLS and DRO)
157
+ self.configSLS()
158
+ self.configDRO()
159
+ self.configICRCredentials()
160
+
161
+ self.configCertManager()
162
+ self.configAibroker()
163
+ if self.devMode:
164
+ self.configAppChannel("aibroker")
165
+
166
+ self.aiServiceSettings()
167
+ self.aiServiceTenantSettings()
168
+ self.aiServiceIntegrations()
169
+
170
+ # Dependencies
171
+ self.configMongoDb()
172
+ self.setDB2DefaultSettings()
173
+
174
+ @logMethodCall
175
+ def nonInteractiveMode(self) -> None:
176
+ self.interactiveMode = False
177
+
178
+ # Set defaults
179
+ # ---------------------------------------------------------------------
180
+ # Unless a config file named "mongodb-system.yaml" is provided via the additional configs mechanism we will be installing a new MongoDb instance
181
+ self.setParam("mongodb_action", "install")
182
+
183
+ self.storageClassProvider = "custom"
184
+ self.slsLicenseFileLocal = None
185
+
186
+ self.approvals = {
187
+ "approval_aiservice": {"id": "aiservice"},
188
+ }
189
+
190
+ self.setDB2DefaultSettings()
191
+
192
+ for key, value in vars(self.args).items():
193
+ # These fields we just pass straight through to the parameters and fail if they are not set
194
+ if key in requiredParams:
195
+ if value is None:
196
+ self.fatalError(f"{key} must be set")
197
+ self.setParam(key, value)
198
+
199
+ # These fields we just pass straight through to the parameters
200
+ elif key in optionalParams:
201
+ if value is not None:
202
+ self.setParam(key, value)
203
+
204
+ elif key == "install_minio_aiservice":
205
+ incompatibleWithMinioInstall = [
206
+ "aiservice_storage_provider",
207
+ "aiservice_storage_accesskey",
208
+ "aiservice_storage_secretkey",
209
+ "aiservice_storage_host",
210
+ "aiservice_storage_port",
211
+ "aiservice_storage_ssl",
212
+ "aiservice_s3_endpoint_url",
213
+ "aiservice_storage_region",
214
+ "aiservice_tenant_s3_access_key",
215
+ "aiservice_tenant_s3_secret_key",
216
+ "aiservice_tenant_s3_endpoint_url",
217
+ "aiservice_tenant_s3_region"
218
+ ]
219
+ if value is None:
220
+ for uKey in incompatibleWithMinioInstall:
221
+ if vars(self.args)[uKey] is None:
222
+ self.fatalError(f"Parameter is required when --install-minio is not set: {uKey}")
223
+ elif value is not None and value == "true":
224
+ # If user is installing Minio in-cluster then we know how to connect to it already
225
+ for uKey in incompatibleWithMinioInstall:
226
+ if vars(self.args)[uKey] is not None:
227
+ self.fatalError(f"Unsupported parameter for --install-minio: {uKey}")
228
+ for rKey in ["minio_root_user", "minio_root_password"]:
229
+ if vars(self.args)[rKey] is None:
230
+ self.fatalError(f"Missing required parameter for --install-minio: {rKey}")
231
+
232
+ self.setParam("aiservice_storage_provider", "minio")
233
+
234
+ self.setParam("aiservice_storage_accesskey", self.args.minio_root_user)
235
+ self.setParam("aiservice_storage_secretkey", self.args.minio_root_password)
236
+
237
+ # TODO: Duplication -- we already have the URL, why do we need all the individual parts,
238
+ # especially when we don't need them for the tenant?
239
+ self.setParam("aiservice_storage_host", "minio-service.minio.svc.cluster.local")
240
+ self.setParam("aiservice_storage_port", "9000")
241
+ self.setParam("aiservice_storage_ssl", "false")
242
+ self.setParam("aiservice_s3_endpoint_url", "http://minio-service.minio.svc.cluster.local:9000")
243
+ self.setParam("aiservice_storage_region", "none")
244
+
245
+ self.setParam("aiservice_tenant_s3_access_key", self.args.minio_root_user)
246
+ self.setParam("aiservice_tenant_s3_secret_key", self.args.minio_root_password)
247
+ self.setParam("aiservice_tenant_s3_endpoint_url", "http://minio-service.minio.svc.cluster.local:9000")
248
+ self.setParam("aiservice_tenant_s3_region", "none")
249
+ else:
250
+ self.fatalError(f"Unsupported value for --install-minio: {value}")
251
+
252
+ elif key == "non_prod":
253
+ if not value:
254
+ self.operationalMode = 1
255
+ self.setParam("environment_type", "production")
256
+ else:
257
+ self.operationalMode = 2
258
+ self.setParam("mas_annotations", "mas.ibm.com/operationalMode=nonproduction")
259
+ self.setParam("environment_type", "non-production")
260
+
261
+ elif key == "additional_configs":
262
+ self.localConfigDir = value
263
+ # If there is a file named mongodb-system.yaml we will use this as a BYO MongoDB datasource
264
+ if self.localConfigDir is not None and path.exists(path.join(self.localConfigDir, "mongodb-system.yaml")):
265
+ self.setParam("mongodb_action", "byo")
266
+ self.setParam("sls_mongodb_cfg_file", "/workspace/additional-configs/mongodb-system.yaml")
267
+
268
+ elif key == "pod_templates":
269
+ # For the named configurations we will convert into the path
270
+ if value in ["best-effort", "guaranteed"]:
271
+ self.setParam("mas_pod_templates_dir", path.join(self.templatesDir, "pod-templates", value))
272
+ else:
273
+ self.setParam("mas_pod_templates_dir", value)
274
+
275
+ # We check for both None and "" values for the application channel parameters
276
+ # value = None means the parameter wasn't set at all
277
+ # value = "" means the parameter was explicitly set to "don't install this application"
278
+ elif key == "aiservice_channel":
279
+ if value is not None and value != "":
280
+ self.setParam("aiservice_channel", value)
281
+
282
+ # MongoDB
283
+ elif key == "mongodb_namespace":
284
+ if value is not None and value != "":
285
+ self.setParam(key, value)
286
+ self.setParam("sls_mongodb_cfg_file", f"/workspace/configs/mongo-{value}.yml")
287
+
288
+ # SLS
289
+ elif key == "license_file":
290
+ if value is not None and value != "":
291
+ self.slsLicenseFileLocal = value
292
+ self.setParam("sls_action", "install")
293
+ elif key == "dedicated_sls":
294
+ if value:
295
+ self.setParam("sls_namespace", f"mas-{self.args.aiservice_instance_id}-sls")
296
+
297
+ # These settings are used by the CLI rather than passed to the PipelineRun
298
+ elif key == "storage_accessmode":
299
+ if value is None:
300
+ self.fatalError(f"{key} must be set")
301
+ self.pipelineStorageAccessMode = value
302
+ elif key == "storage_pipeline":
303
+ if value is None:
304
+ self.fatalError(f"{key} must be set")
305
+ self.pipelineStorageClass = value
306
+
307
+ elif key.startswith("approval_"):
308
+ if key not in self.approvals:
309
+ raise KeyError(f"{key} is not a supported approval workflow ID: {self.approvals.keys()}")
310
+
311
+ if value != "":
312
+ valueParts = value.split(":")
313
+ if len(valueParts) != 3:
314
+ self.fatalError(f"Unsupported format for {key} ({value}). Expected MAX_RETRIES:RETRY_DELAY:IGNORE_FAILURE")
315
+ else:
316
+ try:
317
+ self.approvals[key]["maxRetries"] = int(valueParts[0])
318
+ self.approvals[key]["retryDelay"] = int(valueParts[1])
319
+ self.approvals[key]["ignoreFailure"] = bool(valueParts[2])
320
+ except ValueError:
321
+ self.fatalError(f"Unsupported format for {key} ({value}). Expected int:int:boolean")
322
+
323
+ # Arguments that we don't need to do anything with
324
+ elif key in ["accept_license", "dev_mode", "skip_pre_check", "skip_grafana_install", "no_confirm", "no_wait_for_pvc", "help", "advanced", "simplified"]:
325
+ pass
326
+
327
+ elif key == "manual_certificates":
328
+ if value is not None:
329
+ self.setParam("mas_manual_cert_mgmt", True)
330
+ self.manualCertsDir = value
331
+ else:
332
+ self.setParam("mas_manual_cert_mgmt", False)
333
+ self.manualCertsDir = None
334
+
335
+ elif key == "enable_ipv6":
336
+ self.setParam("enable_ipv6", True)
337
+
338
+ # Fail if there's any arguments we don't know how to handle
339
+ else:
340
+ print(f"Unknown option: {key} {value}")
341
+ self.fatalError(f"Unknown option: {key} {value}")
342
+
343
+ # Load the catalog information
344
+ self.chosenCatalog = getCatalog(self.getParam("mas_catalog_version"))
345
+
346
+ # License file is only optional for existing SLS instance
347
+ if self.slsLicenseFileLocal is None:
348
+ self.fatalError("--license-file must be set for new SLS install")
349
+
350
+ # Once we've processed the inputs, we should validate the catalog source & prompt to accept the license terms
351
+ if not self.devMode:
352
+ self.validateCatalogSource()
353
+ self.licensePrompt()
354
+
355
+ @logMethodCall
356
+ def install(self, argv):
357
+ """
358
+ Install Aiservice
359
+ """
360
+ args = aiServiceinstallArgParser.parse_args(args=argv)
361
+
362
+ # We use the presence of --mas-instance-id to determine whether
363
+ # the CLI is being started in interactive mode or not
364
+ instanceId = args.aiservice_instance_id
365
+
366
+ # Properties for arguments that control the behavior of the CLI
367
+ self.noConfirm = args.no_confirm
368
+ self.waitForPVC = not args.no_wait_for_pvc
369
+ self.licenseAccepted = args.accept_license
370
+ self.devMode = args.dev_mode
371
+
372
+ self.printDescription([
373
+ "<B><U>AI Broker 9.0 Deprecation Notice</U></B>",
374
+ "",
375
+ "Maximo AI Broker (introduced with MAS 9.0) has been replaced with Maximo AI Service as of Aug 1 2025",
376
+ "To continue using the features that were enabled by the AI broker after that time, you must deploy and use Maximo AI Service 9.1:",
377
+ " - Maximo AI Service 9.1 is compatible with both Maximo Application Suite 9.0 and 9.1 releases",
378
+ " - If Maximo AI Service is deployed with Maximo Application Suite 9.0, you can use only the AI features that were included in Maximo Application Suite 9.0",
379
+ "",
380
+ "Note: Maximo AI Service 9.1 includes a limited-use license to watsonx.ai and incurs an additional AppPoint cost"
381
+ ])
382
+
383
+ if not self.devMode:
384
+ self.printDescription([
385
+ "",
386
+ "<ForestGreen>Coming Soon! We are busy putting the finishing touches on Maximo AI Service 9.1 ahead of a re-launch planned for the August 2025 catalog update</ForestGreen>",
387
+ ""
388
+ ])
389
+ exit(1)
390
+
391
+ # Set image_pull_policy of the CLI in interactive mode
392
+ if args.image_pull_policy and args.image_pull_policy != "":
393
+ self.setParam("image_pull_policy", args.image_pull_policy)
394
+
395
+ self.approvals = {}
396
+
397
+ # Store all args
398
+ self.args = args
399
+
400
+ # These flags work for setting params in both interactive and non-interactive modes
401
+ if args.skip_pre_check:
402
+ self.setParam("skip_pre_check", "true")
403
+
404
+ if instanceId is None:
405
+ self.printH1("Set Target OpenShift Cluster")
406
+ # Connect to the target cluster
407
+ self.connect()
408
+ else:
409
+ logger.debug("Aiservice instance ID is set, so we assume already connected to the desired OCP")
410
+ self.lookupTargetArchitecture()
411
+
412
+ if self.dynamicClient is None:
413
+ print_formatted_text(HTML("<Red>Error: The Kubernetes dynamic Client is not available. See log file for details</Red>"))
414
+ exit(1)
415
+
416
+ # 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
417
+ # deprecated MaximoApplicationSuite ImageContentSourcePolicy instead of the new ImageDigestMirrorSet
418
+ self.isAirgap()
419
+
420
+ # Configure the installOptions for the appropriate architecture
421
+ self.catalogOptions = supportedCatalogs[self.architecture]
422
+
423
+ # Basic settings before the user provides any input
424
+ self.configICR()
425
+ self.deployCP4D = False
426
+
427
+ # UDS install has not been supported since the January 2024 catalog update
428
+ self.setParam("uds_action", "install-dro")
429
+
430
+ # Install Db2 for AI Service
431
+ self.setParam("db2_action_aiservice", "install")
432
+
433
+ # User must either provide the configuration via numerous command line arguments, or the interactive prompts
434
+ if instanceId is None:
435
+ self.interactiveMode(simplified=args.simplified, advanced=args.advanced)
436
+ else:
437
+ self.nonInteractiveMode()
438
+
439
+ # Set up the sls license file
440
+ self.slsLicenseFile()
441
+
442
+ # Show a summary of the installation configuration
443
+ self.printH1("Non-Interactive Install Command")
444
+ self.printDescription([
445
+ "Save and re-use the following script to re-run this install without needing to answer the interactive prompts again",
446
+ "",
447
+ self.buildCommand()
448
+ ])
449
+
450
+ self.displayInstallSummary()
451
+
452
+ if not self.noConfirm:
453
+ print()
454
+ self.printDescription([
455
+ "Please carefully review your choices above, correcting mistakes now is much easier than after the install has begun"
456
+ ])
457
+ continueWithInstall = self.yesOrNo("Proceed with these settings")
458
+
459
+ # Prepare the namespace and launch the installation pipeline
460
+ if self.noConfirm or continueWithInstall:
461
+ self.createTektonFileWithDigest()
462
+
463
+ self.printH1("Launch Install")
464
+ pipelinesNamespace = f"aiservice-{self.getParam('aiservice_instance_id')}-pipelines"
465
+
466
+ if not self.noConfirm:
467
+ self.printDescription(["If you are using storage classes that utilize 'WaitForFirstConsumer' binding mode choose 'No' at the prompt below"])
468
+ wait = self.yesOrNo("Wait for PVCs to bind")
469
+ else:
470
+ wait = False
471
+
472
+ with Halo(text='Validating OpenShift Pipelines installation', spinner=self.spinner) as h:
473
+ installOpenShiftPipelines(self.dynamicClient)
474
+ h.stop_and_persist(symbol=self.successIcon, text="OpenShift Pipelines Operator is installed and ready to use")
475
+
476
+ with Halo(text=f'Preparing namespace ({pipelinesNamespace})', spinner=self.spinner) as h:
477
+ createNamespace(self.dynamicClient, pipelinesNamespace)
478
+ prepareAiServicePipelinesNamespace(
479
+ dynClient=self.dynamicClient,
480
+ instanceId=self.getParam("aiservice_instance_id"),
481
+ storageClass=self.pipelineStorageClass,
482
+ accessMode=self.pipelineStorageAccessMode,
483
+ waitForBind=wait,
484
+ configureRBAC=(self.getParam("service_account_name") == "")
485
+ )
486
+ prepareInstallSecrets(
487
+ dynClient=self.dynamicClient,
488
+ namespace=pipelinesNamespace,
489
+ slsLicenseFile=self.slsLicenseFileSecret,
490
+ additionalConfigs=self.additionalConfigsSecret,
491
+ podTemplates=self.podTemplatesSecret,
492
+ certs=self.certsSecret
493
+ )
494
+
495
+ self.setupApprovals(pipelinesNamespace)
496
+
497
+ h.stop_and_persist(symbol=self.successIcon, text=f"Namespace is ready ({pipelinesNamespace})")
498
+
499
+ with Halo(text='Testing availability of MAS CLI image in cluster', spinner=self.spinner) as h:
500
+ testCLI()
501
+ h.stop_and_persist(symbol=self.successIcon, text="MAS CLI image deployment test completed")
502
+
503
+ with Halo(text=f'Installing latest Tekton definitions (v{self.version})', spinner=self.spinner) as h:
504
+ updateTektonDefinitions(pipelinesNamespace, self.tektonDefsPath)
505
+ h.stop_and_persist(symbol=self.successIcon, text=f"Latest Tekton definitions are installed (v{self.version})")
506
+
507
+ with Halo(text=f"Submitting PipelineRun for {self.getParam('aiservice_instance_id')} install", spinner=self.spinner) as h:
508
+ pipelineURL = launchAiServiceInstallPipeline(dynClient=self.dynamicClient, params=self.params)
509
+ if pipelineURL is not None:
510
+ h.stop_and_persist(symbol=self.successIcon, text=f"PipelineRun for {self.getParam('aiservice_instance_id')} install submitted")
511
+ print_formatted_text(HTML(f"\nView progress:\n <Cyan><u>{pipelineURL}</u></Cyan>\n"))
512
+ else:
513
+ h.stop_and_persist(symbol=self.failureIcon, text=f"Failed to submit PipelineRun for {self.getParam('aiservice_instance_id')} install, see log file for details")
514
+ print()
515
+
516
+ @logMethodCall
517
+ def setupApprovals(self, namespace: str) -> None:
518
+ """
519
+ Ensure the supported approval configmaps are in the expected state for the start of the run:
520
+ - not present (if approval is not required)
521
+ - present with the chosen state field initialized to ""
522
+ """
523
+ for approval in self.approvals.values():
524
+ if "maxRetries" in approval:
525
+ # Enable this approval workload
526
+ logger.debug(f"Approval workflow for {approval['id']} will be enabled during install ({approval['maxRetries']} / {approval['retryDelay']}s / {approval['ignoreFailure']})")
527
+ self.initializeApprovalConfigMap(namespace, approval['id'], True, approval['maxRetries'], approval['retryDelay'], approval['ignoreFailure'])
528
+
529
+ def aiServiceSettings(self) -> None:
530
+ self.printH1("AI Service Settings")
531
+
532
+ # Ask about MinIO installation FIRST (moved from aiServiceDependencies)
533
+ self.printH2("Storage Configuration")
534
+ self.printDescription(["AI Service requires object storage for pipelines, tenants, and templates. You can either install MinIO in-cluster or connect to external storage."])
535
+
536
+ if self.yesOrNo("Install Minio"):
537
+ # Only ask for MinIO credentials
538
+ self.promptForString("minio root username", "minio_root_user")
539
+ self.promptForString("minio root password", "minio_root_password", isPassword=True)
540
+
541
+ # Auto-set MinIO storage defaults (same as non-interactive mode)
542
+ self._setMinioStorageDefaults()
543
+ else:
544
+ # Ask for external storage configuration
545
+ self.printDescription(["Configure your external object storage (S3-compatible) connection details:"])
546
+ self.promptForString("Storage provider", "aiservice_storage_provider")
547
+ self.promptForString("Storage access key", "aiservice_storage_accesskey")
548
+ self.promptForString("Storage secret key", "aiservice_storage_secretkey", isPassword=True)
549
+ self.promptForString("Storage host", "aiservice_storage_host")
550
+ self.promptForString("Storage port", "aiservice_storage_port")
551
+ self.promptForString("Storage ssl", "aiservice_storage_ssl")
552
+ self.promptForString("Storage region", "aiservice_storage_region")
553
+ self.promptForString("Storage pipelines bucket", "aiservice_storage_pipelines_bucket")
554
+ self.promptForString("Storage tenants bucket", "aiservice_storage_tenants_bucket")
555
+ self.promptForString("Storage templates bucket", "aiservice_storage_templates_bucket")
556
+
557
+ # S3 parameters are now auto-derived from storage configuration
558
+ self._deriveS3ParametersFromStorage()
559
+
560
+ def aiServiceTenantSettings(self) -> None:
561
+ self.printH1("AI Service Tenant Settings")
562
+ self.promptForString("Tenant entitlement type", "tenant_entitlement_type")
563
+ self.promptForString("Tenant start date", "tenant_entitlement_start_date")
564
+ self.promptForString("Tenant end date", "tenant_entitlement_end_date")
565
+
566
+ def _deriveS3ParametersFromStorage(self) -> None:
567
+ """
568
+ Auto-derive S3 and tenant S3 parameters from the aiservice_storage_* parameters.
569
+ This reuses the values provided for kmodel object storage to avoid redundant prompts.
570
+ """
571
+ storage_provider = self.getParam("aiservice_storage_provider")
572
+ storage_host = self.getParam("aiservice_storage_host")
573
+ storage_port = self.getParam("aiservice_storage_port")
574
+ storage_ssl = self.getParam("aiservice_storage_ssl")
575
+ storage_region = self.getParam("aiservice_storage_region")
576
+ storage_accesskey = self.getParam("aiservice_storage_accesskey")
577
+ storage_secretkey = self.getParam("aiservice_storage_secretkey")
578
+
579
+ # Build endpoint URL from storage configuration
580
+ protocol = "https" if storage_ssl == "true" else "http"
581
+
582
+ if storage_provider == "minio":
583
+ endpoint_url = f"{protocol}://{storage_host}:{storage_port}"
584
+ elif storage_provider == "s3":
585
+ # For AWS S3, construct proper endpoint
586
+ if storage_region and storage_region != "none":
587
+ endpoint_url = f"{protocol}://s3.{storage_region}.amazonaws.com"
588
+ else:
589
+ endpoint_url = f"{protocol}://s3.amazonaws.com"
590
+ else:
591
+ # For other providers, construct basic endpoint
592
+ endpoint_url = f"{protocol}://{storage_host}:{storage_port}" if storage_port else f"{protocol}://{storage_host}"
593
+
594
+ # Set S3 parameters (reusing storage configuration)
595
+ self.setParam("aiservice_s3_bucket_prefix", "aiservice") # Default prefix
596
+ if endpoint_url:
597
+ self.setParam("aiservice_s3_endpoint_url", endpoint_url)
598
+ self.setParam("aiservice_s3_region", storage_region if storage_region else "none")
599
+
600
+ # Set tenant S3 parameters (reusing same storage configuration)
601
+ self.setParam("aiservice_tenant_s3_bucket_prefix", "tenant") # Default tenant prefix
602
+ self.setParam("aiservice_tenant_s3_access_key", storage_accesskey)
603
+ self.setParam("aiservice_tenant_s3_secret_key", storage_secretkey)
604
+ if endpoint_url:
605
+ self.setParam("aiservice_tenant_s3_endpoint_url", endpoint_url)
606
+ self.setParam("aiservice_tenant_s3_region", storage_region if storage_region else "none")
607
+
608
+ def _setMinioStorageDefaults(self) -> None:
609
+ """
610
+ Set MinIO storage defaults when MinIO is being installed in-cluster.
611
+ This mirrors the logic from non-interactive mode.
612
+ """
613
+ self.setParam("aiservice_storage_provider", "minio")
614
+ self.setParam("aiservice_storage_accesskey", self.getParam("minio_root_user"))
615
+ self.setParam("aiservice_storage_secretkey", self.getParam("minio_root_password"))
616
+ self.setParam("aiservice_storage_host", "minio-service.minio.svc.cluster.local")
617
+ self.setParam("aiservice_storage_port", "9000")
618
+ self.setParam("aiservice_storage_ssl", "false")
619
+ self.setParam("aiservice_storage_region", "none")
620
+
621
+ # Set default bucket names
622
+ self.setParam("aiservice_storage_pipelines_bucket", "km-pipelines")
623
+ self.setParam("aiservice_storage_tenants_bucket", "km-tenants")
624
+ self.setParam("aiservice_storage_templates_bucket", "km-templates")
625
+
626
+ def aiServiceIntegrations(self) -> None:
627
+ self.printH1("WatsonX Integration")
628
+ self.printDescription([
629
+ "This CLI section configures the integration between the AI Service and IBM watsonx.ai. AI Service",
630
+ "uses watsonx for model deployment and inferencing.",
631
+ "",
632
+ "The WatsonX API key must be a **platform API key** associated with a user that has at least:",
633
+ "- **Editor permission** for the project",
634
+ "- **Viewer permission** for the space",
635
+ "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",
636
+ "",
637
+ "The endpoint URL is your WatsonX Machine Learning service URL. It can be found in the watsonx.ai",
638
+ "documentation: https://cloud.ibm.com/apidocs/watsonx-ai-cp/watsonx-ai-cp-2.2.0#endpoint-url",
639
+ "",
640
+ "The project ID refers to your specific watsonx.ai project where your ML models and assets are stored.",
641
+ "",
642
+ ])
643
+ self.promptForString("Watsonxai api key", "aiservice_watsonxai_apikey", isPassword=True)
644
+ self.promptForString("Watsonxai machine learning url", "aiservice_watsonxai_url")
645
+ self.promptForString("Watsonxai project id", "aiservice_watsonxai_project_id")
646
+
647
+ self.printH1("RSL Integration")
648
+ self.printDescription([
649
+ "RSL (Reliable Strategy Library) connects to strategic asset management via STRATEGIZEAPI.",
650
+ "",
651
+ "RSL URL: https://api.rsl-service.suite.maximo.com (standard for all customers)",
652
+ "Org ID: Get from MAS Manage > System Properties > 'mxe.rs.rslorgid'",
653
+ "Token: Use your IBM entitlement key (same as MAS installation)",
654
+ "",
655
+ "Note: Future versions will auto-configure these from MAS Manage.",
656
+ ""
657
+ ])
658
+ self.promptForString("RSL url", "rsl_url")
659
+ self.promptForString("ORG Id of RSL", "rsl_org_id")
660
+ self.promptForString("Token for RSL", "rsl_token", isPassword=True)
661
+
662
+ # These are all candidates to centralise in a new mixin used by both install and aiservice-install
663
+
664
+ @logMethodCall
665
+ def configICR(self):
666
+ if self.devMode:
667
+ self.setParam("mas_icr_cp", getenv("MAS_ICR_CP", "docker-na-public.artifactory.swg-devops.com/wiotp-docker-local"))
668
+ self.setParam("mas_icr_cpopen", getenv("MAS_ICR_CPOPEN", "docker-na-public.artifactory.swg-devops.com/wiotp-docker-local/cpopen"))
669
+ self.setParam("sls_icr_cpopen", getenv("SLS_ICR_CPOPEN", "docker-na-public.artifactory.swg-devops.com/wiotp-docker-local/cpopen"))
670
+ else:
671
+ self.setParam("mas_icr_cp", getenv("MAS_ICR_CP", "cp.icr.io/cp"))
672
+ self.setParam("mas_icr_cpopen", getenv("MAS_ICR_CPOPEN", "icr.io/cpopen"))
673
+ self.setParam("sls_icr_cpopen", getenv("SLS_ICR_CPOPEN", "icr.io/cpopen"))
674
+
675
+ @logMethodCall
676
+ def configICRCredentials(self):
677
+ self.printH1("Configure IBM Container Registry")
678
+ self.promptForString("IBM entitlement key", "ibm_entitlement_key", isPassword=True)
679
+ if self.devMode:
680
+ self.promptForString("Artifactory username", "artifactory_username")
681
+ self.promptForString("Artifactory token", "artifactory_token", isPassword=True)
682
+
683
+ @logMethodCall
684
+ def configCertManager(self):
685
+ # Only install of Red Hat Cert-Manager has been supported since the January 2025 catalog update
686
+ self.setParam("cert_manager_provider", "redhat")
687
+ self.setParam("cert_manager_action", "install")
688
+
689
+ def formatCatalog(self, name: str) -> str:
690
+ # Convert "v9-241107-amd64" into "November 2024 Update (v9-241107-amd64)"
691
+ date = name.split("-")[1]
692
+ month = int(date[2:4])
693
+ monthName = calendar.month_name[month]
694
+ year = date[:2]
695
+ return f" - {monthName} 20{year} Update\n <Orange><u>https://ibm-mas.github.io/cli/catalogs/{name}</u></Orange>"
696
+
697
+ @logMethodCall
698
+ def configCatalog(self):
699
+ self.printH1("IBM Maximo Operator Catalog Selection")
700
+ if self.devMode:
701
+ self.promptForString("Select catalog source", "mas_catalog_version", default="v9-master-amd64")
702
+ else:
703
+ catalogInfo = getCurrentCatalog(self.dynamicClient)
704
+
705
+ if catalogInfo is None:
706
+ self.printDescription([
707
+ "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.",
708
+ "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.",
709
+ " - Learn more: <Orange><u>https://ibm-mas.github.io/cli/catalogs/</u></Orange>",
710
+ ""
711
+ ])
712
+ print("Supported Catalogs:")
713
+ for catalog in self.catalogOptions:
714
+ catalogString = self.formatCatalog(catalog)
715
+ print_formatted_text(HTML(f"{catalogString}"))
716
+ print()
717
+
718
+ catalogCompleter = WordCompleter(self.catalogOptions)
719
+ catalogSelection = self.promptForString("Select catalog", completer=catalogCompleter)
720
+ self.setParam("mas_catalog_version", catalogSelection)
721
+ else:
722
+ self.printDescription([
723
+ 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."
724
+ ])
725
+ self.setParam("mas_catalog_version", catalogInfo["catalogId"])
726
+
727
+ self.chosenCatalog = getCatalog(self.getParam("mas_catalog_version"))
728
+ catalogSummary = self.processCatalogChoice()
729
+ self.printDescription(catalogSummary)
730
+ self.printDescription([
731
+ "",
732
+ "Two types of release are available:",
733
+ " - GA releases of Maximo Application Suite are supported under IBM's standard 3+1+3 support lifecycle policy.",
734
+ " - 'Feature' releases allow early access to new features for evaluation in non-production environments and are only supported through to the next GA release.",
735
+ ""
736
+ ])
737
+
738
+ print(tabulate(self.catalogTable, headers="keys", tablefmt="simple_grid"))
739
+
740
+ releaseCompleter = WordCompleter(sorted(self.catalogReleases, reverse=True))
741
+ releaseSelection = self.promptForString("Select release", completer=releaseCompleter)
742
+
743
+ self.setParam("aiservice_channel", self.catalogReleases[releaseSelection])
744
+
745
+ @logMethodCall
746
+ def validateCatalogSource(self):
747
+ catalogsAPI = self.dynamicClient.resources.get(api_version="operators.coreos.com/v1alpha1", kind="CatalogSource")
748
+ try:
749
+ catalog = catalogsAPI.get(name="ibm-operator-catalog", namespace="openshift-marketplace")
750
+ catalogDisplayName = catalog.spec.displayName
751
+
752
+ m = re.match(r".+(?P<catalogId>v[89]-(?P<catalogVersion>[0-9]+)-amd64)", catalogDisplayName)
753
+ if m:
754
+ # catalogId = v8-yymmdd-amd64
755
+ # catalogVersion = yymmdd
756
+ catalogId = m.group("catalogId")
757
+ elif re.match(r".+v8-amd64", catalogDisplayName):
758
+ catalogId = "v8-amd64"
759
+ else:
760
+ 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")
761
+
762
+ if catalogId != self.getParam("mas_catalog_version"):
763
+ 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")
764
+ except NotFoundError:
765
+ # There's no existing catalog installed
766
+ pass
767
+
768
+ # TODO: update licenses for aiservice 9.1.x
769
+ @logMethodCall
770
+ def licensePrompt(self):
771
+ if not self.licenseAccepted:
772
+ self.printH1("License Terms")
773
+ self.printDescription([
774
+ "To continue with the installation, you must accept the license terms:",
775
+ self.licenses[f"aibroker-{self.getParam('aiservice_channel')}"]
776
+ ])
777
+
778
+ if self.noConfirm:
779
+ self.fatalError("You must accept the license terms with --accept-license when using the --no-confirm flag")
780
+ else:
781
+ if not self.yesOrNo("Do you accept the license terms"):
782
+ exit(1)
783
+
784
+ @logMethodCall
785
+ def configStorageClasses(self):
786
+ self.printH1("Configure Storage Class Usage")
787
+ self.printDescription([
788
+ "Maximo Application Suite and it's dependencies require storage classes that support ReadWriteOnce (RWO) and ReadWriteMany (RWX) access modes:",
789
+ " - ReadWriteOnce volumes can be mounted as read-write by multiple pods on a single node.",
790
+ " - ReadWriteMany volumes can be mounted as read-write by multiple pods across many nodes.",
791
+ ""
792
+ ])
793
+ defaultStorageClasses = getDefaultStorageClasses(self.dynamicClient)
794
+ if defaultStorageClasses.provider is not None:
795
+ print_formatted_text(HTML(f"<MediumSeaGreen>Storage provider auto-detected: {defaultStorageClasses.providerName}</MediumSeaGreen>"))
796
+ print_formatted_text(HTML(f"<LightSlateGrey> - Storage class (ReadWriteOnce): {defaultStorageClasses.rwo}</LightSlateGrey>"))
797
+ print_formatted_text(HTML(f"<LightSlateGrey> - Storage class (ReadWriteMany): {defaultStorageClasses.rwx}</LightSlateGrey>"))
798
+ self.storageClassProvider = defaultStorageClasses.provider
799
+ self.params["storage_class_rwo"] = defaultStorageClasses.rwo
800
+ self.params["storage_class_rwx"] = defaultStorageClasses.rwx
801
+
802
+ overrideStorageClasses = False
803
+ if "storage_class_rwx" in self.params and self.params["storage_class_rwx"] != "":
804
+ overrideStorageClasses = not self.yesOrNo("Use the auto-detected storage classes")
805
+
806
+ if "storage_class_rwx" not in self.params or self.params["storage_class_rwx"] == "" or overrideStorageClasses:
807
+ self.storageClassProvider = "custom"
808
+
809
+ self.printDescription([
810
+ "Select the ReadWriteOnce and ReadWriteMany storage classes to use from the list below:",
811
+ "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"
812
+ ])
813
+ for storageClass in getStorageClasses(self.dynamicClient):
814
+ print_formatted_text(HTML(f"<LightSlateGrey> - {storageClass.metadata.name}</LightSlateGrey>"))
815
+
816
+ self.params["storage_class_rwo"] = prompt(HTML('<Yellow>ReadWriteOnce (RWO) storage class</Yellow> '), validator=StorageClassValidator(), validate_while_typing=False)
817
+ self.params["storage_class_rwx"] = prompt(HTML('<Yellow>ReadWriteMany (RWX) storage class</Yellow> '), validator=StorageClassValidator(), validate_while_typing=False)
818
+
819
+ # Configure storage class for pipeline PVC
820
+ # We prefer to use ReadWriteMany, but we can cope with ReadWriteOnce if necessary
821
+ if self.isSNO() or self.params["storage_class_rwx"] == "none":
822
+ self.pipelineStorageClass = self.getParam("storage_class_rwo")
823
+ self.pipelineStorageAccessMode = "ReadWriteOnce"
824
+ else:
825
+ self.pipelineStorageClass = self.getParam("storage_class_rwx")
826
+ self.pipelineStorageAccessMode = "ReadWriteMany"
827
+
828
+ @logMethodCall
829
+ def configSLS(self) -> None:
830
+ self.printH1("Configure AppPoint Licensing")
831
+ self.printDescription(
832
+ [
833
+ "By default the MAS instance will be configured to use a cluster-shared License, this provides a shared pool of AppPoints available to all MAS instances on the cluster.",
834
+ "",
835
+ ]
836
+ )
837
+
838
+ self.slsMode = 1
839
+ self.slsLicenseFileLocal = None
840
+
841
+ if self.showAdvancedOptions:
842
+ self.printDescription(
843
+ [
844
+ "Alternatively you may choose to install using a dedicated license only available to this MAS instance.",
845
+ " 1. Install MAS with Cluster-Shared License (AppPoints)",
846
+ " 2. Install MAS with Dedicated License (AppPoints)",
847
+ ]
848
+ )
849
+ self.slsMode = self.promptForInt("SLS Mode", default=1)
850
+
851
+ if self.slsMode not in [1, 2]:
852
+ self.fatalError(f"Invalid selection: {self.slsMode}")
853
+
854
+ if not (self.slsMode == 2 and not self.getParam("sls_namespace")):
855
+ sls_namespace = "ibm-sls" if self.slsMode == 1 else self.getParam("sls_namespace")
856
+ if findSLSByNamespace(sls_namespace, dynClient=self.dynamicClient):
857
+ print_formatted_text(HTML(f"<MediumSeaGreen>SLS auto-detected: {sls_namespace}</MediumSeaGreen>"))
858
+ print()
859
+ if not self.yesOrNo("Upload/Replace the license file"):
860
+ self.setParam("sls_action", "gencfg")
861
+ return
862
+
863
+ self.slsLicenseFileLocal = self.promptForFile("License file", mustExist=True, envVar="SLS_LICENSE_FILE_LOCAL")
864
+ self.setParam("sls_action", "install")
865
+
866
+ @logMethodCall
867
+ def configDRO(self) -> None:
868
+ self.promptForString("Contact e-mail address", "uds_contact_email")
869
+ self.promptForString("Contact first name", "uds_contact_firstname")
870
+ self.promptForString("Contact last name", "uds_contact_lastname")
871
+
872
+ if self.showAdvancedOptions:
873
+ self.promptForString("IBM Data Reporter Operator (DRO) Namespace", "dro_namespace", default="redhat-marketplace")
874
+
875
+ @logMethodCall
876
+ def configAppChannel(self, appId):
877
+ self.params[f"mas_app_channel_{appId}"] = prompt(HTML('<Yellow>Custom channel for Aibroker</Yellow> '))
878
+
879
+ @logMethodCall
880
+ def configOperationMode(self):
881
+ self.printH1("Configure Operational Mode")
882
+ self.printDescription([
883
+ "Maximo Application Suite can be installed in a non-production mode for internal development and testing, this setting cannot be changed after installation:",
884
+ " - All applications, add-ons, and solutions have 0 (zero) installation AppPoints in non-production installations.",
885
+ " - These specifications are also visible in the metrics that are shared with IBM and in the product UI.",
886
+ "",
887
+ " 1. Production",
888
+ " 2. Non-Production"
889
+ ])
890
+ self.operationalMode = self.promptForInt("Operational Mode", default=1)
891
+ if self.operationalMode == 1:
892
+ self.setParam("environment_type", "production")
893
+ else:
894
+ self.setParam("environment_type", "non-production")