Skip to content

Commit 282ad91

Browse files
[minor] Add changes for installing AI Assistant and AI Service on same cluster (#2247)
1 parent 3124797 commit 282ad91

14 files changed

Lines changed: 469 additions & 2 deletions

File tree

.secrets.baseline

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"files": "build/bin/config/oscap/ssg-rhel9-ds.xml|^.secrets.baseline$|^docs/catalogs/",
44
"lines": null
55
},
6+
67
"generated_at": "2026-05-01T10:41:14Z",
78
"plugins_used": [
89
{

python/src/mas/cli/gencfg.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,52 @@ def generateJDBCCfg(self, instanceId: str, scope: str, destination: str, appId:
6060
f.write(cfg)
6161
f.write('\n')
6262

63+
def generateAiCfg(self, instanceId: str, scope: str, destination: str, workspaceId: str = "") -> None:
64+
templateFile = path.join(self.templatesDir, "aicfg.yml.j2")
65+
with open(templateFile) as tFile:
66+
template = Template(tFile.read())
67+
68+
if scope == "workspace":
69+
assert workspaceId != ""
70+
71+
name = self.promptForString("Configuration Display Name", default="AI Service Configuration")
72+
url = self.promptForString("AI Service URL")
73+
tenantId = self.promptForString("AI Service Tenant ID")
74+
apikey = self.promptForString("AI Service API Key", isPassword=True)
75+
76+
enabled = self.yesOrNo("Enable AI Service (set aiService.enabled to true)")
77+
aiAssistantEnabled = self.yesOrNo("Enable AI Assistant Agent (AI assistant for MAS)")
78+
sslEnabled = self.yesOrNo("Enable SSL Connection")
79+
80+
if sslEnabled:
81+
sslCertFile = self.promptForFile("Path to certificate file")
82+
with open(sslCertFile) as cFile:
83+
certLocalFileContent = cFile.read()
84+
else:
85+
certLocalFileContent = ""
86+
87+
cfg = template.render(
88+
scope=scope,
89+
90+
mas_instance_id=instanceId,
91+
mas_workspace_id=workspaceId,
92+
93+
cfg_display_name=name,
94+
95+
ai_url=url,
96+
ai_tenant_id=tenantId,
97+
ai_apikey=apikey,
98+
ai_enabled=enabled,
99+
ai_assistant_enabled=aiAssistantEnabled,
100+
101+
ai_ssl_enabled=sslEnabled,
102+
ai_cert_local_file_content=certLocalFileContent
103+
)
104+
105+
with open(destination, 'w') as f:
106+
f.write(cfg)
107+
f.write('\n')
108+
63109
def generateMongoCfg(self, instanceId: str, destination: str) -> None:
64110
templateFile = path.join(self.templatesDir, "suite_mongocfg.yml.j2")
65111

python/src/mas/cli/install/app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,6 +1719,7 @@ def interactiveMode(self, simplified: bool, advanced: bool) -> None:
17191719
self.configMongoDb()
17201720
self.configDb2()
17211721
self.configKafka() # Will only do anything if IoT has been selected for install
1722+
self.configAi() # Configure AI Service integration
17221723

17231724
self.configGrafana()
17241725

@@ -1877,6 +1878,9 @@ def nonInteractiveMode(self) -> None:
18771878
# Set manage - bind - AI Service params same as provided AI Service's params
18781879
self.setParam("manage_bind_aiservice_instance_id", vars(self.args).get("aiservice_instance_id", ""))
18791880
self.setParam("manage_bind_aiservice_tenant_id", "user")
1881+
elif key == "configure_aiassistant":
1882+
if value is not None and value != "":
1883+
self.setParam("configure_aiassistant", value)
18801884
elif key == "manage_bind_aiservice_instance_id":
18811885
# only set if AI Service not being installed
18821886
if not vars(self.args).get("aiservice_instance_id") and value is not None and value != "":

python/src/mas/cli/install/argParser.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str:
114114
required=False,
115115
help="AI Service Instance ID"
116116
)
117+
masArgGroup.add_argument(
118+
"--configure-ai-assistant",
119+
dest="configure_aiassistant",
120+
required=False,
121+
help="Configure AI Assistant in silent mode (for example: pipeline, configure, none)"
122+
)
117123
masArgGroup.add_argument(
118124
"--allow-special-chars",
119125
dest="mas_special_characters",

python/src/mas/cli/install/settings/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
from .kafkaSettings import KafkaSettingsMixin
1414
from .manageSettings import ManageSettingsMixin
1515
from .additionalConfigs import AdditionalConfigsMixin
16+
from .aiSettings import AiSettingsMixin
1617

1718

18-
class InstallSettingsMixin(Db2SettingsMixin, MongoDbSettingsMixin, KafkaSettingsMixin, ManageSettingsMixin, AdditionalConfigsMixin):
19+
class InstallSettingsMixin(Db2SettingsMixin, MongoDbSettingsMixin, KafkaSettingsMixin, ManageSettingsMixin, AdditionalConfigsMixin, AiSettingsMixin):
1920
"""
2021
This class collects all the Mixins providing interactive prompts for mas-install
2122
"""
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# *****************************************************************************
2+
# Copyright (c) 2024, 2026 IBM Corporation and other Contributors.
3+
#
4+
# All rights reserved. This program and the accompanying materials
5+
# are made available under the terms of the Eclipse Public License v1.0
6+
# which accompanies this distribution, and is available at
7+
# http://www.eclipse.org/legal/epl-v10.html
8+
#
9+
# *****************************************************************************
10+
11+
from os import path
12+
from typing import TYPE_CHECKING, Dict, List, Any
13+
from prompt_toolkit import print_formatted_text
14+
15+
16+
if TYPE_CHECKING:
17+
# Type hints for methods and attributes provided by other mixins
18+
# These are only used during type checking and have no runtime cost
19+
from prompt_toolkit.completion import WordCompleter
20+
from prompt_toolkit.validation import Validator
21+
22+
23+
class AiSettingsMixin():
24+
if TYPE_CHECKING:
25+
# Attributes from BaseApp and other mixins
26+
params: Dict[str, str]
27+
devMode: bool
28+
showAdvancedOptions: bool
29+
localConfigDir: str | None
30+
templatesDir: str
31+
dynamicClient: Any
32+
installAIService: bool
33+
34+
# Methods from BaseApp
35+
def setParam(self, param: str, value: str) -> None:
36+
...
37+
38+
def getParam(self, param: str) -> str:
39+
...
40+
41+
# Methods from PrintMixin
42+
def printH1(self, message: str) -> None:
43+
...
44+
45+
def printH2(self, message: str) -> None:
46+
...
47+
48+
def printDescription(self, content: List[str]) -> None:
49+
...
50+
51+
# Methods from PromptMixin
52+
def yesOrNo(self, message: str, param: str | None = None) -> bool:
53+
...
54+
55+
def promptForString(
56+
self,
57+
message: str,
58+
param: str | None = None,
59+
default: str = "",
60+
isPassword: bool = False,
61+
validator: Validator | None = None,
62+
completer: WordCompleter | None = None
63+
) -> str:
64+
...
65+
66+
def promptForListSelect(
67+
self,
68+
message: str,
69+
options: List[str],
70+
param: str | None = None,
71+
default: int | None = None
72+
) -> str:
73+
...
74+
75+
# Methods from ConfigGeneratorMixin or InstallSettingsMixin
76+
def selectLocalConfigDir(self) -> None:
77+
...
78+
79+
def generateAiCfg(self, instanceId: str, scope: str, destination: str, workspaceId: str = "") -> None:
80+
...
81+
82+
def configAi(self, silentMode=False) -> None:
83+
"""Configure AiCfg for MAS installation"""
84+
if not silentMode:
85+
self.printH1("Configure AiCfg")
86+
self.printDescription([
87+
"The installer can configure AiCfg integration for your MAS instance.",
88+
"AiCfg provides AI/ML capabilities for MAS applications like Manage, Monitor, and Predict.",
89+
"AiCfg is configured at system scope and available to all workspaces."
90+
])
91+
92+
# Ask if user wants to configure AiCfg
93+
if not silentMode:
94+
# Interactive mode - ask user
95+
configureAi = self.yesOrNo("Do you want to configure AiCfg")
96+
else:
97+
# Silent mode - check if explicitly requested via parameter
98+
# Default to False (skip) unless parameter says otherwise
99+
configureAi = self.getParam("configure_aiassistant") not in [None, "none", ""]
100+
101+
if not configureAi:
102+
self.setParam("configure_aiassistant", "none")
103+
print_formatted_text("AiCfg configuration skipped")
104+
return
105+
106+
instanceId = self.getParam('mas_instance_id')
107+
workspaceId = self.getParam('mas_workspace_id')
108+
109+
# AiCfg is always configured at system scope
110+
scope = "system"
111+
self.setParam("ai_scope", "system")
112+
113+
# Check if AI Service is being installed on the same cluster
114+
if hasattr(self, 'installAIService') and self.installAIService:
115+
# AI Service will be installed - defer AiCfg generation to pipeline
116+
if not silentMode:
117+
self.printH2("AiCfg Configuration (Automatic)")
118+
self.printDescription([
119+
"AI Service is being installed on this cluster.",
120+
"The AiCfg will be automatically generated and applied by the pipeline",
121+
"AFTER AI Service installation completes.",
122+
"",
123+
"The pipeline will:",
124+
" 1. Install AI Service first",
125+
" 2. Auto-detect connection details (URL, API key, certificate)",
126+
" 3. Generate and apply AiCfg automatically",
127+
"",
128+
"No manual configuration needed!"
129+
])
130+
131+
# Set action to indicate pipeline should handle it
132+
self.setParam("configure_aiassistant", "pipeline")
133+
print_formatted_text("\n✓ AiCfg will be automatically configured by the pipeline after AI Service installation")
134+
else:
135+
# Manual configuration for external AI Service
136+
if not silentMode:
137+
self.printH2("AiCfg Configuration")
138+
self.printDescription([
139+
"You can provide connection details for an existing AI Service instance.",
140+
"The installer will generate the AiCfg YAML file with your connection details.",
141+
"",
142+
"IMPORTANT: The AiCfg file must be applied AFTER the MAS Core operator is installed,",
143+
"as the AiCfg CRD is created by the operator (not during initial config phase).",
144+
"Do NOT include this file in the initial configuration directory.",
145+
"Apply it after the operator creates the CRD."
146+
])
147+
148+
createAiConfig = True
149+
if not silentMode:
150+
createAiConfig = self.yesOrNo("Generate AiCfg configuration file (apply after operator install)")
151+
152+
if createAiConfig:
153+
self.setParam("configure_aiassistant", "configure")
154+
155+
self.selectLocalConfigDir()
156+
157+
# Check if a configuration already exists before creating a new one
158+
assert self.localConfigDir is not None, "localConfigDir must be set"
159+
160+
if scope == "system":
161+
aiCfgFile = path.join(self.localConfigDir, f"aicfg-{instanceId}-system.yaml")
162+
print_formatted_text(f"Searching for AiCfg configuration file in {aiCfgFile} ...")
163+
else:
164+
aiCfgFile = path.join(self.localConfigDir, f"aicfg-{instanceId}-{workspaceId}.yaml")
165+
print_formatted_text(f"Searching for AiCfg configuration file in {aiCfgFile} ...")
166+
167+
if path.exists(aiCfgFile):
168+
if self.yesOrNo("AiCfg configuration file already exists. Do you want to generate a new one"):
169+
self.generateAiCfg(instanceId=instanceId, scope=scope, destination=aiCfgFile, workspaceId=workspaceId)
170+
else:
171+
print_formatted_text(f"Expected file ({aiCfgFile}) was not found, generating a valid AiCfg configuration file now ...")
172+
self.generateAiCfg(instanceId=instanceId, scope=scope, destination=aiCfgFile, workspaceId=workspaceId)
173+
174+
print_formatted_text(f"\nAiCfg configuration file created: {aiCfgFile}")
175+
print_formatted_text("This configuration will be applied during MAS installation.")
176+
else:
177+
self.setParam("configure_aiassistant", "none")
178+
print_formatted_text("AiCfg configuration skipped")
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
apiVersion: v1
3+
kind: Secret
4+
type: Opaque
5+
metadata:
6+
name: "{{ mas_instance_id }}-usersupplied-ai-creds-system"
7+
namespace: "mas-{{ mas_instance_id }}-core"
8+
stringData:
9+
api_key: "{{ ai_apikey }}" # pragma: allowlist secret
10+
---
11+
apiVersion: config.mas.ibm.com/v1
12+
kind: AiCfg
13+
metadata:
14+
name: "{{ mas_instance_id }}-ai-system"
15+
namespace: "mas-{{ mas_instance_id }}-core"
16+
labels:
17+
mas.ibm.com/configScope: system
18+
mas.ibm.com/instanceId: {{ mas_instance_id }}
19+
mas.ibm.com/workspaceId: {{ mas_workspace_id }}
20+
mas.ibm.com/configId: ai-system
21+
spec:
22+
displayName: "{{ cfg_display_name }}"
23+
config:
24+
aiservice:
25+
enabled: {{ ai_enabled | lower }}
26+
url: "{{ ai_url }}"
27+
tenantId: "{{ ai_tenant_id }}"
28+
sslEnabled: {{ ai_ssl_enabled | lower }}
29+
credentials:
30+
secretName: "{{ mas_instance_id }}-usersupplied-ai-creds-system"
31+
assistantAgent:
32+
enabled: {{ ai_assistant_enabled | lower }}
33+
{%- if ai_ssl_enabled == True %}
34+
certificates:
35+
- alias: aiservice
36+
crt: |-
37+
{{ ai_cert_local_file_content | indent(8) }}
38+
{%- endif %}

python/test/install/test_dev_mode.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ def test_install_master_dev_mode(tmpdir):
109109
# 14. Kafka configuration
110110
'.*Create system Kafka instance.*': lambda msg: 'y',
111111
'.*Kafka version.*': lambda msg: '3.8.0',
112+
# 14. AiCfg configuration
113+
'.*Do you want to configure AiCfg.*': lambda msg: 'n',
112114
# 15. Final confirmation
113115
'.*Use additional configurations.*': lambda msg: 'n',
114116
".*Proceed with these settings.*": lambda msg: 'y',
@@ -191,6 +193,8 @@ def test_install_master_dev_mode_existing_catalog(tmpdir):
191193
# 14. Kafka configuration
192194
'.*Create system Kafka instance.*': lambda msg: 'y',
193195
'.*Kafka version.*': lambda msg: '3.8.0',
196+
# 14. AiCfg configuration
197+
'.*Do you want to configure AiCfg.*': lambda msg: 'n',
194198
# 15. Final confirmation
195199
'.*Use additional configurations.*': lambda msg: 'n',
196200
".*Proceed with these settings.*": lambda msg: 'y',
@@ -339,6 +343,8 @@ def test_install_master_dev_mode_with_path_routing(tmpdir):
339343
# 26. Kafka configuration
340344
'.*Create system Kafka instance.*': lambda msg: 'y',
341345
'.*Kafka version.*': lambda msg: '3.8.0',
346+
# 24. AiCfg configuration
347+
'.*Do you want to configure AiCfg.*': lambda msg: 'n',
342348
# 27. Final confirmation
343349
'.*Use additional configurations.*': lambda msg: 'n',
344350
".*Proceed with these settings.*": lambda msg: 'y',

python/test/install/test_existing_catalog.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ def test_install_interactive_existing_catalog(tmpdir):
6767
'.*Create MongoDb cluster.*': lambda msg: 'y',
6868
# 14. Db2 configuration
6969
'.*Create Manage dedicated Db2 instance.*': lambda msg: 'y',
70+
# 14. AiCfg configuration
71+
'.*Do you want to configure AiCfg.*': lambda msg: 'n',
7072
# 15. Final confirmation
7173
'.*Use additional configurations.*': lambda msg: 'n',
7274
".*Proceed with these settings.*": lambda msg: 'y',

python/test/install/test_no_catalog.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ def test_install_interactive_no_catalog(tmpdir):
7272
# 14. Kafka configuration
7373
'.*Create system Kafka instance.*': lambda msg: 'y',
7474
'.*Kafka version.*': lambda msg: '3.8.0',
75-
# 15. Final confirmation
75+
# 15. AiCfg configuration
76+
'.*Do you want to configure AiCfg.*': lambda msg: 'n',
77+
# 16. Final confirmation
7678
'.*Use additional configurations.*': lambda msg: 'n',
7779
".*Proceed with these settings.*": lambda msg: 'y',
7880
}

0 commit comments

Comments
 (0)