Skip to content

Commit 2bdf1ce

Browse files
Merge branch 'stable' into mascore-13212
2 parents 5aa580d + 94448bb commit 2bdf1ce

16 files changed

Lines changed: 1777 additions & 318 deletions

.gitignore

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ __pycache__/
66
# Distribution / packaging
77
dist
88
*.egg-info
9-
pandoc-*-amd64.deb
10-
pandoc-*-windows-x86_64.msi
11-
pandoc-*-x86_64-macOS.pkg
12-
README.rst
139

1410
# Environments
1511
.venv/
@@ -19,5 +15,5 @@ venv/
1915
# Other
2016
kubectl.exe
2117
/build
22-
/.vscode
23-
/site
18+
/.vscode
19+
/site

bin/mas-devops-notify-slack

Lines changed: 306 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,300 @@ def notifyProvisionRoks(channels: list[str], rc: int, additionalMsg: str | None
9797
return response.data.get("ok", False)
9898

9999

100+
def notifyPipelineStart(channels: list[str], instanceId: str | None = None, pipelineName: str | None = None) -> dict | None:
101+
"""Send Slack notification about pipeline start and create thread for all channels."""
102+
# Exit early if no channels provided
103+
if not channels or len(channels) == 0:
104+
print("No Slack channels provided - skipping pipeline start notification")
105+
return None
106+
107+
# For update pipeline, use mas-pipelines namespace (no instance ID)
108+
# For install/upgrade pipelines, use mas-{instanceId}-pipelines namespace
109+
if instanceId is None or instanceId == "":
110+
namespace = "mas-pipelines"
111+
else:
112+
namespace = f"mas-{instanceId}-pipelines"
113+
114+
# Check if thread already exists
115+
threadInfo = SlackUtil.getThreadConfigMap(namespace, instanceId, pipelineName)
116+
if threadInfo is not None:
117+
print("Pipeline start notification already sent")
118+
return threadInfo
119+
120+
# Send pipeline started message to all channels
121+
toolchainLink = _getToolchainLink()
122+
instanceInfo = f"Instance ID: `{instanceId}`" if instanceId else ""
123+
message = [
124+
SlackUtil.buildHeader(f"🚀 MAS {pipelineName} Pipeline Started"),
125+
SlackUtil.buildSection(f"Pipeline Run: {pipelineName}\n{instanceInfo}\n{toolchainLink}")
126+
]
127+
response = SlackUtil.postMessageBlocks(channels, message)
128+
129+
# Store thread information for all channels in ConfigMap
130+
configMapData = {"instanceId": instanceId, "pipelineName": pipelineName}
131+
132+
if isinstance(response, list):
133+
# Multiple channels - store each channel's thread info
134+
for idx, res in enumerate(response):
135+
if res.data.get("ok", False):
136+
threadId = res["ts"]
137+
channelId = res["channel"]
138+
# Store with channel-specific keys
139+
configMapData[f"channel_{idx}"] = channelId
140+
configMapData[f"threadId_{idx}"] = threadId
141+
configMapData["channel_count"] = str(len(response))
142+
else:
143+
# Single channel
144+
if response.data.get("ok", False):
145+
threadId = response["ts"]
146+
channelId = response["channel"]
147+
configMapData["channel_0"] = channelId
148+
configMapData["threadId_0"] = threadId
149+
configMapData["channel_count"] = "1"
150+
else:
151+
print("Failed to send pipeline start Slack message")
152+
return False
153+
154+
# Create ConfigMap with all channel/thread info
155+
SlackUtil.createThreadConfigMap(namespace, instanceId, pipelineName)
156+
SlackUtil.updateThreadConfigMap(namespace, instanceId, configMapData, pipelineName)
157+
return SlackUtil.getThreadConfigMap(namespace, instanceId, pipelineName)
158+
159+
160+
def notifyAnsibleStart(channels: list[str], taskName: str, instanceId: str | None = None, pipelineName: str | None = None) -> bool:
161+
"""Send Slack notification about Ansible task start to all channels."""
162+
# Exit early if no channels provided
163+
if not channels or len(channels) == 0:
164+
print("No Slack channels provided - skipping Ansible task start notification")
165+
return False
166+
167+
# For update pipeline, use mas-pipelines namespace (no instance ID)
168+
# For install/upgrade pipelines, use mas-{instanceId}-pipelines namespace
169+
if instanceId is None or instanceId == "":
170+
namespace = "mas-pipelines"
171+
else:
172+
namespace = f"mas-{instanceId}-pipelines"
173+
174+
# Get thread information, create if doesn't exist
175+
threadInfo = SlackUtil.getThreadConfigMap(namespace, instanceId, pipelineName)
176+
if threadInfo is None:
177+
print("No thread found - creating pipeline start notification")
178+
threadInfo = notifyPipelineStart(channels, instanceId, pipelineName)
179+
180+
# Get channel count
181+
channelCount = int(threadInfo.get("channel_count", "0"))
182+
if channelCount == 0:
183+
print("No channels found in thread info")
184+
return False
185+
186+
# Send task start message as thread reply to all channels
187+
taskMessage = [
188+
SlackUtil.buildSection(f"⏳ **{taskName}** - Started")
189+
]
190+
191+
allSuccess = True
192+
taskMessageData = {}
193+
194+
for idx in range(channelCount):
195+
channelId = threadInfo.get(f"channel_{idx}")
196+
threadId = threadInfo.get(f"threadId_{idx}")
197+
198+
if channelId and threadId:
199+
response = SlackUtil.postMessageBlocks(channelId, taskMessage, threadId)
200+
201+
# Save message timestamp for this channel
202+
if response.data.get("ok", False):
203+
messageTs = response.data.get("ts")
204+
if messageTs:
205+
# Store with task name and channel index as key
206+
taskMessageData[f"task_{taskName}_{idx}"] = messageTs
207+
else:
208+
allSuccess = False
209+
else:
210+
allSuccess = False
211+
212+
# Update ConfigMap with all task message timestamps
213+
if taskMessageData:
214+
SlackUtil.updateThreadConfigMap(namespace, instanceId, taskMessageData, pipelineName)
215+
216+
return allSuccess
217+
218+
219+
def notifyAnsibleComplete(channels: list[str], rc: int, taskName: str, instanceId: str | None = None, pipelineName: str | None = None) -> bool:
220+
"""Send Slack notification about Ansible task completion status to all channels."""
221+
# Exit early if no channels provided
222+
if not channels or len(channels) == 0:
223+
print("No Slack channels provided - skipping Ansible task completion notification")
224+
return False
225+
226+
# For update pipeline, use mas-pipelines namespace (no instance ID)
227+
# For install/upgrade pipelines, use mas-{instanceId}-pipelines namespace
228+
if instanceId is None or instanceId == "":
229+
namespace = "mas-pipelines"
230+
else:
231+
namespace = f"mas-{instanceId}-pipelines"
232+
233+
# Get thread information, create if doesn't exist
234+
threadInfo = SlackUtil.getThreadConfigMap(namespace, instanceId, pipelineName)
235+
if threadInfo is None:
236+
print("No thread found - creating pipeline start notification")
237+
threadInfo = notifyPipelineStart(channels, instanceId, pipelineName)
238+
239+
# Get channel count
240+
channelCount = int(threadInfo.get("channel_count", "0"))
241+
if channelCount == 0:
242+
print("No channels found in thread info")
243+
return False
244+
245+
# Determine status
246+
if rc == 0:
247+
emoji = "✅"
248+
status = "Success"
249+
else:
250+
emoji = "❌"
251+
status = "Failed"
252+
253+
allSuccess = True
254+
255+
# Update message in each channel
256+
for idx in range(channelCount):
257+
channelId = threadInfo.get(f"channel_{idx}")
258+
threadId = threadInfo.get(f"threadId_{idx}")
259+
taskMessageTs = threadInfo.get(f"task_{taskName}_{idx}")
260+
261+
if not channelId or not threadId:
262+
allSuccess = False
263+
continue
264+
265+
# Calculate task duration if we have the message timestamp
266+
durationText = ""
267+
if taskMessageTs:
268+
from datetime import datetime, timezone
269+
try:
270+
# Message timestamp is in format "1234567890.123456"
271+
startTime = float(taskMessageTs)
272+
endTime = datetime.now(timezone.utc).timestamp()
273+
duration = int(endTime - startTime)
274+
275+
hours, remainder = divmod(duration, 3600)
276+
minutes, seconds = divmod(remainder, 60)
277+
278+
if hours > 0:
279+
durationText = f" ({hours}h {minutes}m {seconds}s)"
280+
elif minutes > 0:
281+
durationText = f" ({minutes}m {seconds}s)"
282+
else:
283+
durationText = f" ({seconds}s)"
284+
except Exception as e:
285+
print(f"Failed to calculate duration for channel {idx}: {e}")
286+
287+
# Build the completion message
288+
taskMessage = [
289+
SlackUtil.buildSection(f"{emoji} **{taskName}** - {status}{durationText}")
290+
]
291+
if rc != 0:
292+
taskMessage.append(SlackUtil.buildSection(f"Return Code: `{rc}`\nCheck logs for details"))
293+
294+
# If we have the original message timestamp, update it; otherwise post new message
295+
if taskMessageTs:
296+
response = SlackUtil.updateMessageBlocks(channelId, taskMessageTs, taskMessage)
297+
if not response.data.get("ok", False):
298+
allSuccess = False
299+
else:
300+
# Fallback: post new message if task start message wasn't tracked
301+
print(f"No start message found for task {taskName} in channel {idx}, posting new completion message")
302+
response = SlackUtil.postMessageBlocks(channelId, taskMessage, threadId)
303+
if not response.data.get("ok", False):
304+
allSuccess = False
305+
306+
# Special case, mas-update pipeline
307+
if namespace == "mas-pipelines" and taskName == "post-deps-update-verify-ingress":
308+
print(f"mas-update pipeline completed with status: {rc}, sending pipeline complete message")
309+
allSuccess: bool = notifyPipelineComplete(channels, rc, instanceId, pipelineName)
310+
311+
return allSuccess
312+
313+
314+
def notifyPipelineComplete(channels: list[str], rc: int, instanceId: str | None = None, pipelineName: str | None = None) -> bool:
315+
"""Send Slack notification about pipeline completion to all channels and cleanup ConfigMap."""
316+
# Exit early if no channels provided
317+
if not channels or len(channels) == 0:
318+
print("No Slack channels provided - skipping pipeline completion notification")
319+
return False
320+
321+
# For update pipeline, use mas-pipelines namespace (no instance ID)
322+
# For install/upgrade pipelines, use mas-{instanceId}-pipelines namespace
323+
if instanceId is None or instanceId == "":
324+
namespace = "mas-pipelines"
325+
else:
326+
namespace = f"mas-{instanceId}-pipelines"
327+
328+
# Get thread information
329+
threadInfo = SlackUtil.getThreadConfigMap(namespace, instanceId, pipelineName)
330+
if threadInfo is None:
331+
print("No thread information found - pipeline may not have started properly")
332+
return False
333+
334+
# Get channel count
335+
channelCount = int(threadInfo.get("channel_count", "0"))
336+
if channelCount == 0:
337+
print("No channels found in thread info")
338+
return False
339+
340+
startTime = threadInfo.get("startTime")
341+
342+
# Calculate duration if start time is available
343+
durationText = ""
344+
if startTime:
345+
from datetime import datetime, timezone
346+
try:
347+
start = datetime.fromisoformat(startTime.replace("Z", "+00:00"))
348+
end = datetime.now(timezone.utc)
349+
duration = end - start
350+
hours, remainder = divmod(int(duration.total_seconds()), 3600)
351+
minutes, seconds = divmod(remainder, 60)
352+
if hours > 0:
353+
durationText = f"\nTotal Duration: {hours}h {minutes}m {seconds}s"
354+
else:
355+
durationText = f"\nTotal Duration: {minutes}m {seconds}s"
356+
except Exception:
357+
pass
358+
359+
instanceInfo = f"Instance ID: `{instanceId}`" if instanceId else ""
360+
if rc == 0:
361+
emoji = "🎉"
362+
status = "Completed Successfully"
363+
additionalInfo = "\nAll tasks completed successfully"
364+
else:
365+
emoji = "💥"
366+
status = "Failed"
367+
additionalInfo = f"\nPipeline failed with return code: `{rc}`"
368+
369+
message = [
370+
SlackUtil.buildHeader(f"{emoji} MAS {pipelineName} Pipeline {status}"),
371+
SlackUtil.buildSection(f"Pipeline Run: {pipelineName}\n{instanceInfo}{durationText}{additionalInfo}")
372+
]
373+
374+
allSuccess = True
375+
376+
# Send completion message to all channels
377+
for idx in range(channelCount):
378+
channelId = threadInfo.get(f"channel_{idx}")
379+
threadId = threadInfo.get(f"threadId_{idx}")
380+
381+
if channelId and threadId:
382+
response = SlackUtil.postMessageBlocks(channelId, message, threadId)
383+
if not response.data.get("ok", False):
384+
allSuccess = False
385+
else:
386+
allSuccess = False
387+
388+
# Clean up ConfigMap
389+
SlackUtil.deleteThreadConfigMap(namespace, instanceId, pipelineName)
390+
391+
return allSuccess
392+
393+
100394
if __name__ == "__main__":
101395
# If SLACK_TOKEN or SLACK_CHANNEL env vars are not set then silently exit taking no action
102396
SLACK_TOKEN = os.getenv("SLACK_TOKEN", "")
@@ -112,12 +406,23 @@ if __name__ == "__main__":
112406

113407
# Primary Options
114408
parser.add_argument("--action", required=True)
115-
parser.add_argument("--rc", required=True, type=int)
409+
parser.add_argument("--rc", required=False, type=int)
116410
parser.add_argument("--msg", required=False, default=None)
411+
parser.add_argument("--task-name", required=False, default="")
412+
parser.add_argument("--instance-id", required=False, default=None)
413+
parser.add_argument("--pipeline-name", required=False, default=None)
117414

118415
args, unknown = parser.parse_known_args()
119416

120417
if args.action == "ocp-provision-fyre":
121418
notifyProvisionFyre(channelList, args.rc, args.msg)
122419
elif args.action == "ocp-provision-roks":
123420
notifyProvisionRoks(channelList, args.rc, args.msg)
421+
elif args.action == "pipeline-start":
422+
notifyPipelineStart(channelList, args.instance_id, args.pipeline_name)
423+
elif args.action == "ansible-start":
424+
notifyAnsibleStart(channelList, args.task_name, args.instance_id, args.pipeline_name)
425+
elif args.action == "ansible-complete":
426+
notifyAnsibleComplete(channelList, args.rc, args.task_name, args.instance_id, args.pipeline_name)
427+
elif args.action == "pipeline-complete":
428+
notifyPipelineComplete(channelList, args.rc, args.instance_id, args.pipeline_name)

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
[build-system]
22
requires = [
3-
"setuptools",
4-
"pypandoc"
3+
"setuptools"
54
]
65
build-backend = "setuptools.build_meta"

setup.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,8 @@
1414
sys.path.insert(0, 'src')
1515

1616

17-
if not os.path.exists('README.rst'):
18-
import pypandoc
19-
pypandoc.download_pandoc(targetfolder='~/bin/')
20-
pypandoc.convert_file('README.md', 'rst', outputfile='README.rst')
21-
2217
here = os.path.abspath(os.path.dirname(__file__))
23-
with open(os.path.join(here, 'README.rst'), encoding='utf-8') as f:
18+
with open(os.path.join(here, 'README.md'), encoding='utf-8') as f:
2419
long_description = f.read()
2520

2621
# Maintain a single source of versioning
@@ -54,6 +49,7 @@ def get_version(rel_path):
5449
license='Eclipse Public License - v1.0',
5550
description='Python for Maximo Application Suite Dev/Ops',
5651
long_description=long_description,
52+
long_description_content_type='text/markdown',
5753
install_requires=[
5854
'pyyaml', # MIT License
5955
'openshift', # Apache Software License

src/mas/devops/data/catalogs/v9-260216-amd64.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ ccs_extras_version: 11.0.0 # Extra Images for CCS used for
3434
elasticsearch_version: 1.1.2667 # Operator version 1.1.2667 - used in cpd 5.1.3 only
3535
opensearch_version: 1.1.2494 # Operator version 1.1.2494
3636

37-
# TODO: Why is this here, but commented out?
38-
# datarefinery_build: +20240517.202103.146
39-
4037
# I have added this as a guess as to the actual version used, we are currently using the wsl_version, but that does not exist for datarefinery
4138
# See: https://ibm-mas.slack.com/archives/C02PUHKQB5L/p1770849370378689
4239
datarefinery_version: 11.0.0+20250513.203727.232

0 commit comments

Comments
 (0)