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.
- mas/cli/__init__.py +11 -0
- mas/cli/aiservice/install/__init__.py +11 -0
- mas/cli/aiservice/install/app.py +894 -0
- mas/cli/aiservice/install/argBuilder.py +180 -0
- mas/cli/aiservice/install/argParser.py +507 -0
- mas/cli/aiservice/install/params.py +100 -0
- mas/cli/aiservice/install/summarizer.py +134 -0
- mas/cli/cli.py +432 -0
- mas/cli/displayMixins.py +132 -0
- mas/cli/gencfg.py +113 -0
- mas/cli/install/__init__.py +11 -0
- mas/cli/install/app.py +1316 -0
- mas/cli/install/argBuilder.py +465 -0
- mas/cli/install/argParser.py +1176 -0
- mas/cli/install/catalogs.py +27 -0
- mas/cli/install/params.py +172 -0
- mas/cli/install/settings/__init__.py +23 -0
- mas/cli/install/settings/additionalConfigs.py +227 -0
- mas/cli/install/settings/db2Settings.py +252 -0
- mas/cli/install/settings/kafkaSettings.py +103 -0
- mas/cli/install/settings/manageSettings.py +273 -0
- mas/cli/install/settings/mongodbSettings.py +46 -0
- mas/cli/install/settings/turbonomicSettings.py +29 -0
- mas/cli/install/summarizer.py +398 -0
- mas/cli/templates/facilities-configs.yml.j2 +25 -0
- mas/cli/templates/ibm-mas-tekton.yaml +49772 -0
- mas/cli/templates/jdbccfg.yml.j2 +52 -0
- mas/cli/templates/pod-templates/best-effort/ibm-data-dictionary-assetdatadictionary.yml +26 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-bascfg.yml +56 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-coreidp.yml +21 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-actions.yml +28 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-auth.yml +32 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-datapower.yml +12 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-devops.yml +14 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-dm.yml +22 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-dsc.yml +40 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-edgeconfig.yml +10 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-fpl.yml +24 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-guardian.yml +20 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-iot.yml +10 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-mbgx.yml +18 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-mfgx.yml +14 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-monitor.yml +18 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-orgmgmt.yml +48 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-provision.yml +28 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-registry.yml +26 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-state.yml +40 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-iot-webui.yml +22 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-manage-healthextaccelerator.yml +13 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-manage-healthextworkspace.yml +10 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-manage-imagestitching.yml +10 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-manage-manageaccelerators.yml +10 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-manage-manageapp.yml +46 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-manage-manageworkspace.yml +48 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-manage-slackproxy.yml +10 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-pushnotificationcfg.yml +13 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-scimcfg.yml +14 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-slscfg.yml +10 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-smtpcfg.yml +10 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-suite.yml +136 -0
- mas/cli/templates/pod-templates/best-effort/ibm-mas-visualinspection.yml +34 -0
- mas/cli/templates/pod-templates/best-effort/ibm-sls-licenseservice.yml +10 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-data-dictionary-assetdatadictionary.yml +56 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-bascfg.yml +140 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-coreidp.yml +45 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-actions.yml +70 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-auth.yml +80 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-datapower.yml +24 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-devops.yml +26 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-dm.yml +52 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-dsc.yml +106 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-edgeconfig.yml +16 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-fpl.yml +62 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-guardian.yml +44 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-iot.yml +16 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-mbgx.yml +42 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-mfgx.yml +32 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-monitor.yml +42 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-orgmgmt.yml +126 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-provision.yml +70 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-registry.yml +62 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-state.yml +106 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-iot-webui.yml +52 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-manage-healthextaccelerator.yml +28 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-manage-healthextworkspace.yml +18 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-manage-imagestitching.yml +16 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-manage-manageaccelerators.yml +16 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-manage-manageapp.yml +106 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-manage-manageworkspace.yml +126 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-manage-slackproxy.yml +16 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-pushnotificationcfg.yml +25 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-scimcfg.yml +26 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-slscfg.yml +16 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-smtpcfg.yml +16 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-suite.yml +340 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-mas-visualinspection.yml +76 -0
- mas/cli/templates/pod-templates/guaranteed/ibm-sls-licenseservice.yml +16 -0
- mas/cli/templates/suite_mongocfg.yml.j2 +55 -0
- mas/cli/uninstall/__init__.py +11 -0
- mas/cli/uninstall/app.py +197 -0
- mas/cli/uninstall/argParser.py +115 -0
- mas/cli/update/__init__.py +11 -0
- mas/cli/update/app.py +673 -0
- mas/cli/update/argParser.py +156 -0
- mas/cli/upgrade/__init__.py +11 -0
- mas/cli/upgrade/app.py +164 -0
- mas/cli/upgrade/argParser.py +68 -0
- mas/cli/upgrade/settings/__init__.py +19 -0
- mas/cli/validators.py +151 -0
- mas_cli-5.1.4.data/scripts/mas-cli +87 -0
- mas_cli-5.1.4.dist-info/METADATA +73 -0
- mas_cli-5.1.4.dist-info/RECORD +114 -0
- mas_cli-5.1.4.dist-info/WHEEL +5 -0
- 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)
|