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
mas/cli/install/app.py ADDED
@@ -0,0 +1,1316 @@
1
+ #!/usr/bin/env python
2
+ # *****************************************************************************
3
+ # Copyright (c) 2024 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
+ import logging.handlers
14
+ from sys import exit
15
+ from os import path, getenv
16
+ import re
17
+ import calendar
18
+
19
+ from openshift.dynamic.exceptions import NotFoundError
20
+
21
+ from prompt_toolkit import prompt, print_formatted_text, HTML
22
+ from prompt_toolkit.completion import WordCompleter
23
+
24
+ from tabulate import tabulate
25
+
26
+ from halo import Halo
27
+
28
+ from ..cli import BaseApp
29
+ from ..gencfg import ConfigGeneratorMixin
30
+ from .argBuilder import installArgBuilderMixin
31
+ from .argParser import installArgParser
32
+ from .settings import InstallSettingsMixin
33
+ from .summarizer import InstallSummarizerMixin
34
+ from .params import requiredParams, optionalParams
35
+ from .catalogs import supportedCatalogs
36
+
37
+ from mas.cli.validators import (
38
+ InstanceIDFormatValidator,
39
+ WorkspaceIDFormatValidator,
40
+ WorkspaceNameFormatValidator,
41
+ TimeoutFormatValidator,
42
+ StorageClassValidator,
43
+ JsonValidator,
44
+ OptimizerInstallPlanValidator
45
+ )
46
+
47
+ from mas.devops.ocp import createNamespace, getStorageClasses
48
+ from mas.devops.mas import (
49
+ getCurrentCatalog,
50
+ getDefaultStorageClasses,
51
+ isVersionEqualOrAfter
52
+ )
53
+ from mas.devops.sls import findSLSByNamespace
54
+ from mas.devops.data import getCatalog
55
+ from mas.devops.tekton import (
56
+ installOpenShiftPipelines,
57
+ updateTektonDefinitions,
58
+ preparePipelinesNamespace,
59
+ prepareInstallSecrets,
60
+ testCLI,
61
+ launchInstallPipeline
62
+ )
63
+
64
+ logger = logging.getLogger(__name__)
65
+
66
+
67
+ def logMethodCall(func):
68
+ def wrapper(self, *args, **kwargs):
69
+ logger.debug(f">>> InstallApp.{func.__name__}")
70
+ result = func(self, *args, **kwargs)
71
+ logger.debug(f"<<< InstallApp.{func.__name__}")
72
+ return result
73
+ return wrapper
74
+
75
+
76
+ class InstallApp(BaseApp, InstallSettingsMixin, InstallSummarizerMixin, ConfigGeneratorMixin, installArgBuilderMixin):
77
+ @logMethodCall
78
+ def validateCatalogSource(self):
79
+ catalogsAPI = self.dynamicClient.resources.get(api_version="operators.coreos.com/v1alpha1", kind="CatalogSource")
80
+ try:
81
+ catalog = catalogsAPI.get(name="ibm-operator-catalog", namespace="openshift-marketplace")
82
+ catalogDisplayName = catalog.spec.displayName
83
+
84
+ m = re.match(r".+(?P<catalogId>v[89]-(?P<catalogVersion>[0-9]+)-amd64)", catalogDisplayName)
85
+ if m:
86
+ # catalogId = v8-yymmdd-amd64
87
+ # catalogVersion = yymmdd
88
+ catalogId = m.group("catalogId")
89
+ elif re.match(r".+v8-amd64", catalogDisplayName):
90
+ catalogId = "v8-amd64"
91
+ else:
92
+ 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")
93
+
94
+ if catalogId != self.getParam("mas_catalog_version"):
95
+ 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")
96
+ except NotFoundError:
97
+ # There's no existing catalog installed
98
+ pass
99
+
100
+ @logMethodCall
101
+ def validateInternalRegistryAvailable(self):
102
+ """
103
+ We can save customers wasted time by detecting if the image-registry service
104
+ is available in the cluster. If it's not, and they've selected to install
105
+ Manage then their install is going to fail, so let's just prevent the install
106
+ starting in the first place.
107
+ """
108
+ serviceAPI = self.dynamicClient.resources.get(api_version="v1", kind="Service")
109
+ try:
110
+ serviceAPI.get(name="image-registry", namespace="openshift-image-registry")
111
+ except NotFoundError:
112
+ self.fatalError(
113
+ "\n".join([
114
+ "Unable to proceed with installation of Maximo Manage. Could not detect the required \"image-registry\" service in the openshift-image-registry namespace",
115
+ "For more information refer to <Orange><u>https://www.ibm.com/docs/en/masv-and-l/continuous-delivery?topic=installing-enabling-openshift-internal-image-registry</u></Orange>"
116
+ ])
117
+ )
118
+
119
+ @logMethodCall
120
+ def licensePrompt(self):
121
+ if not self.licenseAccepted:
122
+ self.printH1("License Terms")
123
+ self.printDescription([
124
+ "To continue with the installation, you must accept the license terms:",
125
+ self.licenses[self.getParam('mas_channel')]
126
+ ])
127
+
128
+ if self.noConfirm:
129
+ self.fatalError("You must accept the license terms with --accept-license when using the --no-confirm flag")
130
+ else:
131
+ if not self.yesOrNo("Do you accept the license terms"):
132
+ exit(1)
133
+
134
+ @logMethodCall
135
+ def configICR(self):
136
+ if self.devMode:
137
+ self.setParam("mas_icr_cp", getenv("MAS_ICR_CP", "docker-na-public.artifactory.swg-devops.com/wiotp-docker-local"))
138
+ self.setParam("mas_icr_cpopen", getenv("MAS_ICR_CPOPEN", "docker-na-public.artifactory.swg-devops.com/wiotp-docker-local/cpopen"))
139
+ self.setParam("sls_icr_cpopen", getenv("SLS_ICR_CPOPEN", "docker-na-public.artifactory.swg-devops.com/wiotp-docker-local/cpopen"))
140
+ else:
141
+ self.setParam("mas_icr_cp", getenv("MAS_ICR_CP", "cp.icr.io/cp"))
142
+ self.setParam("mas_icr_cpopen", getenv("MAS_ICR_CPOPEN", "icr.io/cpopen"))
143
+ self.setParam("sls_icr_cpopen", getenv("SLS_ICR_CPOPEN", "icr.io/cpopen"))
144
+
145
+ @logMethodCall
146
+ def configICRCredentials(self):
147
+ self.printH1("Configure IBM Container Registry")
148
+ self.promptForString("IBM entitlement key", "ibm_entitlement_key", isPassword=True)
149
+ if self.devMode:
150
+ self.promptForString("Artifactory username", "artifactory_username")
151
+ self.promptForString("Artifactory token", "artifactory_token", isPassword=True)
152
+
153
+ @logMethodCall
154
+ def configCertManager(self):
155
+ # Only install of Red Hat Cert-Manager has been supported since the January 2025 catalog update
156
+ self.setParam("cert_manager_provider", "redhat")
157
+ self.setParam("cert_manager_action", "install")
158
+
159
+ def formatCatalog(self, name: str) -> str:
160
+ # Convert "v9-241107-amd64" into "November 2024 Update (v9-241107-amd64)"
161
+ date = name.split("-")[1]
162
+ month = int(date[2:4])
163
+ monthName = calendar.month_name[month]
164
+ year = date[:2]
165
+ return f" - {monthName} 20{year} Update\n <Orange><u>https://ibm-mas.github.io/cli/catalogs/{name}</u></Orange>"
166
+
167
+ @logMethodCall
168
+ def processCatalogChoice(self) -> list:
169
+ self.catalogDigest = self.chosenCatalog["catalog_digest"]
170
+ self.catalogMongoDbVersion = self.chosenCatalog["mongo_extras_version_default"]
171
+ if self.architecture != "s390x" and self.architecture != "ppc64le":
172
+ self.catalogCp4dVersion = self.chosenCatalog["cpd_product_version_default"]
173
+
174
+ applications = {
175
+ "Core": "mas_core_version",
176
+ "Manage": "mas_manage_version",
177
+ "IoT": "mas_iot_version",
178
+ "Monitor": "mas_monitor_version",
179
+ "Assist": "mas_assist_version",
180
+ "Optimizer": "mas_optimizer_version",
181
+ "Predict": "mas_predict_version",
182
+ "Inspection": "mas_visualinspection_version",
183
+ "Facilities": "mas_facilities_version",
184
+ }
185
+ else:
186
+ applications = {
187
+ "Core": "mas_core_version",
188
+ "Manage": "mas_manage_version",
189
+ }
190
+
191
+ self.catalogReleases = {}
192
+ self.catalogTable = []
193
+
194
+ # Dynamically fetch the channels from the chosen catalog
195
+ # based on mas core
196
+ for channel in self.chosenCatalog["mas_core_version"]:
197
+ # {"9.1-feature": "9.1.x-feature"}
198
+ self.catalogReleases.update({channel.replace('.x', ''): channel})
199
+
200
+ # Generate catalogTable
201
+ for application, key in applications.items():
202
+ # Add 9.1-feature channel based off 9.0 to those apps that have not onboarded yet
203
+ if key in self.chosenCatalog:
204
+ tempChosenCatalog = self.chosenCatalog[key].copy()
205
+ if '9.1.x-feature' not in tempChosenCatalog:
206
+ tempChosenCatalog.update({"9.1.x-feature": tempChosenCatalog["9.0.x"]})
207
+
208
+ self.catalogTable.append({"": application} | {key.replace(".x", ""): value for key, value in sorted(tempChosenCatalog.items(), reverse=True)})
209
+
210
+ if self.architecture == "s390x" or self.architecture == "ppc64le":
211
+ summary = [
212
+ "",
213
+ "<u>Catalog Details</u>",
214
+ f"Catalog Image: icr.io/cpopen/ibm-maximo-operator-catalog:{self.getParam('mas_catalog_version')}",
215
+ f"Catalog Digest: {self.catalogDigest}",
216
+ f"MAS Releases: {', '.join(sorted(self.catalogReleases, reverse=True))}",
217
+ f"MongoDb: {self.catalogMongoDbVersion}",
218
+ ]
219
+ else:
220
+ summary = [
221
+ "",
222
+ "<u>Catalog Details</u>",
223
+ f"Catalog Image: icr.io/cpopen/ibm-maximo-operator-catalog:{self.getParam('mas_catalog_version')}",
224
+ f"Catalog Digest: {self.catalogDigest}",
225
+ f"MAS Releases: {', '.join(sorted(self.catalogReleases, reverse=True))}",
226
+ f"Cloud Pak for Data: {self.catalogCp4dVersion}",
227
+ f"MongoDb: {self.catalogMongoDbVersion}",
228
+ ]
229
+
230
+ return summary
231
+
232
+ @logMethodCall
233
+ def configCatalog(self):
234
+ self.printH1("IBM Maximo Operator Catalog Selection")
235
+ if self.devMode:
236
+ self.promptForString("Select catalog source", "mas_catalog_version", default="v9-master-amd64")
237
+ self.promptForString("Select channel", "mas_channel", default="9.1.x-dev")
238
+ else:
239
+ catalogInfo = getCurrentCatalog(self.dynamicClient)
240
+
241
+ if catalogInfo is None:
242
+ self.printDescription([
243
+ "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.",
244
+ "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.",
245
+ " - Learn more: <Orange><u>https://ibm-mas.github.io/cli/catalogs/</u></Orange>",
246
+ ""
247
+ ])
248
+ print("Supported Catalogs:")
249
+ for catalog in self.catalogOptions:
250
+ catalogString = self.formatCatalog(catalog)
251
+ print_formatted_text(HTML(f"{catalogString}"))
252
+ print()
253
+
254
+ catalogCompleter = WordCompleter(self.catalogOptions)
255
+ catalogSelection = self.promptForString("Select catalog", completer=catalogCompleter)
256
+ self.setParam("mas_catalog_version", catalogSelection)
257
+ else:
258
+ self.printDescription([
259
+ 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."
260
+ ])
261
+ self.setParam("mas_catalog_version", catalogInfo["catalogId"])
262
+
263
+ self.chosenCatalog = getCatalog(self.getParam("mas_catalog_version"))
264
+ catalogSummary = self.processCatalogChoice()
265
+ self.printDescription(catalogSummary)
266
+ self.printDescription([
267
+ "",
268
+ "Two types of release are available:",
269
+ " - GA releases of Maximo Application Suite are supported under IBM's standard 3+1+3 support lifecycle policy.",
270
+ " - 'Feature' releases allow early access to new features for evaluation in non-production environments and are only supported through to the next GA release.",
271
+ ""
272
+ ])
273
+
274
+ print(tabulate(self.catalogTable, headers="keys", tablefmt="simple_grid"))
275
+
276
+ releaseCompleter = WordCompleter(sorted(self.catalogReleases, reverse=True))
277
+ releaseSelection = self.promptForString("Select release", completer=releaseCompleter)
278
+
279
+ self.setParam("mas_channel", self.catalogReleases[releaseSelection])
280
+
281
+ @logMethodCall
282
+ def configSLS(self) -> None:
283
+ self.printH1("Configure AppPoint Licensing")
284
+ self.printDescription(
285
+ [
286
+ "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.",
287
+ "",
288
+ ]
289
+ )
290
+
291
+ self.slsMode = 1
292
+ self.slsLicenseFileLocal = None
293
+
294
+ if self.showAdvancedOptions:
295
+ self.printDescription(
296
+ [
297
+ "Alternatively you may choose to install using a dedicated license only available to this MAS instance.",
298
+ " 1. Install MAS with Cluster-Shared License (AppPoints)",
299
+ " 2. Install MAS with Dedicated License (AppPoints)",
300
+ ]
301
+ )
302
+ self.slsMode = self.promptForInt("SLS Mode", default=1)
303
+
304
+ if self.slsMode not in [1, 2]:
305
+ self.fatalError(f"Invalid selection: {self.slsMode}")
306
+
307
+ if not (self.slsMode == 2 and not self.getParam("sls_namespace")):
308
+ sls_namespace = "ibm-sls" if self.slsMode == 1 else self.getParam("sls_namespace")
309
+ if findSLSByNamespace(sls_namespace, dynClient=self.dynamicClient):
310
+ print_formatted_text(HTML(f"<MediumSeaGreen>SLS auto-detected: {sls_namespace}</MediumSeaGreen>"))
311
+ print()
312
+ if not self.yesOrNo("Upload/Replace the license file"):
313
+ self.setParam("sls_action", "gencfg")
314
+ return
315
+
316
+ self.slsLicenseFileLocal = self.promptForFile("License file", mustExist=True, envVar="SLS_LICENSE_FILE_LOCAL")
317
+ self.setParam("sls_action", "install")
318
+
319
+ @logMethodCall
320
+ def configDRO(self) -> None:
321
+ self.promptForString("Contact e-mail address", "uds_contact_email")
322
+ self.promptForString("Contact first name", "uds_contact_firstname")
323
+ self.promptForString("Contact last name", "uds_contact_lastname")
324
+
325
+ if self.showAdvancedOptions:
326
+ self.promptForString("IBM Data Reporter Operator (DRO) Namespace", "dro_namespace", default="redhat-marketplace")
327
+
328
+ @logMethodCall
329
+ def selectLocalConfigDir(self) -> None:
330
+ if self.localConfigDir is None:
331
+ # You need to tell us where the configuration file can be found
332
+ self.localConfigDir = self.promptForDir("Select Local configuration directory")
333
+
334
+ @logMethodCall
335
+ def configGrafana(self) -> None:
336
+ if self.architecture == "s390x" or self.architecture == "ppc64le":
337
+ # We are not supporting Grafana on s390x /ppc64le at the moment
338
+ self.setParam("grafana_action", "none")
339
+ else:
340
+ try:
341
+ packagemanifestAPI = self.dynamicClient.resources.get(api_version="packages.operators.coreos.com/v1", kind="PackageManifest")
342
+ packagemanifestAPI.get(name="grafana-operator", namespace="openshift-marketplace")
343
+ if self.skipGrafanaInstall:
344
+ self.setParam("grafana_action", "none")
345
+ else:
346
+ self.setParam("grafana_action", "install")
347
+ except NotFoundError:
348
+ self.setParam("grafana_action", "none")
349
+
350
+ if self.interactiveMode and self.showAdvancedOptions:
351
+ self.printH1("Configure Grafana")
352
+ if self.getParam("grafana_action") == "none":
353
+ print_formatted_text("The Grafana operator package is not available in any catalogs on the target cluster, the installation of Grafana will be disabled")
354
+ else:
355
+ self.promptForString("Install namespace", "grafana_v5_namespace", default="grafana5")
356
+ self.promptForString("Grafana storage size", "grafana_instance_storage_size", default="10Gi")
357
+
358
+ @logMethodCall
359
+ def configSpecialCharacters(self):
360
+ if self.showAdvancedOptions:
361
+ self.printH1("Configure special characters for userID and username")
362
+ self.printDescription([
363
+ "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."
364
+ ])
365
+ self.yesOrNo("Allow special characters for user IDs and usernames", "mas_special_characters")
366
+
367
+ @logMethodCall
368
+ def configCP4D(self):
369
+ if self.getParam("mas_catalog_version") in self.catalogOptions:
370
+ # Note: this will override any version provided by the user (which is intentional!)
371
+ logger.debug(f"Using automatic CP4D product version: {self.getParam('cpd_product_version')}")
372
+ self.setParam("cpd_product_version", self.chosenCatalog["cpd_product_version_default"])
373
+ elif self.getParam("cpd_product_version") == "":
374
+ if self.noConfirm:
375
+ self.fatalError("Cloud Pak for Data version must be set manually, but --no-confirm has been set without setting --cp4d-version")
376
+ self.printDescription([
377
+ f"Unknown catalog {self.getParam('mas_catalog_version')}, please manually select the version of Cloud Pak for Data to use"
378
+ ])
379
+ self.promptForString("Cloud Pak for Data product version", "cpd_product_version", default="5.1.3")
380
+ logger.debug(f"Using user-provided (prompt) CP4D product version: {self.getParam('cpd_product_version')}")
381
+ else:
382
+ logger.debug(f"Using user-provided (flags) CP4D product version: {self.getParam('cpd_product_version')}")
383
+ self.deployCP4D = True
384
+
385
+ @logMethodCall
386
+ def configSSOProperties(self):
387
+ if self.showAdvancedOptions:
388
+ self.printH1("Single Sign-On (SSO)")
389
+ self.printDescription([
390
+ "Many aspects of Maximo Application Suite's Single Sign-On (SSO) can be customized:",
391
+ " - Idle session automatic logout timer",
392
+ " - Session, access token, and refresh token timeouts",
393
+ " - Default identity provider (IDP), and seamless login",
394
+ " - Brower cookie properties"
395
+ ])
396
+ if self.yesOrNo("Configure SSO properties"):
397
+ self.promptForInt("Idle session logout timer (seconds)", "idle_timeout")
398
+ self.promptForString("Session timeout (e.g. '12h' for 12 hours)", "idp_session_timeout", validator=TimeoutFormatValidator())
399
+ self.promptForString("Access token timeout (e.g. '30m' for 30 minutes)", "access_token_timeout", validator=TimeoutFormatValidator())
400
+ self.promptForString("Refresh token timeout (e.g. '12h' for 12 hours)", "refresh_token_timeout", validator=TimeoutFormatValidator())
401
+ self.promptForString("Default Identity Provider", "default_idp")
402
+
403
+ self.promptForString("SSO cookie name", "sso_cookie_name")
404
+ self.yesOrNo("Enable seamless login", "seamless_login")
405
+ self.yesOrNo("Allow default SSO cookie name", "allow_default_sso_cookie_name")
406
+ self.yesOrNo("Use only custom cookie name", "use_only_custom_cookie_name")
407
+ self.yesOrNo("Disable LDAP cookie", "disable_ldap_cookie")
408
+ self.yesOrNo("Allow custom cache key", "allow_custom_cache_key")
409
+
410
+ @logMethodCall
411
+ def configGuidedTour(self):
412
+ if self.showAdvancedOptions:
413
+ self.printH1("Enable Guided Tour")
414
+ self.printDescription([
415
+ "By default, Maximo Application Suite is configured with guided tour, you can disable this if it not required"
416
+ ])
417
+ if not self.yesOrNo("Enable Guided Tour"):
418
+ self.setParam("mas_enable_walkme", "false")
419
+
420
+ @logMethodCall
421
+ def configMAS(self):
422
+ self.printH1("Configure MAS Instance")
423
+ self.printDescription([
424
+ "Instance ID restrictions:",
425
+ " - Must be 3-12 characters long",
426
+ " - Must only use lowercase letters, numbers, and hypen (-) symbol",
427
+ " - Must start with a lowercase letter",
428
+ " - Must end with a lowercase letter or a number"
429
+ ])
430
+ self.promptForString("Instance ID", "mas_instance_id", validator=InstanceIDFormatValidator())
431
+ self.printDescription([
432
+ "",
433
+ "Workspace ID restrictions:",
434
+ " - Must be 3-12 characters long",
435
+ " - Must only use lowercase letters and numbers",
436
+ " - Must start with a lowercase letter"
437
+ ])
438
+ self.promptForString("Workspace ID", "mas_workspace_id", validator=WorkspaceIDFormatValidator())
439
+ self.printDescription([
440
+ "",
441
+ "Workspace display name restrictions:",
442
+ " - Must be 3-300 characters long"
443
+ ])
444
+ self.promptForString("Workspace name", "mas_workspace_name", validator=WorkspaceNameFormatValidator())
445
+
446
+ if self.slsMode == 2 and not self.getParam("sls_namespace"):
447
+ self.setParam("sls_namespace", f"mas-{self.getParam('mas_instance_id')}-sls")
448
+
449
+ self.configOperationMode()
450
+ self.configCATrust()
451
+ self.configDNSAndCerts()
452
+ self.configSSOProperties()
453
+ self.configSpecialCharacters()
454
+ self.configGuidedTour()
455
+
456
+ @logMethodCall
457
+ def configCATrust(self) -> None:
458
+ if self.showAdvancedOptions:
459
+ self.printH1("Certificate Authority Trust")
460
+ self.printDescription([
461
+ "By default, Maximo Application Suite is configured to trust well-known certificate authoritories, you can disable this so that it will only trust the CAs that you explicitly define"
462
+ ])
463
+ self.yesOrNo("Trust default CAs", "mas_trust_default_cas")
464
+ else:
465
+ self.setParam("mas_trust_default_cas", True)
466
+
467
+ @logMethodCall
468
+ def configOperationMode(self):
469
+ self.printH1("Configure Operational Mode")
470
+ self.printDescription([
471
+ "Maximo Application Suite can be installed in a non-production mode for internal development and testing, this setting cannot be changed after installation:",
472
+ " - All applications, add-ons, and solutions have 0 (zero) installation AppPoints in non-production installations.",
473
+ " - These specifications are also visible in the metrics that are shared with IBM and in the product UI.",
474
+ "",
475
+ " 1. Production",
476
+ " 2. Non-Production"
477
+ ])
478
+ self.operationalMode = self.promptForInt("Operational Mode", default=1)
479
+
480
+ @logMethodCall
481
+ def configAnnotations(self):
482
+ if self.operationalMode == 2:
483
+ self.setParam("mas_annotations", "mas.ibm.com/operationalMode=nonproduction")
484
+
485
+ @logMethodCall
486
+ def configSNO(self):
487
+ if self.isSNO():
488
+ self.setParam("mongodb_replicas", "1")
489
+ self.setParam("mongodb_cpu_requests", "500m")
490
+ self.setParam("mas_app_settings_aio_flag", "false")
491
+
492
+ @logMethodCall
493
+ def configDNSAndCerts(self):
494
+ if self.showAdvancedOptions:
495
+ self.printH1("Cluster Ingress Secret Override")
496
+ self.printDescription([
497
+ "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",
498
+ "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"
499
+ ])
500
+ self.promptForString("Cluster ingress certificate secret name", "ocp_ingress_tls_secret_name", default="")
501
+
502
+ self.printH1("Configure Domain & Certificate Management")
503
+ configureDomainAndCertMgmt = self.yesOrNo('Configure domain & certificate management')
504
+ if configureDomainAndCertMgmt:
505
+ configureDomain = self.yesOrNo('Configure custom domain')
506
+ if configureDomain:
507
+ self.promptForString("MAS top-level domain", "mas_domain")
508
+ self.printDescription([
509
+ "",
510
+ "DNS Integrations:",
511
+ " 1. Cloudflare",
512
+ " 2. IBM Cloud Internet Services",
513
+ " 3. AWS Route 53",
514
+ " 4. None (I will set up DNS myself)"
515
+ ])
516
+
517
+ dnsProvider = self.promptForInt("DNS Provider")
518
+
519
+ if dnsProvider == 1:
520
+ self.configDNSAndCertsCloudflare()
521
+ elif dnsProvider == 2:
522
+ self.configDNSAndCertsCIS()
523
+ elif dnsProvider == 3:
524
+ self.configDNSAndCertsRoute53()
525
+ elif dnsProvider == 4:
526
+ # Use MAS default self-signed cluster issuer with a custom domain
527
+ self.setParam("dns_provider", "")
528
+ self.setParam("mas_cluster_issuer", "")
529
+ else:
530
+ # Use MAS default self-signed cluster issuer with the default domain
531
+ self.setParam("dns_provider", "")
532
+ self.setParam("mas_domain", "")
533
+ self.setParam("mas_cluster_issuer", "")
534
+ self.manualCerts = self.yesOrNo("Configure manual certificates")
535
+ self.setParam("mas_manual_cert_mgmt", self.manualCerts)
536
+ if self.getParam("mas_manual_cert_mgmt"):
537
+ self.manualCertsDir = self.promptForDir("Enter the path containing the manual certificates", mustExist=True)
538
+ else:
539
+ self.manualCertsDir = None
540
+
541
+ @logMethodCall
542
+ def configDNSAndCertsCloudflare(self):
543
+ # User has chosen to set up DNS integration with Cloudflare
544
+ self.setParam("dns_provider", "cloudflare")
545
+ self.promptForString("Cloudflare e-mail", "cloudflare_email")
546
+ self.promptForString("Cloudflare API token", "cloudflare_apitoken")
547
+ self.promptForString("Cloudflare zone", "cloudflare_zone")
548
+ self.promptForString("Cloudflare subdomain", "cloudflare_subdomain")
549
+
550
+ self.printDescription([
551
+ "Certificate Issuer:",
552
+ " 1. LetsEncrypt (Production)",
553
+ " 2. LetsEncrypt (Staging)",
554
+ " 3. Self-Signed"
555
+ ])
556
+ certIssuer = self.promptForInt("Certificate issuer")
557
+ certIssuerOptions = [
558
+ f"{self.getParam('mas_instance_id')}-cloudflare-le-prod",
559
+ f"{self.getParam('mas_instance_id')}-cloudflare-le-stg",
560
+ ""
561
+ ]
562
+ self.setParam("mas_cluster_issuer", certIssuerOptions[certIssuer - 1])
563
+
564
+ @logMethodCall
565
+ def configDNSAndCertsCIS(self):
566
+ self.setParam("dns_provider", "cis")
567
+ self.promptForString("CIS e-mail", "cis_email")
568
+ self.promptForString("CIS API token", "cis_apikey")
569
+ self.promptForString("CIS CRN", "cis_crn")
570
+ self.promptForString("CIS subdomain", "cis_subdomain")
571
+
572
+ self.printDescription([
573
+ "Certificate Issuer:",
574
+ " 1. LetsEncrypt (Production)",
575
+ " 2. LetsEncrypt (Staging)",
576
+ " 3. Self-Signed"
577
+ ])
578
+ certIssuer = self.promptForInt("Certificate issuer")
579
+ certIssuerOptions = [
580
+ f"{self.getParam('mas_instance_id')}-cis-le-prod",
581
+ f"{self.getParam('mas_instance_id')}-cis-le-stg",
582
+ ""
583
+ ]
584
+ self.setParam("mas_cluster_issuer", certIssuerOptions[certIssuer - 1])
585
+
586
+ @logMethodCall
587
+ def configDNSAndCertsRoute53(self):
588
+ self.setParam("dns_provider", "route53")
589
+ self.printDescription([
590
+ "Provide your AWS account access key ID and secret access key",
591
+ "This will be used to authenticate into the AWS account where your AWS Route 53 hosted zone instance is located",
592
+ ""
593
+ ])
594
+ self.promptForString("AWS Access Key ID", "aws_access_key_id", isPassword=True)
595
+ self.promptForString("AWS Secret Access Key", "aws_secret_access_key", isPassword=True)
596
+
597
+ self.printDescription([
598
+ "Provide your AWS Route 53 hosted zone instance details",
599
+ "This information will be used to create webhook resources between your cluster and your AWS Route 53 instance (cluster issuer and cname records)",
600
+ "in order for it to be able to resolve DNS entries for all the subdomains created for your Maximo Application Suite instance",
601
+ "",
602
+ "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"
603
+ ])
604
+ self.promptForString("AWS Route 53 hosted zone name", "route53_hosted_zone_name")
605
+ self.promptForString("AWS Route 53 hosted zone region", "route53_hosted_zone_region")
606
+ self.promptForString("AWS Route 53 subdomain", "route53_subdomain")
607
+ self.promptForString("AWS Route 53 e-mail", "route53_email")
608
+
609
+ self.setParam("mas_cluster_issuer", f"{self.getParam('mas_instance_id')}-route53-le-prod")
610
+
611
+ @logMethodCall
612
+ def configApps(self):
613
+ self.printH1("Application Selection")
614
+ self.installIoT = self.yesOrNo("Install IoT")
615
+
616
+ if self.installIoT:
617
+ self.configAppChannel("iot")
618
+ self.installMonitor = self.yesOrNo("Install Monitor")
619
+ else:
620
+ self.installMonitor = False
621
+
622
+ if self.installMonitor:
623
+ self.configAppChannel("monitor")
624
+
625
+ self.manageAppName = "Manage"
626
+ self.isManageFoundation = False
627
+ self.installManage = self.yesOrNo(f"Install {self.manageAppName}")
628
+
629
+ # 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
630
+ # Also, we need to force self.installManage to be True because Manage must always be installed in MAS 9.1 or later
631
+ if not self.installManage:
632
+ if not self.getParam("mas_channel").startswith("8.") and not self.getParam("mas_channel").startswith("9.0"):
633
+ self.installManage = True
634
+ self.isManageFoundation = True
635
+ self.setParam("mas_app_settings_aio_flag", "false")
636
+ self.manageAppName = "Manage foundation"
637
+ self.printDescription([f"{self.manageAppName} installs the following capabilities: User, Security groups, Application configurator and Mobile configurator."])
638
+
639
+ if self.installManage:
640
+ self.configAppChannel("manage")
641
+
642
+ if self.installIoT and self.installManage:
643
+ self.installPredict = self.yesOrNo("Install Predict")
644
+ else:
645
+ self.installPredict = False
646
+
647
+ if self.installPredict:
648
+ self.configAppChannel("predict")
649
+
650
+ # 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
651
+ if isVersionEqualOrAfter('9.0.0', self.getParam("mas_channel")):
652
+ self.installAssist = self.yesOrNo("Install Assist")
653
+ if self.installAssist:
654
+ self.configAppChannel("assist")
655
+ else:
656
+ self.installAssist = False
657
+
658
+ self.installOptimizer = self.yesOrNo("Install Optimizer")
659
+ if self.installOptimizer:
660
+ self.configAppChannel("optimizer")
661
+
662
+ self.installInspection = self.yesOrNo("Install Visual Inspection")
663
+ if self.installInspection:
664
+ self.configAppChannel("visualinspection")
665
+
666
+ if isVersionEqualOrAfter('9.1.0', self.getParam("mas_channel")) and self.getParam("mas_channel") != '9.1.x-feature':
667
+ self.installFacilities = self.yesOrNo("Install Real Estate and Facilities")
668
+ if self.installFacilities:
669
+ self.configAppChannel("facilities")
670
+ else:
671
+ self.installFacilities = False
672
+
673
+ @logMethodCall
674
+ def configAppChannel(self, appId):
675
+ versions = self.getCompatibleVersions(self.params["mas_channel"], appId)
676
+ if len(versions) == 0:
677
+ self.params[f"mas_app_channel_{appId}"] = prompt(HTML('<Yellow>Custom channel</Yellow> '))
678
+ else:
679
+ self.params[f"mas_app_channel_{appId}"] = versions[0]
680
+
681
+ @logMethodCall
682
+ def configStorageClasses(self):
683
+ self.printH1("Configure Storage Class Usage")
684
+ self.printDescription([
685
+ "Maximo Application Suite and it's dependencies require storage classes that support ReadWriteOnce (RWO) and ReadWriteMany (RWX) access modes:",
686
+ " - ReadWriteOnce volumes can be mounted as read-write by multiple pods on a single node.",
687
+ " - ReadWriteMany volumes can be mounted as read-write by multiple pods across many nodes.",
688
+ ""
689
+ ])
690
+ defaultStorageClasses = getDefaultStorageClasses(self.dynamicClient)
691
+ if defaultStorageClasses.provider is not None:
692
+ print_formatted_text(HTML(f"<MediumSeaGreen>Storage provider auto-detected: {defaultStorageClasses.providerName}</MediumSeaGreen>"))
693
+ print_formatted_text(HTML(f"<LightSlateGrey> - Storage class (ReadWriteOnce): {defaultStorageClasses.rwo}</LightSlateGrey>"))
694
+ print_formatted_text(HTML(f"<LightSlateGrey> - Storage class (ReadWriteMany): {defaultStorageClasses.rwx}</LightSlateGrey>"))
695
+ self.storageClassProvider = defaultStorageClasses.provider
696
+ self.params["storage_class_rwo"] = defaultStorageClasses.rwo
697
+ self.params["storage_class_rwx"] = defaultStorageClasses.rwx
698
+
699
+ overrideStorageClasses = False
700
+ if "storage_class_rwx" in self.params and self.params["storage_class_rwx"] != "":
701
+ overrideStorageClasses = not self.yesOrNo("Use the auto-detected storage classes")
702
+
703
+ if "storage_class_rwx" not in self.params or self.params["storage_class_rwx"] == "" or overrideStorageClasses:
704
+ self.storageClassProvider = "custom"
705
+
706
+ self.printDescription([
707
+ "Select the ReadWriteOnce and ReadWriteMany storage classes to use from the list below:",
708
+ "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"
709
+ ])
710
+ for storageClass in getStorageClasses(self.dynamicClient):
711
+ print_formatted_text(HTML(f"<LightSlateGrey> - {storageClass.metadata.name}</LightSlateGrey>"))
712
+
713
+ self.params["storage_class_rwo"] = prompt(HTML('<Yellow>ReadWriteOnce (RWO) storage class</Yellow> '), validator=StorageClassValidator(), validate_while_typing=False)
714
+ self.params["storage_class_rwx"] = prompt(HTML('<Yellow>ReadWriteMany (RWX) storage class</Yellow> '), validator=StorageClassValidator(), validate_while_typing=False)
715
+
716
+ # Configure storage class for pipeline PVC
717
+ # We prefer to use ReadWriteMany, but we can cope with ReadWriteOnce if necessary
718
+ if self.isSNO() or self.params["storage_class_rwx"] == "none":
719
+ self.pipelineStorageClass = self.getParam("storage_class_rwo")
720
+ self.pipelineStorageAccessMode = "ReadWriteOnce"
721
+ else:
722
+ self.pipelineStorageClass = self.getParam("storage_class_rwx")
723
+ self.pipelineStorageAccessMode = "ReadWriteMany"
724
+
725
+ @logMethodCall
726
+ def setIoTStorageClasses(self) -> None:
727
+ if self.installIoT:
728
+ self.setParam("mas_app_settings_iot_fpl_pvc_storage_class", self.getParam("storage_class_rwo"))
729
+ self.setParam("mas_app_settings_iot_mqttbroker_pvc_storage_class", self.getParam("storage_class_rwo"))
730
+
731
+ @logMethodCall
732
+ def optimizerSettings(self) -> None:
733
+ if self.installOptimizer:
734
+ self.printH1("Configure Maximo Optimizer")
735
+ if self.isSNO():
736
+ self.printDescription(["Using Optimizer 'limited' plan as it is being installed in a single node cluster"])
737
+ self.setParam("mas_app_plan_optimizer", "limited")
738
+ else:
739
+ self.printDescription(["Customize your Optimizer installation, 'full' and 'limited' install plans are available, refer to the product documentation for more information"])
740
+ self.promptForString("Plan [full/limited]", "mas_app_plan_optimizer", default="full", validator=OptimizerInstallPlanValidator())
741
+
742
+ @logMethodCall
743
+ def predictSettings(self) -> None:
744
+ if self.showAdvancedOptions and self.installPredict:
745
+ self.printH1("Configure Maximo Predict")
746
+ self.printDescription([
747
+ "Predict application supports integration with IBM SPSS which is an optional service installed on top of IBM Cloud Pak for Data",
748
+ "Unless requested these will not be installed"
749
+ ])
750
+ self.configCP4D()
751
+ self.yesOrNo("Install IBM SPSS Statistics", "cpd_install_spss")
752
+
753
+ @logMethodCall
754
+ def assistSettings(self) -> None:
755
+ if self.installAssist:
756
+ self.printH1("Configure Maximo Assist")
757
+ self.printDescription([
758
+ "Assist requires access to Cloud Object Storage (COS), this install supports automatic setup using either IBMCloud COS or in-cluster COS via OpenShift Container Storage/OpenShift Data Foundation (OCS/ODF)"
759
+ ])
760
+ self.configCP4D()
761
+ self.promptForString("COS Provider [ibm/ocs]", "cos_type")
762
+ if self.getParam("cos_type") == "ibm":
763
+ self.promptForString("IBM Cloud API Key", "cos_apikey", isPassword=True)
764
+ self.promptForString("IBM Cloud Resource Group", "cos_resourcegroup")
765
+
766
+ @logMethodCall
767
+ def facilitiesSettings(self) -> None:
768
+ if self.installFacilities:
769
+ self.printH1("Configure Maximo Real Estate and Facilities")
770
+ self.printDescription([
771
+ "Real Estate and Facilities custom configurations"
772
+ ])
773
+ self.printDescription([
774
+ "Maximo Real Estate and Facilities Size:",
775
+ " 1. Small",
776
+ " 2. Medium",
777
+ " 3. Large"
778
+ ])
779
+ self.promptForListSelect("Select the size:", ["small", "medium", "large"], "mas_ws_facilities_size")
780
+
781
+ if self.showAdvancedOptions:
782
+ self.printH2("Maximo Real Estate and Facilities Settings - Advanced")
783
+ self.printDescription([
784
+ "Advanced configurations for Real Estate and Facilities are added through an additional file called facilities-configs.yaml"
785
+ ])
786
+ if self.yesOrNo("Supply extra XML tags for Real Estate and Facilities server.xml"):
787
+ self.promptForString("Real Estate and Facilities Liberty Extension Secret Name", "mas_ws_facilities_liberty_extension_XML")
788
+ if self.yesOrNo("Supply custom AES Encryption Password"):
789
+ self.promptForString("Real Estate and Facilities AES Vault Secret Name", "mas_ws_facilities_vault_secret")
790
+
791
+ self.promptForString("Set Real Estate and Facilities Routes Timeout:", "mas_ws_facilities_routes_timeout", default="600s")
792
+ self.promptForInt("Set Facilities maximum connection poll size:", "mas_ws_facilities_db_maxconnpoolsize", default=200)
793
+
794
+ self.printDescription(["Real Estate and Facilities Persistent Volume Storage Configuration"])
795
+ defaultStorageClasses = getDefaultStorageClasses(self.dynamicClient)
796
+ notUseAutodetectedStorageClasses = False
797
+ if defaultStorageClasses.provider is not None:
798
+ self.storageClassProvider = defaultStorageClasses.provider
799
+ print_formatted_text(HTML(f"<MediumSeaGreen>Storage provider auto-detected: {defaultStorageClasses.providerName}</MediumSeaGreen>"))
800
+ print_formatted_text(HTML(f"<LightSlateGrey> - Storage class (ReadWriteMany): {defaultStorageClasses.rwx}</LightSlateGrey>"))
801
+ print_formatted_text(HTML(f"<LightSlateGrey> - Storage class (ReadWriteOnce): {defaultStorageClasses.rwo}</LightSlateGrey>"))
802
+ if self.yesOrNo("Use the auto-detected storage classes"):
803
+ self.printDescription([
804
+ "Storage Mode for Userfiles PVC:",
805
+ " 1. ReadWriteMany",
806
+ " 2. ReadWriteOnce"
807
+ ])
808
+ storageMode = self.promptForListSelect("Select the storage mode for user files PVC:", ["ReadWriteMany", "ReadWriteOnce"], "mas_ws_facilities_storage_userfiles_mode", default=1)
809
+ _ = self.setParam("mas_ws_facilities_storage_userfiles_class", defaultStorageClasses.rwx) if storageMode == "ReadWriteMany" else self.setParam("mas_ws_facilities_storage_userfiles_class", defaultStorageClasses.rwo)
810
+ self.promptForInt("User file PVC size (Gb):", "mas_ws_facilities_storage_userfiles_size", default=50)
811
+ storageMode = self.promptForListSelect("Select the storage mode for log PVC:", ["ReadWriteMany", "ReadWriteOnce"], "mas_ws_facilities_storage_log_mode", default=1)
812
+ _ = self.setParam("mas_ws_facilities_storage_log_class", defaultStorageClasses.rwx) if storageMode == "ReadWriteMany" else self.setParam("mas_ws_facilities_storage_log_class", defaultStorageClasses.rwo)
813
+ self.promptForInt("Log PVC size (Gb):", "mas_ws_facilities_storage_log_size", default=30)
814
+ else:
815
+ notUseAutodetectedStorageClasses = True
816
+ if defaultStorageClasses.provider is None or notUseAutodetectedStorageClasses:
817
+ for storageClass in getStorageClasses(self.dynamicClient):
818
+ print_formatted_text(HTML(f"<LightSlateGrey> - {storageClass.metadata.name}</LightSlateGrey>"))
819
+ self.promptForString("Select storage class for user files PVC:", "mas_ws_facilities_storage_userfiles_class")
820
+ self.promptForString("Select storage class for log PVC:", "mas_ws_facilities_storage_log_class")
821
+ self.printDescription([
822
+ "Storage Mode for Userfiles PVC:",
823
+ " 1. ReadWriteMany",
824
+ " 2. ReadWriteOnce"
825
+ ])
826
+ self.promptForListSelect("Select the storage mode for user files PVC:", ["ReadWriteMany", "ReadWriteOnce"], "mas_ws_facilities_storage_userfiles_mode", default=1)
827
+ self.promptForListSelect("Select the storage mode for log PVC:", ["ReadWriteMany", "ReadWriteOnce"], "mas_ws_facilities_storage_log_mode", default=1)
828
+ self.promptForInt("User file PVC size (Gb):", "mas_ws_facilities_storage_userfiles_size", default=50)
829
+ self.promptForInt("Log PVC size (Gb):", "mas_ws_facilities_storage_log_size", default=30)
830
+
831
+ if self.yesOrNo("Supply configuration for dedicated workflow agents"):
832
+ print_formatted_text(HTML("<LightSlateGrey> Example: '[{\"name\":\"dwfa1\",\"members\":[{\"name\": \"u1\", \"class\": \"user\"}]}, {\"name\":\"dwfa2\",\"members\":[{\"name\": \"u2\", \"class\": \"user\"},{\"name\":\"g1\", \"class\":\"group\"}]}]' </LightSlateGrey>"))
833
+ self.promptForString("Dedicated Workflow Agent JSON:", "mas_ws_facilities_dwfagents", validator=JsonValidator())
834
+
835
+ # If advanced options is selected, we need to create a file to add props not supported by Tekton
836
+ self.selectLocalConfigDir()
837
+ facilitiesConfigsPath = path.join(self.localConfigDir, "facilities-configs.yaml")
838
+ self.generateFacilitiesCfg(destination=facilitiesConfigsPath)
839
+ self.setParam("mas_ws_facilities_config_file", "/workspace/configs/facilities-configs.yaml")
840
+
841
+ @logMethodCall
842
+ def chooseInstallFlavour(self) -> None:
843
+ self.printH1("Choose Install Mode")
844
+ self.printDescription([
845
+ "There are two flavours of the interactive install to choose from: <u>Simplified</u> and <u>Advanced</u>. The simplified option will present fewer dialogs, but you lose the ability to configure the following aspects of the installation:",
846
+ " - Configure installation namespaces",
847
+ " - Provide pod templates",
848
+ " - Configure Single Sign-On (SSO) settings"
849
+ " - Configure whether to trust well-known certificate authorities by default (defaults to enabled)",
850
+ " - Configure whether the Guided Tour feature is enabled (defaults to enabled)",
851
+ " - Configure whether special characters are allowed in usernames and userids (defaults to disabled)",
852
+ " - Configure a custom domain, DNS integrations, and manual certificates",
853
+ " - Customize Maximo Manage database settings (schema, tablespace, indexspace)",
854
+ " - Customize Maximo Manage server bundle configuration (defaults to \"all\" configuration)",
855
+ " - Enable optional Maximo Manage integration Cognos Analytics and Watson Studio Local",
856
+ " - Enable optional Maximo Predict integration with SPSS",
857
+ " - Enable optional IBM Turbonomic integration",
858
+ " - Enable optional Real Estate and Facilities configurations",
859
+ " - Customize Db2 node affinity and tolerations, memory, cpu, and storage settings (when using the IBM Db2 Universal Operator)",
860
+ " - Choose alternative Apache Kafka providers (default to Strimzi)",
861
+ " - Customize Grafana storage settings"
862
+ ])
863
+ self.showAdvancedOptions = self.yesOrNo("Show advanced installation options")
864
+
865
+ @logMethodCall
866
+ def interactiveMode(self, simplified: bool, advanced: bool) -> None:
867
+ # Interactive mode
868
+ self.interactiveMode = True
869
+
870
+ if simplified:
871
+ self.showAdvancedOptions = False
872
+ elif advanced:
873
+ self.showAdvancedOptions = True
874
+ else:
875
+ self.chooseInstallFlavour()
876
+
877
+ # Catalog
878
+ self.configCatalog()
879
+ if not self.devMode:
880
+ self.validateCatalogSource()
881
+ self.licensePrompt()
882
+
883
+ # SNO & Storage Classes
884
+ self.configSNO()
885
+ self.configStorageClasses()
886
+
887
+ # Licensing (SLS and DRO)
888
+ self.configSLS()
889
+ self.configDRO()
890
+ self.configICRCredentials()
891
+
892
+ # MAS Core
893
+ self.configCertManager()
894
+ self.configMAS()
895
+
896
+ # MAS Applications
897
+ self.configApps()
898
+ self.validateInternalRegistryAvailable()
899
+ # Note: manageSettings(), predictSettings(), or assistSettings() functions can trigger configCP4D()
900
+ self.manageSettings()
901
+ self.optimizerSettings()
902
+ self.predictSettings()
903
+ self.assistSettings()
904
+ self.facilitiesSettings()
905
+
906
+ # Dependencies
907
+ self.configMongoDb()
908
+ self.configDb2()
909
+ self.configKafka() # Will only do anything if IoT has been selected for install
910
+
911
+ self.configGrafana()
912
+ self.configTurbonomic()
913
+
914
+ # TODO: Support ECK integration via the interactive install mode
915
+ # TODO: Support MAS superuser username/password via the interactive install mode
916
+
917
+ @logMethodCall
918
+ def nonInteractiveMode(self) -> None:
919
+ self.interactiveMode = False
920
+
921
+ # Set defaults
922
+ # ---------------------------------------------------------------------
923
+ # Unless a config file named "mongodb-system.yaml" is provided via the additional configs mechanism we will be installing a new MongoDb instance
924
+ self.setParam("mongodb_action", "install")
925
+
926
+ self.storageClassProvider = "custom"
927
+ self.installAssist = False
928
+ self.installIoT = False
929
+ self.installMonitor = False
930
+ self.installManage = False
931
+ self.installPredict = False
932
+ self.installInspection = False
933
+ self.installOptimizer = False
934
+ self.installFacilities = False
935
+ self.deployCP4D = False
936
+ self.db2SetAffinity = False
937
+ self.db2SetTolerations = False
938
+ self.slsLicenseFileLocal = None
939
+
940
+ self.approvals = {
941
+ "approval_core": {"id": "suite-verify"}, # After Core Platform verification has completed
942
+ "approval_assist": {"id": "app-cfg-assist"}, # After Assist workspace has been configured
943
+ "approval_iot": {"id": "app-cfg-iot"}, # After IoT workspace has been configured
944
+ "approval_manage": {"id": "app-cfg-manage"}, # After Manage workspace has been configured
945
+ "approval_monitor": {"id": "app-cfg-monitor"}, # After Monitor workspace has been configured
946
+ "approval_optimizer": {"id": "app-cfg-optimizer"}, # After Optimizer workspace has been configured
947
+ "approval_predict": {"id": "app-cfg-predict"}, # After Predict workspace has been configured
948
+ "approval_visualinspection": {"id": "app-cfg-visualinspection"}, # After Visual Inspection workspace has been configured
949
+ "approval_facilities": {"id": "app-cfg-facilities"}, # After Facilities workspace has been configured 
950
+ }
951
+
952
+ self.configGrafana()
953
+ self.configSNO()
954
+ self.setDB2DefaultSettings()
955
+
956
+ for key, value in vars(self.args).items():
957
+ # These fields we just pass straight through to the parameters and fail if they are not set
958
+ if key in requiredParams:
959
+ if value is None:
960
+ self.fatalError(f"{key} must be set")
961
+ self.setParam(key, value)
962
+
963
+ # These fields we just pass straight through to the parameters
964
+ elif key in optionalParams:
965
+ if value is not None:
966
+ self.setParam(key, value)
967
+
968
+ elif key == "kafka_provider":
969
+ if value is not None:
970
+ self.setParam("kafka_provider", value)
971
+ self.setParam("kafka_action_system", "install")
972
+
973
+ elif key == "kafka_username":
974
+ if value is not None:
975
+ self.setParam("kafka_user_name", value)
976
+ self.setParam("aws_kafka_user_name", value)
977
+
978
+ elif key == "kafka_password":
979
+ if value is not None:
980
+ self.setParam("kafka_user_password", value)
981
+ self.setParam("aws_kafka_user_password", value)
982
+
983
+ elif key == "non_prod":
984
+ if not value:
985
+ self.operationalMode = 1
986
+ else:
987
+ self.operationalMode = 2
988
+ self.setParam("mas_annotations", "mas.ibm.com/operationalMode=nonproduction")
989
+
990
+ elif key == "additional_configs":
991
+ self.localConfigDir = value
992
+ # If there is a file named mongodb-system.yaml we will use this as a BYO MongoDB datasource
993
+ if self.localConfigDir is not None and path.exists(path.join(self.localConfigDir, "mongodb-system.yaml")):
994
+ self.setParam("mongodb_action", "byo")
995
+ self.setParam("sls_mongodb_cfg_file", "/workspace/additional-configs/mongodb-system.yaml")
996
+
997
+ elif key == "pod_templates":
998
+ # For the named configurations we will convert into the path
999
+ if value in ["best-effort", "guaranteed"]:
1000
+ self.setParam("mas_pod_templates_dir", path.join(self.templatesDir, "pod-templates", value))
1001
+ else:
1002
+ self.setParam("mas_pod_templates_dir", value)
1003
+
1004
+ # We check for both None and "" values for the application channel parameters
1005
+ # value = None means the parameter wasn't set at all
1006
+ # value = "" means the paramerter was explicitly set to "don't install this application"
1007
+ elif key == "assist_channel":
1008
+ if value is not None and value != "":
1009
+ self.setParam("mas_app_channel_assist", value)
1010
+ self.installAssist = True
1011
+ elif key == "iot_channel":
1012
+ if value is not None and value != "":
1013
+ self.setParam("mas_app_channel_iot", value)
1014
+ self.installIoT = True
1015
+ elif key == "monitor_channel":
1016
+ if value is not None and value != "":
1017
+ self.setParam("mas_app_channel_monitor", value)
1018
+ self.installMonitor = True
1019
+ elif key == "manage_channel":
1020
+ if value is not None and value != "":
1021
+ self.setParam("mas_app_channel_manage", value)
1022
+ self.installManage = True
1023
+ elif key == "predict_channel":
1024
+ if value is not None and value != "":
1025
+ self.setParam("mas_app_channel_predict", value)
1026
+ self.installPredict = True
1027
+ self.deployCP4D = True
1028
+ elif key == "visualinspection_channel":
1029
+ if value is not None and value != "":
1030
+ self.setParam("mas_app_channel_visualinspection", value)
1031
+ self.installInspection = True
1032
+ elif key == "optimizer_channel":
1033
+ if value is not None and value != "":
1034
+ self.setParam("mas_app_channel_optimizer", value)
1035
+ self.installOptimizer = True
1036
+ elif key == "optimizer_plan":
1037
+ if value is not None and value != "":
1038
+ self.setParam("mas_app_plan_optimizer", value)
1039
+ elif key == "facilities_channel":
1040
+ if value is not None and value != "":
1041
+ self.setParam("mas_app_channel_facilities", value)
1042
+ self.installFacilities = True
1043
+
1044
+ # Manage advanced settings that need extra processing
1045
+ elif key == "mas_app_settings_server_bundle_size":
1046
+ if value is not None:
1047
+ self.setParam(key, value)
1048
+ if value in ["jms", "snojms"]:
1049
+ self.setParam("mas_app_settings_persistent_volumes_flag", "true")
1050
+
1051
+ # MongoDB
1052
+ elif key == "mongodb_namespace":
1053
+ if value is not None and value != "":
1054
+ self.setParam(key, value)
1055
+ self.setParam("sls_mongodb_cfg_file", f"/workspace/configs/mongo-{value}.yml")
1056
+
1057
+ # SLS
1058
+ elif key == "license_file":
1059
+ if value is not None and value != "":
1060
+ self.slsLicenseFileLocal = value
1061
+ self.setParam("sls_action", "install")
1062
+ elif key == "dedicated_sls":
1063
+ if value:
1064
+ self.setParam("sls_namespace", f"mas-{self.args.mas_instance_id}-sls")
1065
+
1066
+ # These settings are used by the CLI rather than passed to the PipelineRun
1067
+ elif key == "storage_accessmode":
1068
+ if value is None:
1069
+ self.fatalError(f"{key} must be set")
1070
+ self.pipelineStorageAccessMode = value
1071
+ elif key == "storage_pipeline":
1072
+ if value is None:
1073
+ self.fatalError(f"{key} must be set")
1074
+ self.pipelineStorageClass = value
1075
+
1076
+ elif key.startswith("approval_"):
1077
+ if key not in self.approvals:
1078
+ raise KeyError(f"{key} is not a supported approval workflow ID: {self.approvals.keys()}")
1079
+
1080
+ if value != "":
1081
+ valueParts = value.split(":")
1082
+ if len(valueParts) != 3:
1083
+ self.fatalError(f"Unsupported format for {key} ({value}). Expected MAX_RETRIES:RETRY_DELAY:IGNORE_FAILURE")
1084
+ else:
1085
+ try:
1086
+ self.approvals[key]["maxRetries"] = int(valueParts[0])
1087
+ self.approvals[key]["retryDelay"] = int(valueParts[1])
1088
+ self.approvals[key]["ignoreFailure"] = bool(valueParts[2])
1089
+ except ValueError:
1090
+ self.fatalError(f"Unsupported format for {key} ({value}). Expected int:int:boolean")
1091
+
1092
+ # Arguments that we don't need to do anything with
1093
+ elif key in ["accept_license", "dev_mode", "skip_pre_check", "skip_grafana_install", "no_confirm", "no_wait_for_pvc", "help", "advanced", "simplified"]:
1094
+ pass
1095
+
1096
+ elif key == "manual_certificates":
1097
+ if value is not None:
1098
+ self.setParam("mas_manual_cert_mgmt", True)
1099
+ self.manualCertsDir = value
1100
+ else:
1101
+ self.setParam("mas_manual_cert_mgmt", False)
1102
+ self.manualCertsDir = None
1103
+
1104
+ elif key == "enable_ipv6":
1105
+ self.setParam("enable_ipv6", True)
1106
+
1107
+ # Fail if there's any arguments we don't know how to handle
1108
+ else:
1109
+ print(f"Unknown option: {key} {value}")
1110
+ self.fatalError(f"Unknown option: {key} {value}")
1111
+
1112
+ if self.installManage:
1113
+
1114
+ # Configure Storage and Access mode
1115
+ self.manageStorageAndAccessMode()
1116
+
1117
+ if self.installFacilities:
1118
+ # Verifiy if any of the props that needs to be in a file are given
1119
+ 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"):
1120
+ self.selectLocalConfigDir()
1121
+ facilitiesConfigsPath = path.join(self.localConfigDir, "facilities-configs.yaml")
1122
+ self.generateFacilitiesCfg(destination=facilitiesConfigsPath)
1123
+ self.setParam("mas_ws_facilities_config_map_name", "facilities-config")
1124
+
1125
+ # Load the catalog information
1126
+ self.chosenCatalog = getCatalog(self.getParam("mas_catalog_version"))
1127
+
1128
+ # License file is only optional for existing SLS instance
1129
+ if self.slsLicenseFileLocal is None:
1130
+ if findSLSByNamespace(self.getParam("sls_namespace"), dynClient=self.dynamicClient):
1131
+ self.setParam("sls_action", "gencfg")
1132
+ else:
1133
+ self.fatalError("--license-file must be set for new SLS install")
1134
+
1135
+ # Once we've processed the inputs, we should validate the catalog source & prompt to accept the license terms
1136
+ if not self.devMode:
1137
+ self.validateCatalogSource()
1138
+ self.licensePrompt()
1139
+
1140
+ # Version before 9.1 cannot have empty components
1141
+ if (self.getParam("mas_channel").startswith("8.") or self.getParam("mas_channel").startswith("9.0")) and self.getParam("mas_appws_components") == "":
1142
+ self.fatalError("--mas_appws_components must be set for versions earlier than 9.1.0")
1143
+
1144
+ # An error should be raised if "health" is not specified when installing Predict.
1145
+ 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")):
1146
+ self.fatalError("--mas_appws_components must include 'health' component when installing Predict")
1147
+
1148
+ @logMethodCall
1149
+ def install(self, argv):
1150
+ """
1151
+ Install MAS instance
1152
+ """
1153
+ args = installArgParser.parse_args(args=argv)
1154
+
1155
+ # We use the presence of --mas-instance-id to determine whether
1156
+ # the CLI is being started in interactive mode or not
1157
+ instanceId = args.mas_instance_id
1158
+
1159
+ # Properties for arguments that control the behavior of the CLI
1160
+ self.noConfirm = args.no_confirm
1161
+ self.waitForPVC = not args.no_wait_for_pvc
1162
+ self.licenseAccepted = args.accept_license
1163
+ self.devMode = args.dev_mode
1164
+ self.skipGrafanaInstall = args.skip_grafana_install
1165
+
1166
+ # Set image_pull_policy of the CLI in interactive mode
1167
+ if args.image_pull_policy and args.image_pull_policy != "":
1168
+ self.setParam("image_pull_policy", args.image_pull_policy)
1169
+
1170
+ self.approvals = {}
1171
+
1172
+ # Store all args
1173
+ self.args = args
1174
+
1175
+ # These flags work for setting params in both interactive and non-interactive modes
1176
+ if args.skip_pre_check:
1177
+ self.setParam("skip_pre_check", "true")
1178
+
1179
+ if instanceId is None:
1180
+ self.printH1("Set Target OpenShift Cluster")
1181
+ # Connect to the target cluster
1182
+ self.connect()
1183
+ else:
1184
+ logger.debug("MAS instance ID is set, so we assume already connected to the desired OCP")
1185
+ self.lookupTargetArchitecture()
1186
+
1187
+ if self.dynamicClient is None:
1188
+ print_formatted_text(HTML("<Red>Error: The Kubernetes dynamic Client is not available. See log file for details</Red>"))
1189
+ exit(1)
1190
+
1191
+ # 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
1192
+ # deprecated MaximoApplicationSuite ImageContentSourcePolicy instead of the new ImageDigestMirrorSet
1193
+ self.isAirgap()
1194
+
1195
+ # Configure the installOptions for the appropriate architecture
1196
+ self.catalogOptions = supportedCatalogs[self.architecture]
1197
+
1198
+ # Basic settings before the user provides any input
1199
+ self.configICR()
1200
+ self.configCertManager() # TODO: I think this is redundant, we should look to remove this and the appropriate params in the install pipeline
1201
+ self.deployCP4D = False
1202
+
1203
+ # UDS install has not been supported since the January 2024 catalog update
1204
+ self.setParam("uds_action", "install-dro")
1205
+
1206
+ # User must either provide the configuration via numerous command line arguments, or the interactive prompts
1207
+ if instanceId is None:
1208
+ self.interactiveMode(simplified=args.simplified, advanced=args.advanced)
1209
+ else:
1210
+ self.nonInteractiveMode()
1211
+
1212
+ # After we've configured the basic inputs, we can calculate these ones
1213
+ self.setIoTStorageClasses()
1214
+ if self.deployCP4D:
1215
+ self.configCP4D()
1216
+
1217
+ # Set up the secrets for additional configs, podtemplates, sls license file and manual certificates
1218
+ self.additionalConfigs()
1219
+ self.podTemplates()
1220
+ self.slsLicenseFile()
1221
+ self.manualCertificates()
1222
+
1223
+ if not self.noConfirm and not self.waitForPVC:
1224
+ self.printDescription(["If you are using storage classes that utilize 'WaitForFirstConsumer' binding mode choose 'No' at the prompt below"])
1225
+ self.waitForPVC = self.yesOrNo("Wait for PVCs to bind")
1226
+
1227
+ if not self.waitForPVC:
1228
+ self.setParam("no_wait_for_pvc", True)
1229
+
1230
+ # Show a summary of the installation configuration
1231
+ self.printH1("Non-Interactive Install Command")
1232
+ self.printDescription([
1233
+ "Save and re-use the following script to re-run this install without needing to answer the interactive prompts again",
1234
+ "",
1235
+ self.buildCommand()
1236
+ ])
1237
+
1238
+ # Based on the parameters set the annotations correctly
1239
+ self.configAnnotations()
1240
+
1241
+ self.displayInstallSummary()
1242
+
1243
+ if not self.noConfirm:
1244
+ print()
1245
+ self.printDescription([
1246
+ "Please carefully review your choices above, correcting mistakes now is much easier than after the install has begun"
1247
+ ])
1248
+ continueWithInstall = self.yesOrNo("Proceed with these settings")
1249
+
1250
+ # Prepare the namespace and launch the installation pipeline
1251
+ if self.noConfirm or continueWithInstall:
1252
+ self.createTektonFileWithDigest()
1253
+
1254
+ self.printH1("Launch Install")
1255
+ pipelinesNamespace = f"mas-{self.getParam('mas_instance_id')}-pipelines"
1256
+
1257
+ with Halo(text='Validating OpenShift Pipelines installation', spinner=self.spinner) as h:
1258
+ installOpenShiftPipelines(self.dynamicClient)
1259
+ h.stop_and_persist(symbol=self.successIcon, text="OpenShift Pipelines Operator is installed and ready to use")
1260
+
1261
+ with Halo(text=f'Preparing namespace ({pipelinesNamespace})', spinner=self.spinner) as h:
1262
+ createNamespace(self.dynamicClient, pipelinesNamespace)
1263
+ preparePipelinesNamespace(
1264
+ dynClient=self.dynamicClient,
1265
+ instanceId=self.getParam("mas_instance_id"),
1266
+ storageClass=self.pipelineStorageClass,
1267
+ accessMode=self.pipelineStorageAccessMode,
1268
+ waitForBind=self.waitForPVC,
1269
+ configureRBAC=(self.getParam("service_account_name") == "")
1270
+ )
1271
+ prepareInstallSecrets(
1272
+ dynClient=self.dynamicClient,
1273
+ namespace=pipelinesNamespace,
1274
+ slsLicenseFile=self.slsLicenseFileSecret,
1275
+ additionalConfigs=self.additionalConfigsSecret,
1276
+ podTemplates=self.podTemplatesSecret,
1277
+ certs=self.certsSecret
1278
+ )
1279
+
1280
+ self.setupApprovals(pipelinesNamespace)
1281
+
1282
+ h.stop_and_persist(symbol=self.successIcon, text=f"Namespace is ready ({pipelinesNamespace})")
1283
+
1284
+ with Halo(text='Testing availability of MAS CLI image in cluster', spinner=self.spinner) as h:
1285
+ testCLI()
1286
+ h.stop_and_persist(symbol=self.successIcon, text="MAS CLI image deployment test completed")
1287
+
1288
+ with Halo(text=f'Installing latest Tekton definitions (v{self.version})', spinner=self.spinner) as h:
1289
+ updateTektonDefinitions(pipelinesNamespace, self.tektonDefsPath)
1290
+ h.stop_and_persist(symbol=self.successIcon, text=f"Latest Tekton definitions are installed (v{self.version})")
1291
+
1292
+ with Halo(text=f"Submitting PipelineRun for {self.getParam('mas_instance_id')} install", spinner=self.spinner) as h:
1293
+ pipelineURL = launchInstallPipeline(dynClient=self.dynamicClient, params=self.params)
1294
+ if pipelineURL is not None:
1295
+ h.stop_and_persist(symbol=self.successIcon, text=f"PipelineRun for {self.getParam('mas_instance_id')} install submitted")
1296
+ print_formatted_text(HTML(f"\nView progress:\n <Cyan><u>{pipelineURL}</u></Cyan>\n"))
1297
+ else:
1298
+ 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")
1299
+ print()
1300
+
1301
+ @logMethodCall
1302
+ def setupApprovals(self, namespace: str) -> None:
1303
+ """
1304
+ Ensure the supported approval configmaps are in the expected state for the start of the run:
1305
+ - not present (if approval is not required)
1306
+ - present with the chosen state field initialized to ""
1307
+ """
1308
+ for approval in self.approvals.values():
1309
+ if "maxRetries" in approval:
1310
+ # Enable this approval workload
1311
+ logger.debug(f"Approval workflow for {approval['id']} will be enabled during install ({approval['maxRetries']} / {approval['retryDelay']}s / {approval['ignoreFailure']})")
1312
+ self.initializeApprovalConfigMap(namespace, approval['id'], True, approval['maxRetries'], approval['retryDelay'], approval['ignoreFailure'])
1313
+ else:
1314
+ # Disable this approval workload
1315
+ logger.debug(f"Approval workflow for {approval['id']} will be disabled during install")
1316
+ self.initializeApprovalConfigMap(namespace, approval['id'], False)