001/* 002 * Copyright 2025 devteam@scivics-lab.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, 011 * software distributed under the License is distributed on an 012 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 013 * either express or implied. See the License for the 014 * specific language governing permissions and limitations 015 * under the License. 016 */ 017 018package com.scivicslab.actoriac; 019 020import java.util.ArrayList; 021import java.util.List; 022import java.util.concurrent.ExecutionException; 023import java.util.logging.Level; 024import java.util.logging.Logger; 025import java.util.regex.Pattern; 026 027import org.json.JSONArray; 028import org.json.JSONObject; 029 030import com.scivicslab.pojoactor.core.ActionResult; 031import com.scivicslab.pojoactor.core.accumulator.Accumulator; 032import com.scivicslab.pojoactor.core.accumulator.BufferedAccumulator; 033import com.scivicslab.pojoactor.core.accumulator.JsonAccumulator; 034import com.scivicslab.pojoactor.core.accumulator.StreamingAccumulator; 035import com.scivicslab.pojoactor.core.accumulator.TableAccumulator; 036import com.scivicslab.pojoactor.workflow.IIActorRef; 037import com.scivicslab.pojoactor.workflow.IIActorSystem; 038 039import com.scivicslab.actoriac.log.DistributedLogStore; 040 041/** 042 * Interpreter-interfaced actor reference for {@link NodeGroupInterpreter} instances. 043 * 044 * <p>This class provides a concrete implementation of {@link IIActorRef} 045 * specifically for {@link NodeGroupInterpreter} objects. It manages groups of infrastructure 046 * nodes and can apply actions to all nodes in a group using wildcard patterns.</p> 047 * 048 * <p>NodeGroupInterpreter extends Interpreter, so this class can execute main workflows 049 * that orchestrate multiple nodes.</p> 050 * 051 * <p><strong>Supported actions:</strong></p> 052 * <p><em>Workflow actions (from Interpreter):</em></p> 053 * <ul> 054 * <li>{@code runWorkflow} - Loads and runs a workflow file</li> 055 * <li>{@code readYaml} - Reads a YAML workflow definition</li> 056 * <li>{@code runUntilEnd} - Executes the workflow until completion</li> 057 * </ul> 058 * <p><em>NodeGroup actions:</em></p> 059 * <ul> 060 * <li>{@code hasInventory} - Returns true if inventory is loaded (for conditional branching)</li> 061 * <li>{@code createNodeActors} - Creates child actors for all nodes in a specified group</li> 062 * <li>{@code apply} - Applies an action to child actors matching a wildcard pattern</li> 063 * <li>{@code hasAccumulator} - Returns true if accumulator exists (for idempotent workflows)</li> 064 * <li>{@code createAccumulator} - Creates an accumulator for result collection</li> 065 * <li>{@code getAccumulatorSummary} - Gets the collected results</li> 066 * </ul> 067 * 068 * <p><strong>Node Actor Hierarchy:</strong></p> 069 * <p>When {@code createNodeActors} is called, it creates a parent-child relationship:</p> 070 * <pre> 071 * NodeGroup (parent) 072 * ├─ node-web-01 (child NodeIIAR) 073 * ├─ node-web-02 (child NodeIIAR) 074 * └─ node-db-01 (child NodeIIAR) 075 * </pre> 076 * 077 * <p><strong>Example YAML Workflow:</strong></p> 078 * <pre>{@code 079 * name: setup-nodegroup 080 * steps: 081 * # Step 1: Create node actors 082 * - states: [0, 1] 083 * actions: 084 * - actor: nodeGroup 085 * method: createNodeActors 086 * arguments: ["web-servers"] 087 * 088 * # Step 2: Run workflow on all nodes (load and execute in one step) 089 * - states: [1, end] 090 * actions: 091 * - actor: nodeGroup 092 * method: apply 093 * arguments: ['{"actor": "node-*", "method": "runWorkflow", "arguments": ["deploy.yaml"]}'] 094 * }</pre> 095 * 096 * @author devteam@scivics-lab.com 097 */ 098public class NodeGroupIIAR extends IIActorRef<NodeGroupInterpreter> { 099 100 Logger logger = null; 101 private LoggingAccumulatorIIAR accumulatorActor = null; 102 103 /** 104 * Constructs a new NodeGroupIIAR with the specified actor name and nodeGroupInterpreter object. 105 * 106 * @param actorName the name of this actor 107 * @param object the {@link NodeGroupInterpreter} instance managed by this actor reference 108 */ 109 public NodeGroupIIAR(String actorName, NodeGroupInterpreter object) { 110 super(actorName, object); 111 logger = Logger.getLogger(actorName); 112 } 113 114 /** 115 * Constructs a new NodeGroupIIAR with the specified actor name, nodeGroupInterpreter object, 116 * and actor system. 117 * 118 * @param actorName the name of this actor 119 * @param object the {@link NodeGroupInterpreter} instance managed by this actor reference 120 * @param system the actor system managing this actor 121 */ 122 public NodeGroupIIAR(String actorName, NodeGroupInterpreter object, IIActorSystem system) { 123 super(actorName, object, system); 124 logger = Logger.getLogger(actorName); 125 126 // Set the selfActorRef in the Interpreter (NodeGroupInterpreter extends Interpreter) 127 object.setSelfActorRef(this); 128 } 129 130 /** 131 * Invokes an action on the node group by name with the given arguments. 132 * 133 * <p>This method dispatches to specialized handler methods based on the action type:</p> 134 * <ul> 135 * <li>Workflow actions: {@link #handleWorkflowAction}</li> 136 * <li>NodeGroup-specific actions: {@link #handleNodeGroupAction}</li> 137 * </ul> 138 * 139 * @param actionName the name of the action to execute 140 * @param arg the argument string (JSON array format) 141 * @return an {@link ActionResult} indicating success or failure with a message 142 */ 143 @Override 144 public ActionResult callByActionName(String actionName, String arg) { 145 logger.fine(String.format("actionName = %s, args = %s", actionName, arg)); 146 147 try { 148 // Workflow execution actions (from Interpreter) 149 ActionResult workflowResult = handleWorkflowAction(actionName, arg); 150 if (workflowResult != null) { 151 return workflowResult; 152 } 153 154 // NodeGroup-specific actions 155 ActionResult nodeGroupResult = handleNodeGroupAction(actionName, arg); 156 if (nodeGroupResult != null) { 157 return nodeGroupResult; 158 } 159 160 // Unknown action 161 logger.log(Level.SEVERE, String.format("Unknown action: actorName = %s, action = %s, arg = %s", 162 this.getName(), actionName, arg)); 163 return new ActionResult(false, "Unknown action: " + actionName); 164 165 } catch (InterruptedException e) { 166 logger.log(Level.SEVERE, String.format("actionName = %s, args = %s", actionName, arg), e); 167 return new ActionResult(false, "Interrupted: " + e.getMessage()); 168 } catch (ExecutionException e) { 169 logger.log(Level.SEVERE, String.format("actionName = %s, args = %s", actionName, arg), e); 170 return new ActionResult(false, "Execution error: " + e.getMessage()); 171 } catch (Exception e) { 172 logger.log(Level.SEVERE, String.format("actionName = %s, args = %s", actionName, arg), e); 173 return new ActionResult(false, "Error: " + e.getMessage()); 174 } 175 } 176 177 /** 178 * Handles workflow-related actions (from Interpreter). 179 * 180 * @param actionName the action name 181 * @param arg the argument string 182 * @return ActionResult if handled, null if not a workflow action 183 */ 184 private ActionResult handleWorkflowAction(String actionName, String arg) 185 throws InterruptedException, ExecutionException { 186 187 switch (actionName) { 188 case "execCode": 189 return this.ask(n -> n.execCode()).get(); 190 191 case "runUntilEnd": 192 int maxIterations = parseMaxIterations(arg, 10000); 193 return this.ask(n -> n.runUntilEnd(maxIterations)).get(); 194 195 case "runWorkflow": 196 JSONArray runArgs = new JSONArray(arg); 197 String workflowFile = runArgs.getString(0); 198 int runMaxIterations = runArgs.length() > 1 ? runArgs.getInt(1) : 10000; 199 logger.info(String.format("Running workflow: %s (maxIterations=%d)", workflowFile, runMaxIterations)); 200 ActionResult result = this.object.runWorkflow(workflowFile, runMaxIterations); 201 logger.info(String.format("Workflow completed: success=%s, result=%s", result.isSuccess(), result.getResult())); 202 return result; 203 204 case "readYaml": 205 return handleReadYaml(arg); 206 207 default: 208 return null; // Not a workflow action 209 } 210 } 211 212 /** 213 * Handles NodeGroup-specific actions. 214 * 215 * @param actionName the action name 216 * @param arg the argument string 217 * @return ActionResult if handled, null if not a NodeGroup action 218 */ 219 private ActionResult handleNodeGroupAction(String actionName, String arg) 220 throws InterruptedException, ExecutionException { 221 222 switch (actionName) { 223 case "hasInventory": 224 boolean hasInv = this.object.getInventory() != null; 225 return new ActionResult(hasInv, hasInv ? "Inventory available" : "No inventory"); 226 227 case "createNodeActors": 228 String groupName = extractSingleArgument(arg); 229 createNodeActors(groupName); 230 return new ActionResult(true, String.format("Created node actors for group '%s'", groupName)); 231 232 case "apply": 233 return apply(arg); 234 235 case "executeCommandOnAllNodes": 236 String command = extractSingleArgument(arg); 237 List<String> results = executeCommandOnAllNodes(command); 238 return new ActionResult(true, 239 String.format("Executed command on %d nodes: %s", results.size(), results)); 240 241 case "hasAccumulator": 242 boolean hasAcc = accumulatorActor != null; 243 return new ActionResult(hasAcc, hasAcc ? "Accumulator exists" : "No accumulator"); 244 245 case "createAccumulator": 246 String type = extractSingleArgument(arg); 247 createAccumulator(type); 248 return new ActionResult(true, String.format("Created %s accumulator", type)); 249 250 case "getAccumulatorSummary": 251 return getAccumulatorSummary(); 252 253 case "printSessionSummary": 254 return printSessionSummary(); 255 256 case "doNothing": 257 return new ActionResult(true, arg); 258 259 default: 260 return null; // Not a NodeGroup action 261 } 262 } 263 264 // --- Helper methods --- 265 266 private ActionResult handleReadYaml(String arg) throws InterruptedException, ExecutionException { 267 String filePath = extractSingleArgument(arg); 268 try { 269 String overlayPath = this.object.getOverlayDir(); 270 if (overlayPath != null) { 271 java.nio.file.Path yamlPath = java.nio.file.Path.of(filePath); 272 java.nio.file.Path overlayDir = java.nio.file.Path.of(overlayPath); 273 this.tell(n -> { 274 try { 275 n.readYaml(yamlPath, overlayDir); 276 } catch (java.io.IOException e) { 277 throw new RuntimeException(e); 278 } 279 }).get(); 280 return new ActionResult(true, "YAML loaded with overlay: " + overlayPath); 281 } else { 282 try (java.io.InputStream input = new java.io.FileInputStream(new java.io.File(filePath))) { 283 this.tell(n -> n.readYaml(input)).get(); 284 return new ActionResult(true, "YAML loaded successfully"); 285 } 286 } 287 } catch (java.io.FileNotFoundException e) { 288 logger.log(Level.SEVERE, String.format("file not found: %s", filePath), e); 289 return new ActionResult(false, "File not found: " + filePath); 290 } catch (java.io.IOException e) { 291 logger.log(Level.SEVERE, String.format("IOException: %s", filePath), e); 292 return new ActionResult(false, "IO error: " + filePath); 293 } catch (RuntimeException e) { 294 if (e.getCause() instanceof java.io.IOException) { 295 logger.log(Level.SEVERE, String.format("IOException: %s", filePath), e.getCause()); 296 return new ActionResult(false, "IO error: " + filePath); 297 } 298 throw e; 299 } 300 } 301 302 private int parseMaxIterations(String arg, int defaultValue) { 303 if (arg != null && !arg.isEmpty() && !arg.equals("[]")) { 304 try { 305 JSONArray args = new JSONArray(arg); 306 if (args.length() > 0) { 307 return args.getInt(0); 308 } 309 } catch (Exception e) { 310 // Use default if parsing fails 311 } 312 } 313 return defaultValue; 314 } 315 316 /** 317 * Creates child node actors for all nodes in the specified group. 318 * 319 * <p>This method creates Node POJOs using the NodeGroup's inventory, 320 * wraps each in a NodeInterpreter (for workflow capabilities), 321 * then wraps in a NodeIIAR, and registers them as children of this actor 322 * using the parent-child relationship mechanism.</p> 323 * 324 * <p>Special handling for "local" group: creates a localhost node without 325 * requiring an inventory file. This is useful for development and testing.</p> 326 * 327 * @param groupName the name of the group from the inventory file, or "local" for localhost 328 */ 329 private void createNodeActors(String groupName) { 330 // Direct execution (no tell().get() to avoid deadlock when called from workflow) 331 IIActorSystem sys = (IIActorSystem) this.system(); 332 NodeGroupInterpreter nodeGroupInterpreter = this.object; 333 334 // Create Node POJOs for the group 335 // Special handling for "local" group: create localhost node without inventory 336 List<Node> nodes; 337 if ("local".equals(groupName)) { 338 nodes = nodeGroupInterpreter.createLocalNode(); 339 } else { 340 nodes = nodeGroupInterpreter.createNodesForGroup(groupName); 341 } 342 343 // Create child actors for each node 344 for (Node node : nodes) { 345 String nodeName = "node-" + node.getHostname(); 346 347 // Wrap Node in NodeInterpreter to add workflow capabilities 348 NodeInterpreter nodeInterpreter = new NodeInterpreter(node, sys); 349 350 // Propagate workflowBaseDir to child interpreter 351 if (nodeGroupInterpreter.getWorkflowBaseDir() != null) { 352 nodeInterpreter.setWorkflowBaseDir(nodeGroupInterpreter.getWorkflowBaseDir()); 353 } 354 355 // Propagate overlayDir to child interpreter 356 if (nodeGroupInterpreter.getOverlayDir() != null) { 357 nodeInterpreter.setOverlayDir(nodeGroupInterpreter.getOverlayDir()); 358 } 359 360 // Create child actor using ActorRef.createChild() 361 // This establishes parent-child relationship 362 this.createChild(nodeName, nodeInterpreter); 363 364 // Also wrap in NodeIIAR and add to system for workflow execution 365 NodeIIAR nodeActor = new NodeIIAR(nodeName, nodeInterpreter, sys); 366 sys.addIIActor(nodeActor); 367 368 logger.fine(String.format("Created child node actor: %s", nodeName)); 369 } 370 logger.info(String.format("Created %d node actors for group '%s'", nodes.size(), groupName)); 371 } 372 373 /** 374 * Applies an action to child actors matching a wildcard pattern. 375 * 376 * <p>This method parses an action definition JSON and executes the specified 377 * method on all child actors whose names match the pattern.</p> 378 * 379 * <p>Action definition format:</p> 380 * <pre>{@code 381 * { 382 * "actor": "node-*", // Wildcard pattern for actor names 383 * "method": "executeCommand", // Method to call 384 * "arguments": ["ls -la"] // Arguments (optional) 385 * } 386 * }</pre> 387 * 388 * <p>Supported wildcard patterns:</p> 389 * <ul> 390 * <li>{@code *} - Matches all child actors</li> 391 * <li>{@code node-*} - Matches actors starting with "node-"</li> 392 * <li>{@code *-web} - Matches actors ending with "-web"</li> 393 * </ul> 394 * 395 * @param actionDef JSON string defining the action to apply 396 * @return ActionResult indicating success or failure 397 */ 398 private ActionResult apply(String actionDef) { 399 try { 400 JSONObject action = new JSONObject(actionDef); 401 String actorPattern = action.getString("actor"); 402 String method = action.getString("method"); 403 JSONArray argsArray = action.optJSONArray("arguments"); 404 String args = argsArray != null ? argsArray.toString() : "[]"; 405 406 // Find matching child actors 407 List<IIActorRef<?>> matchedActors = findMatchingChildActors(actorPattern); 408 409 if (matchedActors.isEmpty()) { 410 return new ActionResult(false, "No actors matched pattern: " + actorPattern); 411 } 412 413 logger.info(String.format("Applying method '%s' to %d actors matching '%s'", 414 method, matchedActors.size(), actorPattern)); 415 416 // Apply action to each matching actor (continue on failure, report all errors) 417 int successCount = 0; 418 List<String> failures = new ArrayList<>(); 419 for (IIActorRef<?> actor : matchedActors) { 420 ActionResult result = actor.callByActionName(method, args); 421 if (!result.isSuccess()) { 422 failures.add(String.format("%s: %s", actor.getName(), result.getResult())); 423 logger.warning(String.format("Failed on %s: %s", actor.getName(), result.getResult())); 424 } else { 425 successCount++; 426 logger.fine(String.format("Applied to %s: %s", actor.getName(), result.getResult())); 427 } 428 } 429 430 if (failures.isEmpty()) { 431 return new ActionResult(true, 432 String.format("Applied to %d actors", successCount)); 433 } else { 434 return new ActionResult(false, 435 String.format("Applied to %d/%d actors. Failures: %s", 436 successCount, matchedActors.size(), String.join("; ", failures))); 437 } 438 439 } catch (Exception e) { 440 logger.log(Level.SEVERE, "Error in apply: " + actionDef, e); 441 return new ActionResult(false, "Error: " + e.getMessage()); 442 } 443 } 444 445 /** 446 * Finds child actors matching a wildcard pattern. 447 * 448 * @param pattern the wildcard pattern (e.g., "node-*", "*-web", "*") 449 * @return list of matching child actors 450 */ 451 private List<IIActorRef<?>> findMatchingChildActors(String pattern) { 452 List<IIActorRef<?>> matched = new ArrayList<>(); 453 IIActorSystem system = (IIActorSystem) this.system(); 454 455 if (system == null) { 456 return matched; 457 } 458 459 List<String> childNames = new ArrayList<>(this.getNamesOfChildren()); 460 461 // Exact match (no wildcard) 462 if (!pattern.contains("*")) { 463 if (childNames.contains(pattern)) { 464 IIActorRef<?> actor = system.getIIActor(pattern); 465 if (actor != null) { 466 matched.add(actor); 467 } 468 } 469 return matched; 470 } 471 472 // Convert wildcard to regex 473 String regex = pattern 474 .replace(".", "\\.") 475 .replace("*", ".*"); 476 Pattern compiled = Pattern.compile(regex); 477 478 for (String childName : childNames) { 479 if (compiled.matcher(childName).matches()) { 480 IIActorRef<?> actor = system.getIIActor(childName); 481 if (actor != null) { 482 matched.add(actor); 483 } 484 } 485 } 486 487 return matched; 488 } 489 490 /** 491 * Executes a single command on all child node actors. 492 * 493 * @param command the command to execute 494 * @return list of results from each node 495 * @throws ExecutionException if command execution fails 496 * @throws InterruptedException if the operation is interrupted 497 */ 498 private List<String> executeCommandOnAllNodes(String command) 499 throws ExecutionException, InterruptedException { 500 501 IIActorSystem system = (IIActorSystem) this.system(); 502 List<String> results = new ArrayList<>(); 503 504 // Get all child node names 505 List<String> childNames = new ArrayList<>(this.getNamesOfChildren()); 506 507 logger.info(String.format("Executing command on %d nodes: %s", childNames.size(), command)); 508 509 // Execute on each child node 510 for (String childName : childNames) { 511 IIActorRef<?> actorRef = system.getIIActor(childName); 512 if (actorRef == null || !(actorRef instanceof NodeIIAR)) { 513 logger.warning(String.format("Child node actor not found or wrong type: %s", childName)); 514 continue; 515 } 516 NodeIIAR nodeActor = (NodeIIAR) actorRef; 517 518 // Execute the command 519 JSONArray commandArgs = new JSONArray(); 520 commandArgs.put(command); 521 ActionResult result = nodeActor.callByActionName("executeCommand", commandArgs.toString()); 522 523 results.add(String.format("%s: %s", childName, result.getResult())); 524 } 525 526 return results; 527 } 528 529 /** 530 * Extracts a single argument from JSON array format. 531 * 532 * @param arg the JSON array argument string 533 * @return the extracted argument 534 */ 535 private String extractSingleArgument(String arg) { 536 try { 537 JSONArray jsonArray = new JSONArray(arg); 538 if (jsonArray.length() == 0) { 539 throw new IllegalArgumentException("Arguments cannot be empty"); 540 } 541 return jsonArray.getString(0); 542 } catch (Exception e) { 543 throw new IllegalArgumentException( 544 "Invalid argument format. Expected JSON array: " + arg, e); 545 } 546 } 547 548 /** 549 * Creates an accumulator as a child actor. 550 * 551 * <p>If a log store is configured in the NodeGroupInterpreter, creates a 552 * LoggingAccumulatorIIAR that also writes to the H2 database.</p> 553 * 554 * @param type the accumulator type ("streaming", "buffered", "table", "json") 555 */ 556 private void createAccumulator(String type) { 557 IIActorSystem sys = (IIActorSystem) this.system(); 558 559 // Create POJO based on type 560 Accumulator accumulator; 561 switch (type.toLowerCase()) { 562 case "streaming": 563 accumulator = new StreamingAccumulator(); 564 break; 565 case "buffered": 566 accumulator = new BufferedAccumulator(); 567 break; 568 case "table": 569 accumulator = new TableAccumulator(); 570 break; 571 case "json": 572 accumulator = new JsonAccumulator(); 573 break; 574 default: 575 throw new IllegalArgumentException("Unknown accumulator type: " + type); 576 } 577 578 // Get logStore and sessionId from NodeGroupInterpreter 579 DistributedLogStore logStore = this.object.getLogStore(); 580 long sessionId = this.object.getSessionId(); 581 582 // Create LoggingAccumulatorIIAR that also writes to H2 database 583 accumulatorActor = new LoggingAccumulatorIIAR("accumulator", accumulator, sys, logStore, sessionId); 584 this.createChild("accumulator", accumulator); 585 sys.addIIActor(accumulatorActor); 586 587 logger.info(String.format("Created %s accumulator as child actor (logging=%s)", 588 type, logStore != null ? "enabled" : "disabled")); 589 } 590 591 /** 592 * Gets the summary from the accumulator. 593 * 594 * @return ActionResult with the summary or error 595 */ 596 private ActionResult getAccumulatorSummary() { 597 if (accumulatorActor == null) { 598 return new ActionResult(false, "No accumulator created"); 599 } 600 ActionResult result = accumulatorActor.callByActionName("getSummary", ""); 601 // Print the summary to stdout 602 System.out.println(result.getResult()); 603 return result; 604 } 605 606 /** 607 * Prints a summary of the current session's verification results. 608 * 609 * <p>Groups results by vertex name (step) and displays a formatted table.</p> 610 * 611 * @return ActionResult with success status and summary text 612 */ 613 private ActionResult printSessionSummary() { 614 DistributedLogStore logStore = this.object.getLogStore(); 615 long sessionId = this.object.getSessionId(); 616 617 if (logStore == null || sessionId < 0) { 618 String msg = "Log store not available"; 619 System.out.println(msg); 620 return new ActionResult(false, msg); 621 } 622 623 // Get all logs for this session 624 List<com.scivicslab.actoriac.log.LogEntry> logs = logStore.getLogsByLevel(sessionId, 625 com.scivicslab.actoriac.log.LogLevel.DEBUG); 626 627 // Group logs by vertex name and count results 628 java.util.Map<String, VerifyResult> resultsByVertex = new java.util.LinkedHashMap<>(); 629 630 for (com.scivicslab.actoriac.log.LogEntry entry : logs) { 631 String message = entry.getMessage(); 632 String vertexName = entry.getVertexName(); 633 if (message == null) continue; 634 635 // Extract vertex name from the message if it contains step info 636 // Format: "- states: [...]\n vertexName: xxx\n..." 637 if (vertexName != null && vertexName.contains("vertexName:")) { 638 int idx = vertexName.indexOf("vertexName:"); 639 if (idx >= 0) { 640 String rest = vertexName.substring(idx + 11).trim(); 641 int end = rest.indexOf('\n'); 642 vertexName = end > 0 ? rest.substring(0, end).trim() : rest.trim(); 643 } 644 } 645 646 // Skip non-verify steps 647 if (vertexName == null || !vertexName.startsWith("verify-")) { 648 continue; 649 } 650 651 VerifyResult result = resultsByVertex.computeIfAbsent(vertexName, k -> new VerifyResult()); 652 653 // Count occurrences in message 654 result.okCount += countOccurrences(message, "[OK]"); 655 result.warnCount += countOccurrences(message, "[WARN]"); 656 result.errorCount += countOccurrences(message, "[ERROR]"); 657 result.infoCount += countOccurrences(message, "[INFO]"); 658 659 // Extract special info (like document count, cluster health) 660 extractSpecialInfo(message, result); 661 } 662 663 // Build summary output 664 StringBuilder sb = new StringBuilder(); 665 sb.append("\n"); 666 sb.append("============================================================\n"); 667 sb.append(" VERIFICATION SUMMARY\n"); 668 sb.append("============================================================\n"); 669 sb.append("\n"); 670 sb.append(String.format("| %-35s | %-20s |\n", "Item", "Status")); 671 sb.append("|-------------------------------------|----------------------|\n"); 672 673 // Mapping from vertex names to display names and aggregation 674 String[][] mappings = { 675 {"verify-repos", "Document repositories"}, 676 {"verify-utility-cli", "Utility-cli"}, 677 {"verify-utility-sau3", "Utility-sau3"}, 678 {"verify-builds", "Docusaurus builds"}, 679 {"verify-public-html", "public_html deploy"}, 680 {"verify-apache", "Apache2 + UserDir"}, 681 {"verify-opensearch-install", "OpenSearch install"}, 682 {"verify-opensearch-running", "OpenSearch status"}, 683 {"verify-docusearch-build", "quarkus-docusearch build"}, 684 {"verify-docusearch-running", "quarkus-docusearch server"}, 685 {"verify-search-index", "Search index"}, 686 {"verify-web-access", "Web access"}, 687 }; 688 689 int totalOk = 0, totalWarn = 0, totalError = 0; 690 List<String> errorDetails = new ArrayList<>(); 691 List<String> warnDetails = new ArrayList<>(); 692 693 for (String[] mapping : mappings) { 694 String vertexName = mapping[0]; 695 String displayName = mapping[1]; 696 VerifyResult result = resultsByVertex.get(vertexName); 697 698 if (result == null) { 699 sb.append(String.format("| %-35s | %-20s |\n", displayName, "-")); 700 continue; 701 } 702 703 totalOk += result.okCount; 704 totalWarn += result.warnCount; 705 totalError += result.errorCount; 706 707 String status = formatStatus(result); 708 sb.append(String.format("| %-35s | %-20s |\n", displayName, status)); 709 710 // Collect error/warning details 711 if (result.errorCount > 0) { 712 errorDetails.add(displayName + ": " + result.errorCount + " error(s)"); 713 } 714 if (result.warnCount > 0) { 715 warnDetails.add(displayName + ": " + result.warnCount + " warning(s)"); 716 } 717 } 718 719 sb.append("|-------------------------------------|----------------------|\n"); 720 sb.append(String.format("| %-35s | %d OK, %d WARN, %d ERR |\n", 721 "TOTAL", totalOk, totalWarn, totalError)); 722 sb.append("============================================================\n"); 723 724 // Show error details if any 725 if (!errorDetails.isEmpty()) { 726 sb.append("\n--- Errors ---\n"); 727 for (String detail : errorDetails) { 728 sb.append(" * ").append(detail).append("\n"); 729 } 730 } 731 732 // Show warning details if any 733 if (!warnDetails.isEmpty()) { 734 sb.append("\n--- Warnings ---\n"); 735 for (String detail : warnDetails) { 736 sb.append(" * ").append(detail).append("\n"); 737 } 738 } 739 740 sb.append("\n"); 741 if (totalError == 0 && totalWarn == 0) { 742 sb.append("All checks passed!\n"); 743 } else if (totalError > 0) { 744 sb.append("To fix issues, run:\n"); 745 sb.append(" ./actor_iac.java --dir ./docu-search --workflow main-setup\n"); 746 } 747 748 String summary = sb.toString(); 749 System.out.println(summary); 750 return new ActionResult(true, summary); 751 } 752 753 /** 754 * Formats the status string for a verification result. 755 */ 756 private String formatStatus(VerifyResult result) { 757 if (result.errorCount > 0) { 758 if (result.okCount > 0) { 759 return String.format("%d OK, %d ERROR", result.okCount, result.errorCount); 760 } 761 return "ERROR"; 762 } 763 if (result.warnCount > 0) { 764 if (result.okCount > 0) { 765 return String.format("%d OK, %d WARN", result.okCount, result.warnCount); 766 } 767 return "WARN"; 768 } 769 if (result.okCount > 0) { 770 String extra = result.extraInfo != null ? " " + result.extraInfo : ""; 771 return result.okCount + " OK" + extra; 772 } 773 return "OK"; 774 } 775 776 /** 777 * Extracts special information from log messages (like document count, cluster health). 778 */ 779 private void extractSpecialInfo(String message, VerifyResult result) { 780 // Extract document count from search index 781 if (message.contains("documents")) { 782 java.util.regex.Matcher m = java.util.regex.Pattern.compile("(\\d+)\\s+documents").matcher(message); 783 if (m.find()) { 784 result.extraInfo = "(" + m.group(1) + " docs)"; 785 } 786 } 787 // Extract cluster health 788 if (message.contains("Cluster health:")) { 789 java.util.regex.Matcher m = java.util.regex.Pattern.compile("Cluster health:\\s*(\\w+)").matcher(message); 790 if (m.find()) { 791 result.extraInfo = "(" + m.group(1) + ")"; 792 } 793 } 794 // Extract web access count 795 if (message.contains("Accessible at")) { 796 // Count from "X / Y" pattern is handled by OK count 797 } 798 } 799 800 /** 801 * Counts occurrences of a substring in a string. 802 */ 803 private int countOccurrences(String text, String sub) { 804 int count = 0; 805 int idx = 0; 806 while ((idx = text.indexOf(sub, idx)) != -1) { 807 count++; 808 idx += sub.length(); 809 } 810 return count; 811 } 812 813 /** 814 * Helper class to hold verification results for a step. 815 */ 816 private static class VerifyResult { 817 int okCount = 0; 818 int warnCount = 0; 819 int errorCount = 0; 820 int infoCount = 0; 821 String extraInfo = null; 822 } 823 824}