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