Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions image/cli/app-root/src/.bashrc
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ if [ $arch != "s390x" ] && [ $arch != "ppc64le" ]; then
echo " - ${TEXT_BOLD}${COLOR_GREEN}mas provision-fyre${TEXT_RESET} to provision an OCP cluster on IBM DevIT Fyre (internal)"
echo "AI Service (Standalone) Management:"
echo " - ${TEXT_BOLD}${COLOR_GREEN}mas aiservice-install${TEXT_RESET} to install a new AI Service instance"
echo " - ${TEXT_BOLD}${COLOR_GREEN}mas aiservice-upgrade${TEXT_RESET} to upgrade a existing AI Service instance"
echo
fi
10 changes: 10 additions & 0 deletions image/cli/mascli/mas
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,16 @@ case $1 in
mas-cli aiservice-install "$@"
;;

aiservice-upgrade)
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >> $LOGFILE
echo "!! aiservice-upgrade !!" >> $LOGFILE
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >> $LOGFILE
# Take the first parameter off (it will be "aiservice-upgrade")
shift
# Run the new Python-based aiservice-upgrade
mas-cli aiservice-upgrade "$@"
;;

update)
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >> $LOGFILE
echo "!! update !!" >> $LOGFILE
Expand Down
4 changes: 4 additions & 0 deletions python/src/mas-cli
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ from sys import argv
from mas.cli import __version__ as VERSION
from mas.cli.install.app import InstallApp
from mas.cli.aiservice.install.app import AiServiceInstallApp
from mas.cli.aiservice.upgrade.app import AiServiceUpgradeApp
from mas.cli.update.app import UpdateApp
from mas.cli.upgrade.app import UpgradeApp
from mas.cli.uninstall.app import UninstallApp
Expand Down Expand Up @@ -58,6 +59,9 @@ if __name__ == '__main__':
elif function == "aiservice-install":
app = AiServiceInstallApp()
app.install(argv[2:])
elif function == "aiservice-upgrade":
app = AiServiceUpgradeApp()
app.upgrade(argv[2:])
elif function == "uninstall":
app = UninstallApp()
app.uninstall(argv[2:])
Expand Down
11 changes: 11 additions & 0 deletions python/src/mas/cli/aiservice/upgrade/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# *****************************************************************************
# Copyright (c) 2024, 2025 IBM Corporation and other Contributors.
#
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the Eclipse Public License v1.0
# which accompanies this distribution, and is available at
# http://www.eclipse.org/legal/epl-v10.html
#
# *****************************************************************************

from ...cli import BaseApp # noqa: F401
136 changes: 136 additions & 0 deletions python/src/mas/cli/aiservice/upgrade/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env python
# *****************************************************************************
# Copyright (c) 2024, 2025 IBM Corporation and other Contributors.
#
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the Eclipse Public License v1.0
# which accompanies this distribution, and is available at
# http://www.eclipse.org/legal/epl-v10.html
#
# *****************************************************************************

import sys
import logging
import logging.handlers
from prompt_toolkit import prompt, print_formatted_text, HTML
from prompt_toolkit.completion import WordCompleter

from halo import Halo

from ...cli import BaseApp
from ...validators import AiserviceInstanceIDValidator
from .argParser import upgradeArgParser

from mas.devops.ocp import createNamespace
from mas.devops.mas import listAiServiceInstances, getAiserviceChannel
from mas.devops.tekton import installOpenShiftPipelines, updateTektonDefinitions, launchAiServiceUpgradePipeline
from openshift.dynamic.exceptions import ResourceNotFoundError
logger = logging.getLogger(__name__)


class AiServiceUpgradeApp(BaseApp):
def upgrade(self, argv):
"""
Upgrade AI Service instance
"""
args = upgradeArgParser.parse_args(args=argv)
aiserviceInstanceId = args.aiservice_instance_id
self.noConfirm = args.no_confirm
self.skipPreCheck = args.skip_pre_check
self.licenseAccepted = args.accept_license
self.devMode = args.dev_mode

if aiserviceInstanceId is None:
self.printH1("Set Target OpenShift Cluster")
# Connect to the target cluster
self.connect()
else:
logger.debug("AI Service instance ID is set, so we assume already connected to the desired OCP")
# Need to lookup target architecture because configDb2 will try to access self.architecture
self.lookupTargetArchitecture()

if self.dynamicClient is None:
print_formatted_text(HTML("<Red>Error: The Kubernetes dynamic Client is not available. See log file for details</Red>"))
sys.exit(1)

if aiserviceInstanceId is None:
# Interactive mode
self.printH1("AI Service Instance Selection")
print_formatted_text(HTML("<LightSlateGrey>Select a AI Service instance to upgrade from the list below:</LightSlateGrey>"))
try:
aiserviceInstances = listAiServiceInstances(self.dynamicClient)
except ResourceNotFoundError:
aiserviceInstances = []
aiserviceOptions = []

if len(aiserviceInstances) == 0:
print_formatted_text(HTML("<Red>Error: No AI Service instances detected on this cluster</Red>"))
sys.exit(1)

for aiservice in aiserviceInstances:
print_formatted_text(HTML(f"- <u>{aiservice['metadata']['name']}</u> v{aiservice['status']['versions']['reconciled']}"))
aiserviceOptions.append(aiservice['metadata']['name'])

aiserviceCompleter = WordCompleter(aiserviceOptions)
print()
aiserviceInstanceId = prompt(HTML('<Yellow>Enter AI Service instance ID: </Yellow>'), completer=aiserviceCompleter, validator=AiserviceInstanceIDValidator(), validate_while_typing=False)

currentAiserviceChannel = getAiserviceChannel(self.dynamicClient, aiserviceInstanceId)
if currentAiserviceChannel is not None:
if self.devMode:
# this enables upgrade of custom channel for AI service
nextAiserviceChannel = prompt(HTML('<Yellow>Custom channel</Yellow> '))
else:
if currentAiserviceChannel not in self.upgrade_path:
self.fatalError(f"No upgrade available, {aiserviceInstanceId} is are already on the latest release {currentAiserviceChannel}")
nextAiserviceChannel = self.upgrade_path[currentAiserviceChannel]

if not self.licenseAccepted and not self.devMode:
self.printH1("License Terms")
self.printDescription([
"To continue with the upgrade, you must accept the license terms:",
self.licenses[nextAiserviceChannel]
])

if self.noConfirm:
self.fatalError("You must accept the license terms with --accept-license when using the --no-confirm flag")
else:
if not self.yesOrNo("Do you accept the license terms"):
exit(1)

self.printH1("Review Settings")
print_formatted_text(HTML(f"<LightSlateGrey>AI Service Instance ID ..................... {aiserviceInstanceId}</LightSlateGrey>"))
print_formatted_text(HTML(f"<LightSlateGrey>Current AI Service Channel ............. {currentAiserviceChannel}</LightSlateGrey>"))
print_formatted_text(HTML(f"<LightSlateGrey>Next AI Service Channel ................ {nextAiserviceChannel}</LightSlateGrey>"))
print_formatted_text(HTML(f"<LightSlateGrey>Skip Pre-Upgrade Checks ......... {self.skipPreCheck}</LightSlateGrey>"))

if not self.noConfirm:
print()
continueWithUpgrade = self.yesOrNo("Proceed with these settings")

if self.noConfirm or continueWithUpgrade:
self.createTektonFileWithDigest()

self.printH1("Launch Upgrade")
pipelinesNamespace = f"aiservice-{aiserviceInstanceId}-pipelines"

with Halo(text='Validating OpenShift Pipelines installation', spinner=self.spinner) as h:
installOpenShiftPipelines(self.dynamicClient)
h.stop_and_persist(symbol=self.successIcon, text="OpenShift Pipelines Operator is installed and ready to use")

with Halo(text=f'Preparing namespace ({pipelinesNamespace})', spinner=self.spinner) as h:
createNamespace(self.dynamicClient, pipelinesNamespace)
h.stop_and_persist(symbol=self.successIcon, text=f"Namespace is ready ({pipelinesNamespace})")

with Halo(text=f'Installing latest Tekton definitions (v{self.version})', spinner=self.spinner) as h:
updateTektonDefinitions(pipelinesNamespace, self.tektonDefsPath)
h.stop_and_persist(symbol=self.successIcon, text=f"Latest Tekton definitions are installed (v{self.version})")

with Halo(text='Submitting PipelineRun for {aiserviceInstanceId} upgrade', spinner=self.spinner) as h:
pipelineURL = launchAiServiceUpgradePipeline(self.dynamicClient, aiserviceInstanceId, self.skipPreCheck, aiserviceChannel=nextAiserviceChannel, params=self.params)
if pipelineURL is not None:
h.stop_and_persist(symbol=self.successIcon, text=f"PipelineRun for {aiserviceInstanceId} upgrade submitted")
print_formatted_text(HTML(f"\nView progress:\n <Cyan><u>{pipelineURL}</u></Cyan>\n"))
else:
h.stop_and_persist(symbol=self.failureIcon, text=f"Failed to submit PipelineRun for {aiserviceInstanceId} upgrade, see log file for details")
print()
69 changes: 69 additions & 0 deletions python/src/mas/cli/aiservice/upgrade/argParser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# *****************************************************************************
# Copyright (c) 2024, 2025 IBM Corporation and other Contributors.
#
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the Eclipse Public License v1.0
# which accompanies this distribution, and is available at
# http://www.eclipse.org/legal/epl-v10.html
#
# *****************************************************************************

import argparse

from ... import __version__ as packageVersion
from ...cli import getHelpFormatter

upgradeArgParser = argparse.ArgumentParser(
prog='mas aiservice-upgrade',
description="\n".join([
f"IBM Maximo Application Suite Admin CLI v{packageVersion}",
"Upgrade AI Service by configuring and launching the AI Service Upgrade Tekton Pipeline.\n",
"Interactive Mode:",
"Omitting the --aiservice-instance-id option will trigger an interactive prompt"
]),
epilog="Refer to the online documentation for more information: https://ibm-mas.github.io/cli/",
formatter_class=getHelpFormatter(),
add_help=False
)

masArgGroup = upgradeArgParser.add_argument_group('MAS Instance Selection')
masArgGroup.add_argument(
'--aiservice-instance-id',
required=False,
help="The AI Service Instance ID to be upgraded"
)

otherArgGroup = upgradeArgParser.add_argument_group('More')
otherArgGroup.add_argument(
'--skip-pre-check',
required=False,
action='store_true',
default=False,
help="Disable the 'pre-upgrade-check' and 'post-upgrade-verify' tasks in the upgrade pipeline"
)
otherArgGroup.add_argument(
'--no-confirm',
required=False,
action='store_true',
default=False,
help="Launch the upgrade without prompting for confirmation",
)
otherArgGroup.add_argument(
"--accept-license",
action="store_true",
default=False,
help="Accept all license terms without prompting"
)
otherArgGroup.add_argument(
"--dev-mode",
required=False,
action="store_true",
default=False,
help="Configure upgrade for development mode",
)
otherArgGroup.add_argument(
'-h', "--help",
action='help',
default=False,
help="Show this help message and exit",
)
16 changes: 15 additions & 1 deletion python/src/mas/cli/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from prompt_toolkit.validation import Validator, ValidationError

from mas.devops.ocp import getStorageClass
from mas.devops.mas import verifyMasInstance
from mas.devops.mas import verifyMasInstance, verifyAiServiceInstance

import logging

Expand Down Expand Up @@ -85,6 +85,20 @@ def validate(self, document):
raise ValidationError(message='Not a valid MAS instance ID on this cluster', cursor_position=len(instanceId))


class AiserviceInstanceIDValidator(Validator):
def validate(self, document):
"""
Validate that a AI Service instance ID exists on the target cluster
"""
instanceId = document.text

dynClient = dynamic.DynamicClient(
api_client.ApiClient(configuration=config.load_kube_config())
)
if not verifyAiServiceInstance(dynClient, instanceId):
raise ValidationError(message='Not a valid AI Service instance ID on this cluster', cursor_position=len(instanceId))


class StorageClassValidator(Validator):
def validate(self, document):
"""
Expand Down
1 change: 1 addition & 0 deletions tekton/generate-tekton-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
- mas-ivt-manage
# AI Service Pipelines
- aiservice-install
- aiservice-upgrade
# AI Service FVT Pipelines
- aiservice-fvt-launcher
- aiservice-fvt
Expand Down
5 changes: 3 additions & 2 deletions tekton/generate-tekton-tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@
- turbonomic
- uds

# 3. Generate Tasks (AI Broker)
# 3. Generate Tasks (AI Service)
# -------------------------------------------------------------------------
- name: Generate Tasks (AI Broker)
- name: Generate Tasks (AI Service)
ansible.builtin.template:
src: "{{ task_src_dir }}/aiservice/{{ item }}.yml.j2"
dest: "{{ task_target_dir }}/{{ item }}.yaml"
Expand All @@ -55,6 +55,7 @@
- odh
- aiservice-tenant
- aiservice-post-verify
- aiservice-upgrade

# 4. Generate Tasks (FVT)
# -------------------------------------------------------------------------
Expand Down
Loading