001/* 002 * Copyright 2025 devteam@scivicslab.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.Map; 023import java.util.concurrent.CompletableFuture; 024import java.util.concurrent.ConcurrentHashMap; 025import java.util.concurrent.ExecutionException; 026import java.util.concurrent.atomic.AtomicInteger; 027import java.util.logging.Level; 028import java.util.logging.Logger; 029import java.util.regex.Pattern; 030 031import org.json.JSONArray; 032import org.json.JSONObject; 033 034import com.scivicslab.pojoactor.core.Action; 035import com.scivicslab.pojoactor.core.ActionResult; 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 * <li>{@code getSessionId} - Gets the current session ID for log queries</li> 067 * </ul> 068 * 069 * <p><strong>Node Actor Hierarchy:</strong></p> 070 * <p>When {@code createNodeActors} is called, it creates a parent-child relationship:</p> 071 * <pre> 072 * NodeGroup (parent) 073 * ├─ node-web-01 (child NodeIIAR) 074 * ├─ node-web-02 (child NodeIIAR) 075 * └─ node-db-01 (child NodeIIAR) 076 * </pre> 077 * 078 * <p><strong>Example YAML Workflow:</strong></p> 079 * <pre>{@code 080 * name: setup-nodegroup 081 * steps: 082 * # Step 1: Create node actors 083 * - states: [0, 1] 084 * actions: 085 * - actor: nodeGroup 086 * method: createNodeActors 087 * arguments: ["web-servers"] 088 * 089 * # Step 2: Run workflow on all nodes (load and execute in one step) 090 * - states: [1, end] 091 * actions: 092 * - actor: nodeGroup 093 * method: apply 094 * arguments: ['{"actor": "node-*", "method": "runWorkflow", "arguments": ["deploy.yaml"]}'] 095 * }</pre> 096 * 097 * @author devteam@scivicslab.com 098 */ 099public class NodeGroupIIAR extends IIActorRef<NodeGroupInterpreter> { 100 101 Logger logger = null; 102 103 /** Current workflow file path being executed. */ 104 private String currentWorkflowPath = null; 105 106 /** 107 * Constructs a new NodeGroupIIAR with the specified actor name and nodeGroupInterpreter object. 108 * 109 * @param actorName the name of this actor 110 * @param object the {@link NodeGroupInterpreter} instance managed by this actor reference 111 */ 112 public NodeGroupIIAR(String actorName, NodeGroupInterpreter object) { 113 super(actorName, object); 114 logger = Logger.getLogger(actorName); 115 } 116 117 /** 118 * Constructs a new NodeGroupIIAR with the specified actor name, nodeGroupInterpreter object, 119 * and actor system. 120 * 121 * @param actorName the name of this actor 122 * @param object the {@link NodeGroupInterpreter} instance managed by this actor reference 123 * @param system the actor system managing this actor 124 */ 125 public NodeGroupIIAR(String actorName, NodeGroupInterpreter object, IIActorSystem system) { 126 super(actorName, object, system); 127 logger = Logger.getLogger(actorName); 128 129 // Set the selfActorRef in the Interpreter (NodeGroupInterpreter extends Interpreter) 130 object.setSelfActorRef(this); 131 } 132 133 // ======================================================================== 134 // Workflow Actions (from Interpreter) 135 // ======================================================================== 136 137 /** 138 * Executes the current step code. 139 * 140 * @param args the argument string (not used) 141 * @return ActionResult indicating success or failure 142 */ 143 @Action("execCode") 144 public ActionResult execCode(String args) { 145 try { 146 return this.ask(n -> n.execCode()).get(); 147 } catch (InterruptedException | ExecutionException e) { 148 logger.log(Level.SEVERE, "execCode failed", e); 149 return new ActionResult(false, "Error: " + e.getMessage()); 150 } 151 } 152 153 /** 154 * Runs the workflow until completion. 155 * 156 * @param args the argument string (optional max iterations) 157 * @return ActionResult indicating success or failure 158 */ 159 @Action("runUntilEnd") 160 public ActionResult runUntilEnd(String args) { 161 try { 162 int maxIterations = parseMaxIterations(args, 10000); 163 return this.ask(n -> n.runUntilEnd(maxIterations)).get(); 164 } catch (InterruptedException | ExecutionException e) { 165 logger.log(Level.SEVERE, "runUntilEnd failed", e); 166 return new ActionResult(false, "Error: " + e.getMessage()); 167 } 168 } 169 170 /** 171 * Loads and runs a workflow file. 172 * 173 * @param args JSON array with workflow file path and optional max iterations 174 * @return ActionResult indicating success or failure 175 */ 176 @Action("runWorkflow") 177 public ActionResult runWorkflow(String args) { 178 try { 179 JSONArray runArgs = new JSONArray(args); 180 String workflowFile = runArgs.getString(0); 181 this.currentWorkflowPath = workflowFile; // Store for WorkflowReporter 182 int runMaxIterations = runArgs.length() > 1 ? runArgs.getInt(1) : 10000; 183 logger.info(String.format("Running workflow: %s (maxIterations=%d)", workflowFile, runMaxIterations)); 184 ActionResult result = this.object.runWorkflow(workflowFile, runMaxIterations); 185 logger.info(String.format("Workflow completed: success=%s, result=%s", result.isSuccess(), result.getResult())); 186 return result; 187 } catch (Exception e) { 188 logger.log(Level.SEVERE, "runWorkflow failed: " + args, e); 189 return new ActionResult(false, "Error: " + e.getMessage()); 190 } 191 } 192 193 /** 194 * Reads a YAML workflow definition. 195 * 196 * @param args JSON array with file path 197 * @return ActionResult indicating success or failure 198 */ 199 @Action("readYaml") 200 public ActionResult readYaml(String args) { 201 String filePath = extractSingleArgument(args); 202 this.currentWorkflowPath = filePath; // Store for WorkflowReporter 203 try { 204 String overlayPath = this.object.getOverlayDir(); 205 if (overlayPath != null) { 206 java.nio.file.Path yamlPath = java.nio.file.Path.of(filePath); 207 java.nio.file.Path overlayDir = java.nio.file.Path.of(overlayPath); 208 this.tell(n -> { 209 try { 210 n.readYaml(yamlPath, overlayDir); 211 } catch (java.io.IOException e) { 212 throw new RuntimeException(e); 213 } 214 }).get(); 215 return new ActionResult(true, "YAML loaded with overlay: " + overlayPath); 216 } else { 217 try (java.io.InputStream input = new java.io.FileInputStream(new java.io.File(filePath))) { 218 this.tell(n -> n.readYaml(input)).get(); 219 return new ActionResult(true, "YAML loaded successfully"); 220 } 221 } 222 } catch (java.io.FileNotFoundException e) { 223 logger.log(Level.SEVERE, String.format("file not found: %s", filePath), e); 224 return new ActionResult(false, "File not found: " + filePath); 225 } catch (java.io.IOException e) { 226 logger.log(Level.SEVERE, String.format("IOException: %s", filePath), e); 227 return new ActionResult(false, "IO error: " + filePath); 228 } catch (RuntimeException e) { 229 if (e.getCause() instanceof java.io.IOException) { 230 logger.log(Level.SEVERE, String.format("IOException: %s", filePath), e.getCause()); 231 return new ActionResult(false, "IO error: " + filePath); 232 } 233 throw e; 234 } catch (InterruptedException | ExecutionException e) { 235 logger.log(Level.SEVERE, "readYaml failed: " + filePath, e); 236 return new ActionResult(false, "Error: " + e.getMessage()); 237 } 238 } 239 240 // ======================================================================== 241 // NodeGroup-specific Actions 242 // ======================================================================== 243 244 /** 245 * Checks if inventory is loaded. 246 * 247 * @param args the argument string (not used) 248 * @return ActionResult with true if inventory exists 249 */ 250 @Action("hasInventory") 251 public ActionResult hasInventory(String args) { 252 boolean hasInv = this.object.getInventory() != null; 253 return new ActionResult(hasInv, hasInv ? "Inventory available" : "No inventory"); 254 } 255 256 /** 257 * Creates child actors for all nodes in a specified group. 258 * 259 * @param args JSON array with group name 260 * @return ActionResult indicating success or failure 261 */ 262 @Action("createNodeActors") 263 public ActionResult createNodeActorsAction(String args) { 264 String groupName = extractSingleArgument(args); 265 createNodeActors(groupName); 266 return new ActionResult(true, String.format("Created node actors for group '%s'", groupName)); 267 } 268 269 /** 270 * Applies an action to child actors matching a wildcard pattern. 271 * 272 * @param args JSON object defining the action to apply 273 * @return ActionResult indicating success or failure 274 */ 275 @Action("apply") 276 public ActionResult applyAction(String args) { 277 return apply(args); 278 } 279 280 /** 281 * Executes a command on all child node actors. 282 * 283 * @param args JSON array with command 284 * @return ActionResult with execution results 285 */ 286 @Action("executeCommandOnAllNodes") 287 public ActionResult executeCommandOnAllNodesAction(String args) { 288 try { 289 String command = extractSingleArgument(args); 290 List<String> results = executeCommandOnAllNodes(command); 291 return new ActionResult(true, 292 String.format("Executed command on %d nodes: %s", results.size(), results)); 293 } catch (InterruptedException | ExecutionException e) { 294 logger.log(Level.SEVERE, "executeCommandOnAllNodes failed", e); 295 return new ActionResult(false, "Error: " + e.getMessage()); 296 } 297 } 298 299 /** 300 * Checks if accumulator exists. 301 * 302 * @param args the argument string (not used) 303 * @return ActionResult with true if accumulator exists 304 */ 305 @Action("hasAccumulator") 306 public ActionResult hasAccumulator(String args) { 307 boolean hasAcc = ((IIActorSystem) this.system()).getIIActor("outputMultiplexer") != null; 308 return new ActionResult(hasAcc, hasAcc ? "Accumulator exists" : "No accumulator"); 309 } 310 311 /** 312 * Creates an accumulator (no-op, kept for backward compatibility). 313 * 314 * @param args the argument string (not used) 315 * @return ActionResult with success 316 */ 317 @Action("createAccumulator") 318 public ActionResult createAccumulator(String args) { 319 // No-op: MultiplexerAccumulator is now created by RunCLI 320 // This case is kept for backward compatibility with existing workflows 321 return new ActionResult(true, "Accumulator managed by CLI"); 322 } 323 324 /** 325 * Gets the summary from the output multiplexer. 326 * 327 * @param args the argument string (not used) 328 * @return ActionResult with the summary 329 */ 330 @Action("getAccumulatorSummary") 331 public ActionResult getAccumulatorSummaryAction(String args) { 332 return getAccumulatorSummary(); 333 } 334 335 /** 336 * Prints a summary of the current session's verification results. 337 * 338 * @param args the argument string (not used) 339 * @return ActionResult with the summary 340 */ 341 @Action("printSessionSummary") 342 public ActionResult printSessionSummaryAction(String args) { 343 return printSessionSummary(); 344 } 345 346 /** 347 * Gets the current session ID. 348 * 349 * @param args the argument string (not used) 350 * @return ActionResult with the session ID 351 */ 352 @Action("getSessionId") 353 public ActionResult getSessionId(String args) { 354 long sessionId = this.object.getSessionId(); 355 if (sessionId < 0) { 356 return new ActionResult(false, "No session ID set"); 357 } 358 return new ActionResult(true, String.valueOf(sessionId)); 359 } 360 361 /** 362 * Gets the current workflow file path. 363 * 364 * @param args the argument string (not used) 365 * @return ActionResult with the workflow path 366 */ 367 @Action("getWorkflowPath") 368 public ActionResult getWorkflowPath(String args) { 369 if (currentWorkflowPath == null) { 370 return new ActionResult(false, "No workflow path set"); 371 } 372 return new ActionResult(true, currentWorkflowPath); 373 } 374 375 /** 376 * Does nothing, returns the argument as result. 377 * 378 * @param args the argument string 379 * @return ActionResult with the argument 380 */ 381 @Action("doNothing") 382 public ActionResult doNothing(String args) { 383 return new ActionResult(true, args); 384 } 385 386 // ======================================================================== 387 // JSON State Output Actions 388 // ======================================================================== 389 390 /** 391 * Outputs JSON State at the given path in pretty JSON format via outputMultiplexer. 392 * 393 * @param args the path to output (from JSON array) 394 * @return ActionResult with the formatted JSON 395 */ 396 @Action("printJson") 397 public ActionResult printJson(String args) { 398 String path = getFirst(args); 399 String formatted = toStringOfJson(path); 400 sendToMultiplexer(formatted); 401 return new ActionResult(true, formatted); 402 } 403 404 /** 405 * Outputs JSON State at the given path in YAML format via outputMultiplexer. 406 * 407 * @param args the path to output (from JSON array) 408 * @return ActionResult with the formatted YAML 409 */ 410 @Action("printYaml") 411 public ActionResult printYaml(String args) { 412 String path = getFirst(args); 413 String formatted = toStringOfYaml(path); 414 sendToMultiplexer(formatted); 415 return new ActionResult(true, formatted); 416 } 417 418 /** 419 * Sends formatted output to the outputMultiplexer, line by line. 420 */ 421 private void sendToMultiplexer(String formatted) { 422 IIActorSystem sys = (IIActorSystem) this.system(); 423 IIActorRef<?> multiplexer = sys.getIIActor("outputMultiplexer"); 424 if (multiplexer == null) { 425 return; 426 } 427 428 String nodeName = this.getName(); 429 for (String line : formatted.split("\n")) { 430 JSONObject arg = new JSONObject(); 431 arg.put("source", nodeName); 432 arg.put("type", "stdout"); 433 arg.put("data", line); 434 multiplexer.callByActionName("add", arg.toString()); 435 } 436 } 437 438 // ======================================================================== 439 // Helper Methods 440 // ======================================================================== 441 442 private int parseMaxIterations(String arg, int defaultValue) { 443 if (arg != null && !arg.isEmpty() && !arg.equals("[]")) { 444 try { 445 JSONArray args = new JSONArray(arg); 446 if (args.length() > 0) { 447 return args.getInt(0); 448 } 449 } catch (Exception e) { 450 // Use default if parsing fails 451 } 452 } 453 return defaultValue; 454 } 455 456 private String getFirst(String args) { 457 if (args == null || args.isEmpty() || args.equals("[]")) { 458 return ""; 459 } 460 try { 461 JSONArray jsonArray = new JSONArray(args); 462 if (jsonArray.length() > 0) { 463 return jsonArray.getString(0); 464 } 465 } catch (Exception e) { 466 // Return empty string if parsing fails 467 } 468 return ""; 469 } 470 471 /** 472 * Creates child node actors for all nodes in the specified group. 473 * 474 * <p>This method creates Node POJOs using the NodeGroup's inventory, 475 * wraps each in a NodeInterpreter (for workflow capabilities), 476 * then wraps in a NodeIIAR, and registers them as children of this actor 477 * using the parent-child relationship mechanism.</p> 478 * 479 * <p>Special handling for "local" group: creates a localhost node without 480 * requiring an inventory file. This is useful for development and testing.</p> 481 * 482 * @param groupName the name of the group from the inventory file, or "local" for localhost 483 */ 484 private void createNodeActors(String groupName) { 485 // Direct execution (no tell().get() to avoid deadlock when called from workflow) 486 IIActorSystem sys = (IIActorSystem) this.system(); 487 NodeGroupInterpreter nodeGroupInterpreter = this.object; 488 489 // Create Node POJOs for the group 490 // Special handling for "local" group: create localhost node without inventory 491 List<Node> nodes; 492 if ("local".equals(groupName)) { 493 nodes = nodeGroupInterpreter.createLocalNode(); 494 } else { 495 nodes = nodeGroupInterpreter.createNodesForGroup(groupName); 496 } 497 498 // Create child actors for each node 499 for (Node node : nodes) { 500 String nodeName = "node-" + node.getHostname(); 501 502 // Wrap Node in NodeInterpreter to add workflow capabilities 503 NodeInterpreter nodeInterpreter = new NodeInterpreter(node, sys); 504 505 // Propagate workflowBaseDir to child interpreter 506 if (nodeGroupInterpreter.getWorkflowBaseDir() != null) { 507 nodeInterpreter.setWorkflowBaseDir(nodeGroupInterpreter.getWorkflowBaseDir()); 508 } 509 510 // Propagate overlayDir to child interpreter 511 if (nodeGroupInterpreter.getOverlayDir() != null) { 512 nodeInterpreter.setOverlayDir(nodeGroupInterpreter.getOverlayDir()); 513 } 514 515 // Propagate accumulator to child interpreter 516 if (nodeGroupInterpreter.getAccumulator() != null) { 517 nodeInterpreter.setAccumulator(nodeGroupInterpreter.getAccumulator()); 518 } 519 520 // Create child actor using ActorRef.createChild() 521 // This establishes parent-child relationship 522 this.createChild(nodeName, nodeInterpreter); 523 524 // Also wrap in NodeIIAR and add to system for workflow execution 525 NodeIIAR nodeActor = new NodeIIAR(nodeName, nodeInterpreter, sys); 526 sys.addIIActor(nodeActor); 527 528 logger.fine(String.format("Created child node actor: %s", nodeName)); 529 } 530 logger.info(String.format("Created %d node actors for group '%s'", nodes.size(), groupName)); 531 } 532 533 /** 534 * Applies an action to child actors matching a wildcard pattern. 535 * 536 * <p>This method parses an action definition JSON and executes the specified 537 * method on all child actors whose names match the pattern.</p> 538 * 539 * <p>Action definition format:</p> 540 * <pre>{@code 541 * { 542 * "actor": "node-*", // Wildcard pattern for actor names 543 * "method": "executeCommand", // Method to call 544 * "arguments": ["ls -la"] // Arguments (optional) 545 * } 546 * }</pre> 547 * 548 * <p>Supported wildcard patterns:</p> 549 * <ul> 550 * <li>{@code *} - Matches all child actors</li> 551 * <li>{@code node-*} - Matches actors starting with "node-"</li> 552 * <li>{@code *-web} - Matches actors ending with "-web"</li> 553 * </ul> 554 * 555 * @param actionDef JSON string defining the action to apply 556 * @return ActionResult indicating success or failure 557 */ 558 /** 559 * Applies an action to multiple actors matching a pattern in parallel. 560 * 561 * <p>This method executes the specified action on all matching actors 562 * asynchronously and in parallel, then waits for all executions to complete 563 * before returning. This provides significant performance benefits when 564 * executing actions on many nodes.</p> 565 * 566 * @param actionDef JSON string defining the action to apply 567 * @return ActionResult indicating success or failure 568 */ 569 private ActionResult apply(String actionDef) { 570 try { 571 JSONObject action = new JSONObject(actionDef); 572 String actorPattern = action.getString("actor"); 573 String method = action.getString("method"); 574 JSONArray argsArray = action.optJSONArray("arguments"); 575 String args = argsArray != null ? argsArray.toString() : "[]"; 576 577 // Find matching child actors 578 List<IIActorRef<?>> matchedActors = findMatchingChildActors(actorPattern); 579 580 if (matchedActors.isEmpty()) { 581 return new ActionResult(false, "No actors matched pattern: " + actorPattern); 582 } 583 584 logger.info(String.format("Applying method '%s' to %d actors matching '%s' (async parallel)", 585 method, matchedActors.size(), actorPattern)); 586 587 // Thread-safe collections for gathering results 588 AtomicInteger successCount = new AtomicInteger(0); 589 Map<String, String> failures = new ConcurrentHashMap<>(); 590 DistributedLogStore logStore = this.object.getLogStore(); 591 long sessionId = this.object.getSessionId(); 592 593 // Create async tasks for all actors 594 List<CompletableFuture<Void>> futures = new ArrayList<>(); 595 596 for (IIActorRef<?> actor : matchedActors) { 597 CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { 598 try { 599 ActionResult result = actor.callByActionName(method, args); 600 if (!result.isSuccess()) { 601 failures.put(actor.getName(), result.getResult()); 602 logger.warning(String.format("Failed on %s: %s", actor.getName(), result.getResult())); 603 // Record node failure in log store 604 if (logStore != null && sessionId >= 0) { 605 logStore.markNodeFailed(sessionId, actor.getName(), result.getResult()); 606 } 607 } else { 608 successCount.incrementAndGet(); 609 logger.fine(String.format("Applied to %s: %s", actor.getName(), result.getResult())); 610 // Record node success in log store 611 if (logStore != null && sessionId >= 0) { 612 logStore.markNodeSuccess(sessionId, actor.getName()); 613 } 614 } 615 } catch (Exception e) { 616 failures.put(actor.getName(), e.getMessage()); 617 logger.log(Level.WARNING, "Exception on " + actor.getName(), e); 618 } 619 }); 620 futures.add(future); 621 } 622 623 // Wait for all async tasks to complete 624 CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); 625 626 if (failures.isEmpty()) { 627 return new ActionResult(true, 628 String.format("Applied to %d actors", successCount.get())); 629 } else { 630 List<String> failureMessages = new ArrayList<>(); 631 failures.forEach((name, msg) -> failureMessages.add(name + ": " + msg)); 632 return new ActionResult(false, 633 String.format("Applied to %d/%d actors. Failures: %s", 634 successCount.get(), matchedActors.size(), String.join("; ", failureMessages))); 635 } 636 637 } catch (Exception e) { 638 logger.log(Level.SEVERE, "Error in apply: " + actionDef, e); 639 return new ActionResult(false, "Error: " + e.getMessage()); 640 } 641 } 642 643 /** 644 * Finds child actors matching a wildcard pattern. 645 * 646 * @param pattern the wildcard pattern (e.g., "node-*", "*-web", "*") 647 * @return list of matching child actors 648 */ 649 private List<IIActorRef<?>> findMatchingChildActors(String pattern) { 650 List<IIActorRef<?>> matched = new ArrayList<>(); 651 IIActorSystem system = (IIActorSystem) this.system(); 652 653 if (system == null) { 654 return matched; 655 } 656 657 List<String> childNames = new ArrayList<>(this.getNamesOfChildren()); 658 659 // Exact match (no wildcard) 660 if (!pattern.contains("*")) { 661 if (childNames.contains(pattern)) { 662 IIActorRef<?> actor = system.getIIActor(pattern); 663 if (actor != null) { 664 matched.add(actor); 665 } 666 } 667 return matched; 668 } 669 670 // Convert wildcard to regex 671 String regex = pattern 672 .replace(".", "\\.") 673 .replace("*", ".*"); 674 Pattern compiled = Pattern.compile(regex); 675 676 for (String childName : childNames) { 677 if (compiled.matcher(childName).matches()) { 678 IIActorRef<?> actor = system.getIIActor(childName); 679 if (actor != null) { 680 matched.add(actor); 681 } 682 } 683 } 684 685 return matched; 686 } 687 688 /** 689 * Executes a single command on all child node actors. 690 * 691 * @param command the command to execute 692 * @return list of results from each node 693 * @throws ExecutionException if command execution fails 694 * @throws InterruptedException if the operation is interrupted 695 */ 696 private List<String> executeCommandOnAllNodes(String command) 697 throws ExecutionException, InterruptedException { 698 699 IIActorSystem system = (IIActorSystem) this.system(); 700 List<String> results = new ArrayList<>(); 701 702 // Get all child node names 703 List<String> childNames = new ArrayList<>(this.getNamesOfChildren()); 704 705 logger.info(String.format("Executing command on %d nodes: %s", childNames.size(), command)); 706 707 // Execute on each child node 708 for (String childName : childNames) { 709 IIActorRef<?> actorRef = system.getIIActor(childName); 710 if (actorRef == null || !(actorRef instanceof NodeIIAR)) { 711 logger.warning(String.format("Child node actor not found or wrong type: %s", childName)); 712 continue; 713 } 714 NodeIIAR nodeActor = (NodeIIAR) actorRef; 715 716 // Execute the command 717 JSONArray commandArgs = new JSONArray(); 718 commandArgs.put(command); 719 ActionResult result = nodeActor.callByActionName("executeCommand", commandArgs.toString()); 720 721 results.add(String.format("%s: %s", childName, result.getResult())); 722 } 723 724 return results; 725 } 726 727 /** 728 * Extracts a single argument from JSON array format. 729 * 730 * @param arg the JSON array argument string 731 * @return the extracted argument 732 */ 733 private String extractSingleArgument(String arg) { 734 try { 735 JSONArray jsonArray = new JSONArray(arg); 736 if (jsonArray.length() == 0) { 737 throw new IllegalArgumentException("Arguments cannot be empty"); 738 } 739 return jsonArray.getString(0); 740 } catch (Exception e) { 741 throw new IllegalArgumentException( 742 "Invalid argument format. Expected JSON array: " + arg, e); 743 } 744 } 745 746 /** 747 * Gets the summary from the output multiplexer. 748 * 749 * <p>Retrieves the multiplexer actor from ActorSystem by name ("outputMultiplexer") 750 * and calls its getSummary action.</p> 751 * 752 * @return ActionResult with the summary or error 753 */ 754 private ActionResult getAccumulatorSummary() { 755 IIActorSystem sys = (IIActorSystem) this.system(); 756 IIActorRef<?> multiplexer = sys.getIIActor("outputMultiplexer"); 757 if (multiplexer == null) { 758 return new ActionResult(false, "No output multiplexer registered"); 759 } 760 ActionResult result = multiplexer.callByActionName("getSummary", ""); 761 return result; 762 } 763 764 /** 765 * Prints a summary of the current session's verification results. 766 * 767 * <p>Groups results by label (step) and displays a formatted table.</p> 768 * 769 * @return ActionResult with success status and summary text 770 */ 771 private ActionResult printSessionSummary() { 772 DistributedLogStore logStore = this.object.getLogStore(); 773 long sessionId = this.object.getSessionId(); 774 775 if (logStore == null || sessionId < 0) { 776 String msg = "Log store not available"; 777 System.out.println(msg); 778 return new ActionResult(false, msg); 779 } 780 781 // Get all logs for this session 782 List<com.scivicslab.actoriac.log.LogEntry> logs = logStore.getLogsByLevel(sessionId, 783 com.scivicslab.actoriac.log.LogLevel.DEBUG); 784 785 // Group logs by label and count results 786 java.util.Map<String, VerifyResult> resultsByLabel = new java.util.LinkedHashMap<>(); 787 788 for (com.scivicslab.actoriac.log.LogEntry entry : logs) { 789 String message = entry.getMessage(); 790 String label = entry.getLabel(); 791 if (message == null) continue; 792 793 // Extract label from the message if it contains step info 794 // Format: "- states: [...]\n label: xxx\n..." 795 if (label != null && label.contains("label:")) { 796 int idx = label.indexOf("label:"); 797 if (idx >= 0) { 798 String rest = label.substring(idx + 11).trim(); 799 int end = rest.indexOf('\n'); 800 label = end > 0 ? rest.substring(0, end).trim() : rest.trim(); 801 } 802 } 803 804 // Skip non-verify steps 805 if (label == null || !label.startsWith("verify-")) { 806 continue; 807 } 808 809 VerifyResult result = resultsByLabel.computeIfAbsent(label, k -> new VerifyResult()); 810 811 // Count occurrences in message 812 result.okCount += countOccurrences(message, "[OK]"); 813 result.warnCount += countOccurrences(message, "[WARN]"); 814 result.errorCount += countOccurrences(message, "[ERROR]"); 815 result.infoCount += countOccurrences(message, "[INFO]"); 816 817 // Extract special info (like document count, cluster health) 818 extractSpecialInfo(message, result); 819 } 820 821 // Build summary output 822 StringBuilder sb = new StringBuilder(); 823 sb.append("\n"); 824 sb.append("============================================================\n"); 825 sb.append(" VERIFICATION SUMMARY\n"); 826 sb.append("============================================================\n"); 827 sb.append("\n"); 828 sb.append(String.format("| %-35s | %-20s |\n", "Item", "Status")); 829 sb.append("|-------------------------------------|----------------------|\n"); 830 831 // Mapping from labels to display names and aggregation 832 String[][] mappings = { 833 {"verify-repos", "Document repositories"}, 834 {"verify-utility-cli", "Utility-cli"}, 835 {"verify-utility-sau3", "Utility-sau3"}, 836 {"verify-builds", "Docusaurus builds"}, 837 {"verify-public-html", "public_html deploy"}, 838 {"verify-apache", "Apache2 + UserDir"}, 839 {"verify-opensearch-install", "OpenSearch install"}, 840 {"verify-opensearch-running", "OpenSearch status"}, 841 {"verify-docusearch-build", "quarkus-docusearch build"}, 842 {"verify-docusearch-running", "quarkus-docusearch server"}, 843 {"verify-search-index", "Search index"}, 844 {"verify-web-access", "Web access"}, 845 }; 846 847 int totalOk = 0, totalWarn = 0, totalError = 0; 848 int executedChecks = 0; 849 List<String> errorDetails = new ArrayList<>(); 850 List<String> warnDetails = new ArrayList<>(); 851 852 for (String[] mapping : mappings) { 853 String label = mapping[0]; 854 String displayName = mapping[1]; 855 VerifyResult result = resultsByLabel.get(label); 856 857 if (result == null) { 858 sb.append(String.format("| %-35s | %-20s |\n", displayName, "SKIP")); 859 continue; 860 } 861 executedChecks++; 862 863 totalOk += result.okCount; 864 totalWarn += result.warnCount; 865 totalError += result.errorCount; 866 867 String status = formatStatus(result); 868 sb.append(String.format("| %-35s | %-20s |\n", displayName, status)); 869 870 // Collect error/warning details 871 if (result.errorCount > 0) { 872 errorDetails.add(displayName + ": " + result.errorCount + " error(s)"); 873 } 874 if (result.warnCount > 0) { 875 warnDetails.add(displayName + ": " + result.warnCount + " warning(s)"); 876 } 877 } 878 879 sb.append("|-------------------------------------|----------------------|\n"); 880 sb.append(String.format("| %-35s | %d OK, %d WARN, %d ERR |\n", 881 "TOTAL", totalOk, totalWarn, totalError)); 882 sb.append("============================================================\n"); 883 884 // Show error details if any 885 if (!errorDetails.isEmpty()) { 886 sb.append("\n--- Errors ---\n"); 887 for (String detail : errorDetails) { 888 sb.append(" * ").append(detail).append("\n"); 889 } 890 } 891 892 // Show warning details if any 893 if (!warnDetails.isEmpty()) { 894 sb.append("\n--- Warnings ---\n"); 895 for (String detail : warnDetails) { 896 sb.append(" * ").append(detail).append("\n"); 897 } 898 } 899 900 sb.append("\n"); 901 if (executedChecks == 0) { 902 sb.append("No verification checks were executed.\n"); 903 } else if (totalError == 0 && totalWarn == 0) { 904 sb.append("All checks passed!\n"); 905 } else if (totalError > 0) { 906 sb.append("To fix issues, run:\n"); 907 sb.append(" ./actor_iac.java --dir ./docu-search --workflow main-setup\n"); 908 } 909 910 String summary = sb.toString(); 911 System.out.println(summary); 912 return new ActionResult(true, summary); 913 } 914 915 /** 916 * Formats the status string for a verification result. 917 */ 918 private String formatStatus(VerifyResult result) { 919 if (result.errorCount > 0) { 920 if (result.okCount > 0) { 921 return String.format("%d OK, %d ERROR", result.okCount, result.errorCount); 922 } 923 return "ERROR"; 924 } 925 if (result.warnCount > 0) { 926 if (result.okCount > 0) { 927 return String.format("%d OK, %d WARN", result.okCount, result.warnCount); 928 } 929 return "WARN"; 930 } 931 if (result.okCount > 0) { 932 String extra = result.extraInfo != null ? " " + result.extraInfo : ""; 933 return result.okCount + " OK" + extra; 934 } 935 return "OK"; 936 } 937 938 /** 939 * Extracts special information from log messages (like document count, cluster health). 940 */ 941 private void extractSpecialInfo(String message, VerifyResult result) { 942 // Extract document count from search index 943 if (message.contains("documents")) { 944 java.util.regex.Matcher m = java.util.regex.Pattern.compile("(\\d+)\\s+documents").matcher(message); 945 if (m.find()) { 946 result.extraInfo = "(" + m.group(1) + " docs)"; 947 } 948 } 949 // Extract cluster health 950 if (message.contains("Cluster health:")) { 951 java.util.regex.Matcher m = java.util.regex.Pattern.compile("Cluster health:\\s*(\\w+)").matcher(message); 952 if (m.find()) { 953 result.extraInfo = "(" + m.group(1) + ")"; 954 } 955 } 956 // Extract web access count 957 if (message.contains("Accessible at")) { 958 // Count from "X / Y" pattern is handled by OK count 959 } 960 } 961 962 /** 963 * Counts occurrences of a substring in a string. 964 */ 965 private int countOccurrences(String text, String sub) { 966 int count = 0; 967 int idx = 0; 968 while ((idx = text.indexOf(sub, idx)) != -1) { 969 count++; 970 idx += sub.length(); 971 } 972 return count; 973 } 974 975 /** 976 * Helper class to hold verification results for a step. 977 */ 978 private static class VerifyResult { 979 int okCount = 0; 980 int warnCount = 0; 981 int errorCount = 0; 982 int infoCount = 0; 983 String extraInfo = null; 984 } 985 986}