diff --git a/connpy/cli/help_text.py b/connpy/cli/help_text.py index 3afabd7..3096bac 100644 --- a/connpy/cli/help_text.py +++ b/connpy/cli/help_text.py @@ -153,9 +153,7 @@ tasks: nodes: #List of nodes to work on. Mandatory - 'router1@office' #You can add specific nodes - '@aws' #entire folders or subfolders - - '@office': #or filter inside a folder or subfolder - - 'router2' - - 'router7' + - 'router.*@office' #or use regex to filter inside a folder commands: #List of commands to send, use {name} to pass variables - 'term len 0' @@ -181,7 +179,7 @@ tasks: vrouterN@aws: id: 5 - output: /home/user/logs #Type of output, if null you only get Connection and test result. Choices are: null,stdout,/path/to/folder. Folder path only works on 'run' action. + output: /home/user/logs #Type of output, if null you only get Connection and test result. Choices are: null,stdout,/path/to/folder. Folder path works on both 'run' and 'test' actions. options: prompt: r'>$|#$|\$$|>.$|#.$|\$.$' #Optional prompt to check on your devices, default should work on most devices. @@ -193,9 +191,6 @@ tasks: nodes: - 'router1@office' - '@aws' - - '@office': - - 'router2' - - 'router7' commands: - 'ping 10.100.100.{id}' expected: '!' #Expected text to find when running test action. Mandatory for 'test' diff --git a/connpy/cli/run_handler.py b/connpy/cli/run_handler.py index 7f88b15..1fd7b1f 100644 --- a/connpy/cli/run_handler.py +++ b/connpy/cli/run_handler.py @@ -1,6 +1,7 @@ import os import sys import yaml +import threading from rich.rule import Rule from .. import printer from ..services.exceptions import ConnpyError @@ -9,6 +10,7 @@ from .help_text import get_instructions class RunHandler: def __init__(self, app): self.app = app + self.print_lock = threading.Lock() def dispatch(self, args): if len(args.data) > 1: @@ -19,22 +21,43 @@ class RunHandler: def node_run(self, args): nodes_filter = args.data[0] commands = [" ".join(args.data[1:])] - + try: header_printed = False - # Inline execution with streaming results - def _on_node_complete(unique, node_output, node_status): - nonlocal header_printed - if not header_printed: - printer.console.print(Rule("OUTPUT", style="header")) - header_printed = True - printer.node_panel(unique, node_output, node_status) - - self.app.services.execution.run_commands( - nodes_filter=nodes_filter, - commands=commands, - on_node_complete=_on_node_complete - ) + + if hasattr(args, 'test_expected') and args.test_expected: + # Mode: Test + def _on_node_complete(unique, node_output, node_status, node_result): + nonlocal header_printed + with self.print_lock: + if not header_printed: + printer.console.print(Rule("OUTPUT", style="header")) + header_printed = True + printer.test_panel(unique, node_output, node_status, node_result) + + results = self.app.services.execution.test_commands( + nodes_filter=nodes_filter, + commands=commands, + expected=args.test_expected, + on_node_complete=_on_node_complete + ) + printer.test_summary(results) + else: + # Mode: Normal Run + def _on_node_complete(unique, node_output, node_status): + nonlocal header_printed + with self.print_lock: + if not header_printed: + printer.console.print(Rule("OUTPUT", style="header")) + header_printed = True + printer.node_panel(unique, node_output, node_status) + + results = self.app.services.execution.run_commands( + nodes_filter=nodes_filter, + commands=commands, + on_node_complete=_on_node_complete + ) + printer.run_summary(results) except ConnpyError as e: printer.error(str(e)) @@ -55,36 +78,44 @@ class RunHandler: try: with open(path, "r") as f: playbook = yaml.load(f, Loader=yaml.FullLoader) - + for task in playbook.get("tasks", []): self.cli_run(task) - + except Exception as e: printer.error(f"Failed to run playbook {path}: {e}") sys.exit(10) def cli_run(self, script): + name = script.get("name", "Task") try: action = script["action"] nodelist = script["nodes"] commands = script["commands"] variables = script.get("variables") output_cfg = script["output"] - name = script.get("name", "Task") options = script.get("options", {}) except KeyError as e: - printer.error(f"'{e.args[0]}' is mandatory in script") + printer.error(f"[{name}] '{e.args[0]}' is mandatory in script") sys.exit(11) stdout = (output_cfg == "stdout") folder = output_cfg if output_cfg not in [None, "stdout"] else None prompt = options.get("prompt") - printer.header(name.upper()) - + try: + header_printed = False if action == "run": # If stdout is true, we stream results as they arrive - on_complete = printer.node_panel if stdout else None + def _on_run_complete(unique, node_output, node_status): + nonlocal header_printed + if stdout: + with self.print_lock: + if not header_printed: + printer.console.print(Rule(name.upper(), style="header")) + header_printed = True + printer.node_panel(unique, node_output, node_status) + results = self.app.services.execution.run_commands( nodes_filter=nodelist, commands=commands, @@ -93,16 +124,31 @@ class RunHandler: timeout=options.get("timeout", 10), folder=folder, prompt=prompt, - on_node_complete=on_complete + on_node_complete=_on_run_complete ) - # If not streaming, we could print a summary table here if needed - if not stdout: - for unique, output in results.items(): + # Final Summary + if not stdout and not folder: + with self.print_lock: + printer.console.print(Rule(name.upper(), style="header")) + for unique, data in results.items(): + output = data["output"] if isinstance(data, dict) else data printer.node_panel(unique, output, 0) - + + # ALWAYS show the aggregate execution summary at the end + printer.run_summary(results) + elif action == "test": expected = script.get("expected", []) - on_complete = printer.test_panel if stdout else None + # Show test_panel per node ONLY if stdout is True + def _on_test_complete(unique, node_output, node_status, node_result): + nonlocal header_printed + if stdout: + with self.print_lock: + if not header_printed: + printer.console.print(Rule(name.upper(), style="header")) + header_printed = True + printer.test_panel(unique, node_output, node_status, node_result) + results = self.app.services.execution.test_commands( nodes_filter=nodelist, commands=commands, @@ -110,11 +156,12 @@ class RunHandler: variables=variables, parallel=options.get("parallel", 10), timeout=options.get("timeout", 10), + folder=folder, prompt=prompt, - on_node_complete=on_complete + on_node_complete=_on_test_complete ) - if not stdout: - printer.test_summary(results) - + # ALWAYS show the aggregate summary at the end + printer.test_summary(results) + except ConnpyError as e: printer.error(str(e)) diff --git a/connpy/completion.py b/connpy/completion.py index 6dbc061..e436705 100755 --- a/connpy/completion.py +++ b/connpy/completion.py @@ -147,12 +147,27 @@ def _build_tree(nodes, folders, profiles, plugins, configdir): "__extra__": lambda w: get_cwd(w, "import") }) - run_dict = {"--generate": None, "--help": None, "-g": None, "-h": None} - run_dict.update({ - "*": run_dict, - "__extra__": lambda w: get_cwd(w, "run") + list(nodes) + # --- Run Loop --- + # After the first positional argument (Node filter or YAML file), + # we stop suggesting nodes and only allow flags or commands. + run_after_node = {"--help": None, "-h": None} + run_after_node.update({ + "--test": {"*": run_after_node}, + "-t": {"*": run_after_node}, + "*": run_after_node # Consume commands }) + run_dict = { + "--generate": {"__extra__": lambda w: get_cwd(w, "--generate")}, + "-g": {"__extra__": lambda w: get_cwd(w, "-g")}, + "--test": {"*": None}, + "-t": {"*": None}, + "--help": None, + "-h": None, + "__extra__": lambda w: get_cwd(w, "run") + list(nodes), + "*": run_after_node + } + # State Machine Definitions ai_dict = {"__exclude_used__": True, "--help": None, "-h": None} for opt in ["--engineer-model", "--engineer-api-key", "--architect-model", "--architect-api-key"]: diff --git a/connpy/configfile.py b/connpy/configfile.py index 673f93a..b408f18 100755 --- a/connpy/configfile.py +++ b/connpy/configfile.py @@ -400,15 +400,7 @@ class configfile: if isinstance(uniques, str): uniques = [uniques] for i in uniques: - if isinstance(i, dict): - name = list(i.keys())[0] - mylist = i[name] - if not self.config["case"]: - name = name.lower() - mylist = [item.lower() for item in mylist] - this = self.getitem(name, mylist, extract = extract) - nodes.update(this) - elif i.startswith("@"): + if i.startswith("@"): if not self.config["case"]: i = i.lower() this = self.getitem(i, extract = extract) @@ -487,13 +479,17 @@ class configfile: layer3 = [k + "@" + s + "@" + f for k,v in self.connections[f][s].items() if isinstance(v, dict) and v.get("type") == "connection"] nodes.extend(layer3) if filter: + flat_filter = [] if isinstance(filter, str): - nodes = [item for item in nodes if re.search(filter, item)] + flat_filter = [filter] elif isinstance(filter, list): - nodes = [item for item in nodes if any(re.search(pattern, item) for pattern in filter)] + for item in filter: + if isinstance(item, str): + flat_filter.append(item) else: - printer.error("Invalid filter: must be a string or a list of strings.") + printer.error("Filter must be a string or a list of strings") sys.exit(1) + nodes = [item for item in nodes if any(re.search(pattern, item) for pattern in flat_filter)] return nodes @MethodHook @@ -511,15 +507,18 @@ class configfile: layer3 = {k + "@" + s + "@" + f:v for k,v in self.connections[f][s].items() if isinstance(v, dict) and v.get("type") == "connection"} nodes.update(layer3) if filter: + flat_filter = [] if isinstance(filter, str): - filter = "^(?!.*@).+$" if filter == "@" else filter - nodes = {k: v for k, v in nodes.items() if re.search(filter, k)} + flat_filter = [filter] elif isinstance(filter, list): - filter = ["^(?!.*@).+$" if item == "@" else item for item in filter] - nodes = {k: v for k, v in nodes.items() if any(re.search(pattern, k) for pattern in filter)} + for item in filter: + if isinstance(item, str): + flat_filter.append(item) else: - printer.error("Invalid filter: must be a string or a list of strings.") + printer.error("Filter must be a string or a list of strings") sys.exit(1) + flat_filter = ["^(?!.*@).+$" if item == "@" else item for item in flat_filter] + nodes = {k: v for k, v in nodes.items() if any(re.search(pattern, k) for pattern in flat_filter)} if extract: for node, keys in nodes.items(): for key, value in keys.items(): diff --git a/connpy/connapp.py b/connpy/connapp.py index 0391d50..0f53132 100755 --- a/connpy/connapp.py +++ b/connpy/connapp.py @@ -289,6 +289,7 @@ class connapp: runparser = subparsers.add_parser("run", help="Run scripts or commands on nodes", description="Run scripts or commands on nodes", formatter_class=RichHelpFormatter) runparser.error = self._custom_error runparser.add_argument("run", nargs='+', action=self._store_type, help=get_help("run"), default="run").completer = nodes_completer + runparser.add_argument("-t", "--test", dest="test_expected", nargs='+', help="Expected text(s) to validate in output. Converts the action from 'run' to 'test'") runparser.add_argument("-g","--generate", dest="action", action="store_const", help="Generate yaml file template", const="generate", default="run") runparser.set_defaults(func=self._run.dispatch) #APIPARSER diff --git a/connpy/core.py b/connpy/core.py index 5efaebb..ef76328 100755 --- a/connpy/core.py +++ b/connpy/core.py @@ -148,6 +148,10 @@ class node: self.jumphost = f"-o ProxyCommand=\"{jumphost_cmd}\"" else: self.jumphost = "" + + self.output = "" + self.status = 1 + self.result = {} @MethodHook def _passtx(self, passwords, *, keyfile=None): @@ -548,7 +552,12 @@ class node: self.child.logfile_read = self.mylog for c in commands: if vars is not None: - c = c.format(**vars) + try: + c = c.format(**vars) + except KeyError as e: + self.output = f"Error: Variable {e} not defined in task or inventory" + self.status = 1 + return self.output result = self.child.expect(expects, timeout = timeout) self.child.sendline(c) if result == 2: @@ -582,7 +591,7 @@ class node: return connect @MethodHook - def test(self, commands, expected, vars = None,*, prompt = r'>$|#$|\$$|>.$|#.$|\$.$', timeout = 10, logger = None): + def test(self, commands, expected, vars = None,*, folder = '', prompt = r'>$|#$|\$$|>.$|#.$|\$.$', timeout = 10, logger = None): ''' Run a command or list of commands on the node, then check if expected value appears on the output after the last command. @@ -608,6 +617,9 @@ class node: ### Optional Named Parameters: + - folder (str): Path where output log should be stored, leave + empty to not store logs. + - prompt (str): Prompt to be expected after a command is finished running. Usually linux uses ">" or EOF while routers use ">" or "#". The default value should @@ -622,6 +634,7 @@ class node: false if prompt is found before. ''' + now = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") connect = self._connect(timeout = timeout, logger = logger) if connect == True: if logger: @@ -639,6 +652,7 @@ class node: if "prompt" in self.tags: prompt = self.tags["prompt"] expects = [prompt, pexpect.EOF, pexpect.TIMEOUT] + output = '' if not isinstance(commands, list): commands = [commands] @@ -650,7 +664,12 @@ class node: self.child.logfile_read = self.mylog for c in commands: if vars is not None: - c = c.format(**vars) + try: + c = c.format(**vars) + except KeyError as e: + self.output = f"Error: Variable {e} not defined in task or inventory" + self.status = 1 + return self.output result = self.child.expect(expects, timeout = timeout) self.child.sendline(c) if result == 2: @@ -659,6 +678,12 @@ class node: result = self.child.expect(expects, timeout = timeout) self.child.close() output = self._logclean(self.mylog.getvalue().decode(), True) + if logger: + logger("output", output) + if folder != '': + with open(folder + "/" + self.unique + "_" + now + ".txt", "w") as f: + f.write(output) + f.close() self.output = output if result in [0, 1]: # lastcommand = commands[-1] @@ -1020,8 +1045,15 @@ class nodes: nodesargs[n.unique]["vars"] = {} if "__global__" in vars.keys(): nodesargs[n.unique]["vars"].update(vars["__global__"]) - if n.unique in vars.keys(): - nodesargs[n.unique]["vars"].update(vars[n.unique]) + for var_key, var_val in vars.items(): + if var_key == "__global__": + continue + try: + if re.search(var_key, n.unique, re.IGNORECASE): + nodesargs[n.unique]["vars"].update(var_val) + except re.error: + if var_key == n.unique: + nodesargs[n.unique]["vars"].update(var_val) # Pass the logger to the node nodesargs[n.unique]["logger"] = logger @@ -1046,7 +1078,7 @@ class nodes: return output @MethodHook - def test(self, commands, expected, vars = None,*, prompt = None, parallel = 10, timeout = None, on_complete = None, logger = None): + def test(self, commands, expected, vars = None,*, folder = None, prompt = None, parallel = 10, timeout = None, on_complete = None, logger = None): ''' Run a command or list of commands on all the nodes in nodelist, then check if expected value appears on the output after the last command. @@ -1101,6 +1133,9 @@ class nodes: nodesargs = {} args["commands"] = commands args["expected"] = expected + if folder != None: + args["folder"] = folder + Path(folder).mkdir(parents=True, exist_ok=True) if prompt != None: args["prompt"] = prompt if timeout != None: @@ -1122,8 +1157,15 @@ class nodes: nodesargs[n.unique]["vars"] = {} if "__global__" in vars.keys(): nodesargs[n.unique]["vars"].update(vars["__global__"]) - if n.unique in vars.keys(): - nodesargs[n.unique]["vars"].update(vars[n.unique]) + for var_key, var_val in vars.items(): + if var_key == "__global__": + continue + try: + if re.search(var_key, n.unique, re.IGNORECASE): + nodesargs[n.unique]["vars"].update(var_val) + except re.error: + if var_key == n.unique: + nodesargs[n.unique]["vars"].update(var_val) nodesargs[n.unique]["logger"] = logger if on_complete: diff --git a/connpy/grpc_layer/connpy_pb2.py b/connpy/grpc_layer/connpy_pb2.py index ef8dbeb..3cb95c2 100644 --- a/connpy/grpc_layer/connpy_pb2.py +++ b/connpy/grpc_layer/connpy_pb2.py @@ -26,7 +26,7 @@ from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63onnpy.proto\x12\x06\x63onnpy\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1bgoogle/protobuf/empty.proto\"\x8a\x01\n\x0fInteractRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04sftp\x18\x02 \x01(\x08\x12\r\n\x05\x64\x65\x62ug\x18\x03 \x01(\x08\x12\x12\n\nstdin_data\x18\x04 \x01(\x0c\x12\x0c\n\x04\x63ols\x18\x05 \x01(\x05\x12\x0c\n\x04rows\x18\x06 \x01(\x05\x12\x1e\n\x16\x63onnection_params_json\x18\x07 \x01(\t\"O\n\x10InteractResponse\x12\x13\n\x0bstdout_data\x18\x01 \x01(\x0c\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x15\n\rerror_message\x18\x03 \x01(\t\"7\n\rFilterRequest\x12\x12\n\nfilter_str\x18\x01 \x01(\t\x12\x12\n\nformat_str\x18\x02 \x01(\t\"5\n\rValueResponse\x12$\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x16.google.protobuf.Value\"\x17\n\tIdRequest\x12\n\n\x02id\x18\x01 \x01(\t\"S\n\x0bNodeRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12%\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x11\n\tis_folder\x18\x03 \x01(\x08\".\n\rDeleteRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tis_folder\x18\x02 \x01(\x08\"\x1d\n\x0cMessageValue\x12\r\n\x05value\x18\x01 \x01(\t\";\n\x0bMoveRequest\x12\x0e\n\x06src_id\x18\x01 \x01(\t\x12\x0e\n\x06\x64st_id\x18\x02 \x01(\t\x12\x0c\n\x04\x63opy\x18\x03 \x01(\x08\"W\n\x0b\x42ulkRequest\x12\x0b\n\x03ids\x18\x01 \x03(\t\x12\r\n\x05hosts\x18\x02 \x03(\t\x12,\n\x0b\x63ommon_data\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\"7\n\x0eStructResponse\x12%\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\"/\n\x0eProfileRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07resolve\x18\x02 \x01(\x08\"6\n\rStructRequest\x12%\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\"\x1e\n\rStringRequest\x12\r\n\x05value\x18\x01 \x01(\t\"\x1f\n\x0eStringResponse\x12\r\n\x05value\x18\x01 \x01(\t\"C\n\rUpdateRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value\"B\n\rPluginRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0bsource_file\x18\x02 \x01(\t\x12\x0e\n\x06update\x18\x03 \x01(\x08\"\x86\x01\n\nRunRequest\x12\r\n\x05nodes\x18\x01 \x03(\t\x12\x10\n\x08\x63ommands\x18\x02 \x03(\t\x12\x0e\n\x06\x66older\x18\x03 \x01(\t\x12\x0e\n\x06prompt\x18\x04 \x01(\t\x12\x10\n\x08parallel\x18\x05 \x01(\x05\x12%\n\x04vars\x18\x06 \x01(\x0b\x32\x17.google.protobuf.Struct\"\x99\x01\n\x0bTestRequest\x12\r\n\x05nodes\x18\x01 \x03(\t\x12\x10\n\x08\x63ommands\x18\x02 \x03(\t\x12\x10\n\x08\x65xpected\x18\x03 \x01(\t\x12\x0e\n\x06\x66older\x18\x04 \x01(\t\x12\x0e\n\x06prompt\x18\x05 \x01(\t\x12\x10\n\x08parallel\x18\x06 \x01(\x05\x12%\n\x04vars\x18\x07 \x01(\x0b\x32\x17.google.protobuf.Struct\"A\n\rScriptRequest\x12\x0e\n\x06param1\x18\x01 \x01(\t\x12\x0e\n\x06param2\x18\x02 \x01(\t\x12\x10\n\x08parallel\x18\x03 \x01(\x05\"3\n\rExportRequest\x12\x11\n\tfile_path\x18\x01 \x01(\t\x12\x0f\n\x07\x66olders\x18\x02 \x03(\t\"\x1c\n\x0bListRequest\x12\r\n\x05items\x18\x01 \x03(\t\"\xa6\x02\n\nAskRequest\x12\x12\n\ninput_text\x18\x01 \x01(\t\x12\x0e\n\x06\x64ryrun\x18\x02 \x01(\x08\x12,\n\x0c\x63hat_history\x18\x03 \x01(\x0b\x32\x16.google.protobuf.Value\x12\x12\n\nsession_id\x18\x04 \x01(\t\x12\r\n\x05\x64\x65\x62ug\x18\x05 \x01(\x08\x12\x16\n\x0e\x65ngineer_model\x18\x06 \x01(\t\x12\x18\n\x10\x65ngineer_api_key\x18\x07 \x01(\t\x12\x17\n\x0f\x61rchitect_model\x18\x08 \x01(\t\x12\x19\n\x11\x61rchitect_api_key\x18\t \x01(\t\x12\r\n\x05trust\x18\n \x01(\x08\x12\x1b\n\x13\x63onfirmation_answer\x18\x0b \x01(\t\x12\x11\n\tinterrupt\x18\x0c \x01(\x08\"\xc8\x01\n\nAIResponse\x12\x12\n\ntext_chunk\x18\x01 \x01(\t\x12\x10\n\x08is_final\x18\x02 \x01(\x08\x12,\n\x0b\x66ull_result\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x15\n\rstatus_update\x18\x04 \x01(\t\x12\x15\n\rdebug_message\x18\x05 \x01(\t\x12\x1d\n\x15requires_confirmation\x18\x06 \x01(\x08\x12\x19\n\x11important_message\x18\x07 \x01(\t\"\x1d\n\x0c\x42oolResponse\x12\r\n\x05value\x18\x01 \x01(\x08\"C\n\x0fProviderRequest\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\r\n\x05model\x18\x02 \x01(\t\x12\x0f\n\x07\x61pi_key\x18\x03 \x01(\t\"\x1b\n\nIntRequest\x12\r\n\x05value\x18\x01 \x01(\x05\"p\n\rNodeRunResult\x12\x11\n\tunique_id\x18\x01 \x01(\t\x12\x0e\n\x06output\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x05\x12,\n\x0btest_result\x18\x04 \x01(\x0b\x32\x17.google.protobuf.Struct\"m\n\x12\x46ullReplaceRequest\x12,\n\x0b\x63onnections\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12)\n\x08profiles\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct2\xe1\x07\n\x0bNodeService\x12<\n\nlist_nodes\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12>\n\x0clist_folders\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12?\n\x10get_node_details\x12\x11.connpy.IdRequest\x1a\x16.connpy.StructResponse\"\x00\x12<\n\x0e\x65xplode_unique\x12\x11.connpy.IdRequest\x1a\x15.connpy.ValueResponse\"\x00\x12\x42\n\x0egenerate_cache\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\x08\x61\x64\x64_node\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\x0bupdate_node\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12>\n\x0b\x64\x65lete_node\x12\x15.connpy.DeleteRequest\x1a\x16.google.protobuf.Empty\"\x00\x12:\n\tmove_node\x12\x13.connpy.MoveRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\x08\x62ulk_add\x12\x13.connpy.BulkRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x45\n\x16validate_parent_folder\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x12set_reserved_names\x12\x13.connpy.ListRequest\x1a\x16.google.protobuf.Empty\"\x00\x12H\n\rinteract_node\x12\x17.connpy.InteractRequest\x1a\x18.connpy.InteractResponse\"\x00(\x01\x30\x01\x12\x44\n\x0c\x66ull_replace\x12\x1a.connpy.FullReplaceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x45\n\rget_inventory\x12\x16.google.protobuf.Empty\x1a\x1a.connpy.FullReplaceRequest\"\x00\x32\x96\x03\n\x0eProfileService\x12?\n\rlist_profiles\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12?\n\x0bget_profile\x12\x16.connpy.ProfileRequest\x1a\x16.connpy.StructResponse\"\x00\x12<\n\x0b\x61\x64\x64_profile\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x44\n\x11resolve_node_data\x12\x15.connpy.StructRequest\x1a\x16.connpy.StructResponse\"\x00\x12=\n\x0e\x64\x65lete_profile\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12?\n\x0eupdate_profile\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\xae\x03\n\rConfigService\x12@\n\x0cget_settings\x12\x16.google.protobuf.Empty\x1a\x16.connpy.StructResponse\"\x00\x12\x43\n\x0fget_default_dir\x12\x16.google.protobuf.Empty\x1a\x16.connpy.StringResponse\"\x00\x12\x44\n\x11set_config_folder\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x41\n\x0eupdate_setting\x12\x15.connpy.UpdateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x10\x65ncrypt_password\x12\x15.connpy.StringRequest\x1a\x16.connpy.StringResponse\"\x00\x12H\n\x15\x61pply_theme_from_file\x12\x15.connpy.StringRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xca\x02\n\rPluginService\x12?\n\x0clist_plugins\x12\x16.google.protobuf.Empty\x1a\x15.connpy.ValueResponse\"\x00\x12=\n\nadd_plugin\x12\x15.connpy.PluginRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\rdelete_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\renable_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12=\n\x0e\x64isable_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\x9b\x02\n\x10\x45xecutionService\x12=\n\x0crun_commands\x12\x12.connpy.RunRequest\x1a\x15.connpy.NodeRunResult\"\x00\x30\x01\x12?\n\rtest_commands\x12\x13.connpy.TestRequest\x1a\x15.connpy.NodeRunResult\"\x00\x30\x01\x12\x41\n\x0erun_cli_script\x12\x15.connpy.ScriptRequest\x1a\x16.connpy.StructResponse\"\x00\x12\x44\n\x11run_yaml_playbook\x12\x15.connpy.ScriptRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xe2\x01\n\x13ImportExportService\x12\x41\n\x0e\x65xport_to_file\x12\x15.connpy.ExportRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x10import_from_file\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x12set_reserved_names\x12\x13.connpy.ListRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\x8e\x03\n\tAIService\x12\x33\n\x03\x61sk\x12\x12.connpy.AskRequest\x1a\x12.connpy.AIResponse\"\x00(\x01\x30\x01\x12\x38\n\x07\x63onfirm\x12\x15.connpy.StringRequest\x1a\x14.connpy.BoolResponse\"\x00\x12@\n\rlist_sessions\x12\x16.google.protobuf.Empty\x1a\x15.connpy.ValueResponse\"\x00\x12\x41\n\x0e\x64\x65lete_session\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n\x12\x63onfigure_provider\x12\x17.connpy.ProviderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x44\n\x11load_session_data\x12\x15.connpy.StringRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xc2\x02\n\rSystemService\x12\x39\n\tstart_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\tdebug_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\x08stop_api\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12;\n\x0brestart_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12@\n\x0eget_api_status\x12\x16.google.protobuf.Empty\x1a\x14.connpy.BoolResponse\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63onnpy.proto\x12\x06\x63onnpy\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1bgoogle/protobuf/empty.proto\"\x8a\x01\n\x0fInteractRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04sftp\x18\x02 \x01(\x08\x12\r\n\x05\x64\x65\x62ug\x18\x03 \x01(\x08\x12\x12\n\nstdin_data\x18\x04 \x01(\x0c\x12\x0c\n\x04\x63ols\x18\x05 \x01(\x05\x12\x0c\n\x04rows\x18\x06 \x01(\x05\x12\x1e\n\x16\x63onnection_params_json\x18\x07 \x01(\t\"O\n\x10InteractResponse\x12\x13\n\x0bstdout_data\x18\x01 \x01(\x0c\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x15\n\rerror_message\x18\x03 \x01(\t\"7\n\rFilterRequest\x12\x12\n\nfilter_str\x18\x01 \x01(\t\x12\x12\n\nformat_str\x18\x02 \x01(\t\"5\n\rValueResponse\x12$\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x16.google.protobuf.Value\"\x17\n\tIdRequest\x12\n\n\x02id\x18\x01 \x01(\t\"S\n\x0bNodeRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12%\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x11\n\tis_folder\x18\x03 \x01(\x08\".\n\rDeleteRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tis_folder\x18\x02 \x01(\x08\"\x1d\n\x0cMessageValue\x12\r\n\x05value\x18\x01 \x01(\t\";\n\x0bMoveRequest\x12\x0e\n\x06src_id\x18\x01 \x01(\t\x12\x0e\n\x06\x64st_id\x18\x02 \x01(\t\x12\x0c\n\x04\x63opy\x18\x03 \x01(\x08\"W\n\x0b\x42ulkRequest\x12\x0b\n\x03ids\x18\x01 \x03(\t\x12\r\n\x05hosts\x18\x02 \x03(\t\x12,\n\x0b\x63ommon_data\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\"7\n\x0eStructResponse\x12%\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\"/\n\x0eProfileRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07resolve\x18\x02 \x01(\x08\"6\n\rStructRequest\x12%\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\"\x1e\n\rStringRequest\x12\r\n\x05value\x18\x01 \x01(\t\"\x1f\n\x0eStringResponse\x12\r\n\x05value\x18\x01 \x01(\t\"C\n\rUpdateRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value\"B\n\rPluginRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0bsource_file\x18\x02 \x01(\t\x12\x0e\n\x06update\x18\x03 \x01(\x08\"\xa5\x01\n\nRunRequest\x12\r\n\x05nodes\x18\x01 \x03(\t\x12\x10\n\x08\x63ommands\x18\x02 \x03(\t\x12\x0e\n\x06\x66older\x18\x03 \x01(\t\x12\x0e\n\x06prompt\x18\x04 \x01(\t\x12\x10\n\x08parallel\x18\x05 \x01(\x05\x12%\n\x04vars\x18\x06 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x0f\n\x07timeout\x18\x07 \x01(\x05\x12\x0c\n\x04name\x18\x08 \x01(\t\"\xb8\x01\n\x0bTestRequest\x12\r\n\x05nodes\x18\x01 \x03(\t\x12\x10\n\x08\x63ommands\x18\x02 \x03(\t\x12\x10\n\x08\x65xpected\x18\x03 \x03(\t\x12\x0e\n\x06\x66older\x18\x04 \x01(\t\x12\x0e\n\x06prompt\x18\x05 \x01(\t\x12\x10\n\x08parallel\x18\x06 \x01(\x05\x12%\n\x04vars\x18\x07 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x0f\n\x07timeout\x18\x08 \x01(\x05\x12\x0c\n\x04name\x18\t \x01(\t\"A\n\rScriptRequest\x12\x0e\n\x06param1\x18\x01 \x01(\t\x12\x0e\n\x06param2\x18\x02 \x01(\t\x12\x10\n\x08parallel\x18\x03 \x01(\x05\"3\n\rExportRequest\x12\x11\n\tfile_path\x18\x01 \x01(\t\x12\x0f\n\x07\x66olders\x18\x02 \x03(\t\"\x1c\n\x0bListRequest\x12\r\n\x05items\x18\x01 \x03(\t\"\xa6\x02\n\nAskRequest\x12\x12\n\ninput_text\x18\x01 \x01(\t\x12\x0e\n\x06\x64ryrun\x18\x02 \x01(\x08\x12,\n\x0c\x63hat_history\x18\x03 \x01(\x0b\x32\x16.google.protobuf.Value\x12\x12\n\nsession_id\x18\x04 \x01(\t\x12\r\n\x05\x64\x65\x62ug\x18\x05 \x01(\x08\x12\x16\n\x0e\x65ngineer_model\x18\x06 \x01(\t\x12\x18\n\x10\x65ngineer_api_key\x18\x07 \x01(\t\x12\x17\n\x0f\x61rchitect_model\x18\x08 \x01(\t\x12\x19\n\x11\x61rchitect_api_key\x18\t \x01(\t\x12\r\n\x05trust\x18\n \x01(\x08\x12\x1b\n\x13\x63onfirmation_answer\x18\x0b \x01(\t\x12\x11\n\tinterrupt\x18\x0c \x01(\x08\"\xc8\x01\n\nAIResponse\x12\x12\n\ntext_chunk\x18\x01 \x01(\t\x12\x10\n\x08is_final\x18\x02 \x01(\x08\x12,\n\x0b\x66ull_result\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x15\n\rstatus_update\x18\x04 \x01(\t\x12\x15\n\rdebug_message\x18\x05 \x01(\t\x12\x1d\n\x15requires_confirmation\x18\x06 \x01(\x08\x12\x19\n\x11important_message\x18\x07 \x01(\t\"\x1d\n\x0c\x42oolResponse\x12\r\n\x05value\x18\x01 \x01(\x08\"C\n\x0fProviderRequest\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\r\n\x05model\x18\x02 \x01(\t\x12\x0f\n\x07\x61pi_key\x18\x03 \x01(\t\"\x1b\n\nIntRequest\x12\r\n\x05value\x18\x01 \x01(\x05\"p\n\rNodeRunResult\x12\x11\n\tunique_id\x18\x01 \x01(\t\x12\x0e\n\x06output\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x05\x12,\n\x0btest_result\x18\x04 \x01(\x0b\x32\x17.google.protobuf.Struct\"m\n\x12\x46ullReplaceRequest\x12,\n\x0b\x63onnections\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12)\n\x08profiles\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct2\xe1\x07\n\x0bNodeService\x12<\n\nlist_nodes\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12>\n\x0clist_folders\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12?\n\x10get_node_details\x12\x11.connpy.IdRequest\x1a\x16.connpy.StructResponse\"\x00\x12<\n\x0e\x65xplode_unique\x12\x11.connpy.IdRequest\x1a\x15.connpy.ValueResponse\"\x00\x12\x42\n\x0egenerate_cache\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\x08\x61\x64\x64_node\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\x0bupdate_node\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12>\n\x0b\x64\x65lete_node\x12\x15.connpy.DeleteRequest\x1a\x16.google.protobuf.Empty\"\x00\x12:\n\tmove_node\x12\x13.connpy.MoveRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\x08\x62ulk_add\x12\x13.connpy.BulkRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x45\n\x16validate_parent_folder\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x12set_reserved_names\x12\x13.connpy.ListRequest\x1a\x16.google.protobuf.Empty\"\x00\x12H\n\rinteract_node\x12\x17.connpy.InteractRequest\x1a\x18.connpy.InteractResponse\"\x00(\x01\x30\x01\x12\x44\n\x0c\x66ull_replace\x12\x1a.connpy.FullReplaceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x45\n\rget_inventory\x12\x16.google.protobuf.Empty\x1a\x1a.connpy.FullReplaceRequest\"\x00\x32\x96\x03\n\x0eProfileService\x12?\n\rlist_profiles\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12?\n\x0bget_profile\x12\x16.connpy.ProfileRequest\x1a\x16.connpy.StructResponse\"\x00\x12<\n\x0b\x61\x64\x64_profile\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x44\n\x11resolve_node_data\x12\x15.connpy.StructRequest\x1a\x16.connpy.StructResponse\"\x00\x12=\n\x0e\x64\x65lete_profile\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12?\n\x0eupdate_profile\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\xae\x03\n\rConfigService\x12@\n\x0cget_settings\x12\x16.google.protobuf.Empty\x1a\x16.connpy.StructResponse\"\x00\x12\x43\n\x0fget_default_dir\x12\x16.google.protobuf.Empty\x1a\x16.connpy.StringResponse\"\x00\x12\x44\n\x11set_config_folder\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x41\n\x0eupdate_setting\x12\x15.connpy.UpdateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x10\x65ncrypt_password\x12\x15.connpy.StringRequest\x1a\x16.connpy.StringResponse\"\x00\x12H\n\x15\x61pply_theme_from_file\x12\x15.connpy.StringRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xca\x02\n\rPluginService\x12?\n\x0clist_plugins\x12\x16.google.protobuf.Empty\x1a\x15.connpy.ValueResponse\"\x00\x12=\n\nadd_plugin\x12\x15.connpy.PluginRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\rdelete_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\renable_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12=\n\x0e\x64isable_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\x9b\x02\n\x10\x45xecutionService\x12=\n\x0crun_commands\x12\x12.connpy.RunRequest\x1a\x15.connpy.NodeRunResult\"\x00\x30\x01\x12?\n\rtest_commands\x12\x13.connpy.TestRequest\x1a\x15.connpy.NodeRunResult\"\x00\x30\x01\x12\x41\n\x0erun_cli_script\x12\x15.connpy.ScriptRequest\x1a\x16.connpy.StructResponse\"\x00\x12\x44\n\x11run_yaml_playbook\x12\x15.connpy.ScriptRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xe2\x01\n\x13ImportExportService\x12\x41\n\x0e\x65xport_to_file\x12\x15.connpy.ExportRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x10import_from_file\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x12set_reserved_names\x12\x13.connpy.ListRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\x8e\x03\n\tAIService\x12\x33\n\x03\x61sk\x12\x12.connpy.AskRequest\x1a\x12.connpy.AIResponse\"\x00(\x01\x30\x01\x12\x38\n\x07\x63onfirm\x12\x15.connpy.StringRequest\x1a\x14.connpy.BoolResponse\"\x00\x12@\n\rlist_sessions\x12\x16.google.protobuf.Empty\x1a\x15.connpy.ValueResponse\"\x00\x12\x41\n\x0e\x64\x65lete_session\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n\x12\x63onfigure_provider\x12\x17.connpy.ProviderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x44\n\x11load_session_data\x12\x15.connpy.StringRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xc2\x02\n\rSystemService\x12\x39\n\tstart_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\tdebug_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\x08stop_api\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12;\n\x0brestart_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12@\n\x0eget_api_status\x12\x16.google.protobuf.Empty\x1a\x14.connpy.BoolResponse\"\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -68,43 +68,43 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['_PLUGINREQUEST']._serialized_start=1052 _globals['_PLUGINREQUEST']._serialized_end=1118 _globals['_RUNREQUEST']._serialized_start=1121 - _globals['_RUNREQUEST']._serialized_end=1255 - _globals['_TESTREQUEST']._serialized_start=1258 - _globals['_TESTREQUEST']._serialized_end=1411 - _globals['_SCRIPTREQUEST']._serialized_start=1413 - _globals['_SCRIPTREQUEST']._serialized_end=1478 - _globals['_EXPORTREQUEST']._serialized_start=1480 - _globals['_EXPORTREQUEST']._serialized_end=1531 - _globals['_LISTREQUEST']._serialized_start=1533 - _globals['_LISTREQUEST']._serialized_end=1561 - _globals['_ASKREQUEST']._serialized_start=1564 - _globals['_ASKREQUEST']._serialized_end=1858 - _globals['_AIRESPONSE']._serialized_start=1861 - _globals['_AIRESPONSE']._serialized_end=2061 - _globals['_BOOLRESPONSE']._serialized_start=2063 - _globals['_BOOLRESPONSE']._serialized_end=2092 - _globals['_PROVIDERREQUEST']._serialized_start=2094 - _globals['_PROVIDERREQUEST']._serialized_end=2161 - _globals['_INTREQUEST']._serialized_start=2163 - _globals['_INTREQUEST']._serialized_end=2190 - _globals['_NODERUNRESULT']._serialized_start=2192 - _globals['_NODERUNRESULT']._serialized_end=2304 - _globals['_FULLREPLACEREQUEST']._serialized_start=2306 - _globals['_FULLREPLACEREQUEST']._serialized_end=2415 - _globals['_NODESERVICE']._serialized_start=2418 - _globals['_NODESERVICE']._serialized_end=3411 - _globals['_PROFILESERVICE']._serialized_start=3414 - _globals['_PROFILESERVICE']._serialized_end=3820 - _globals['_CONFIGSERVICE']._serialized_start=3823 - _globals['_CONFIGSERVICE']._serialized_end=4253 - _globals['_PLUGINSERVICE']._serialized_start=4256 - _globals['_PLUGINSERVICE']._serialized_end=4586 - _globals['_EXECUTIONSERVICE']._serialized_start=4589 - _globals['_EXECUTIONSERVICE']._serialized_end=4872 - _globals['_IMPORTEXPORTSERVICE']._serialized_start=4875 - _globals['_IMPORTEXPORTSERVICE']._serialized_end=5101 - _globals['_AISERVICE']._serialized_start=5104 - _globals['_AISERVICE']._serialized_end=5502 - _globals['_SYSTEMSERVICE']._serialized_start=5505 - _globals['_SYSTEMSERVICE']._serialized_end=5827 + _globals['_RUNREQUEST']._serialized_end=1286 + _globals['_TESTREQUEST']._serialized_start=1289 + _globals['_TESTREQUEST']._serialized_end=1473 + _globals['_SCRIPTREQUEST']._serialized_start=1475 + _globals['_SCRIPTREQUEST']._serialized_end=1540 + _globals['_EXPORTREQUEST']._serialized_start=1542 + _globals['_EXPORTREQUEST']._serialized_end=1593 + _globals['_LISTREQUEST']._serialized_start=1595 + _globals['_LISTREQUEST']._serialized_end=1623 + _globals['_ASKREQUEST']._serialized_start=1626 + _globals['_ASKREQUEST']._serialized_end=1920 + _globals['_AIRESPONSE']._serialized_start=1923 + _globals['_AIRESPONSE']._serialized_end=2123 + _globals['_BOOLRESPONSE']._serialized_start=2125 + _globals['_BOOLRESPONSE']._serialized_end=2154 + _globals['_PROVIDERREQUEST']._serialized_start=2156 + _globals['_PROVIDERREQUEST']._serialized_end=2223 + _globals['_INTREQUEST']._serialized_start=2225 + _globals['_INTREQUEST']._serialized_end=2252 + _globals['_NODERUNRESULT']._serialized_start=2254 + _globals['_NODERUNRESULT']._serialized_end=2366 + _globals['_FULLREPLACEREQUEST']._serialized_start=2368 + _globals['_FULLREPLACEREQUEST']._serialized_end=2477 + _globals['_NODESERVICE']._serialized_start=2480 + _globals['_NODESERVICE']._serialized_end=3473 + _globals['_PROFILESERVICE']._serialized_start=3476 + _globals['_PROFILESERVICE']._serialized_end=3882 + _globals['_CONFIGSERVICE']._serialized_start=3885 + _globals['_CONFIGSERVICE']._serialized_end=4315 + _globals['_PLUGINSERVICE']._serialized_start=4318 + _globals['_PLUGINSERVICE']._serialized_end=4648 + _globals['_EXECUTIONSERVICE']._serialized_start=4651 + _globals['_EXECUTIONSERVICE']._serialized_end=4934 + _globals['_IMPORTEXPORTSERVICE']._serialized_start=4937 + _globals['_IMPORTEXPORTSERVICE']._serialized_end=5163 + _globals['_AISERVICE']._serialized_start=5166 + _globals['_AISERVICE']._serialized_end=5564 + _globals['_SYSTEMSERVICE']._serialized_start=5567 + _globals['_SYSTEMSERVICE']._serialized_end=5889 # @@protoc_insertion_point(module_scope) diff --git a/connpy/grpc_layer/connpy_pb2_grpc.py b/connpy/grpc_layer/connpy_pb2_grpc.py index f883e03..6975cf7 100644 --- a/connpy/grpc_layer/connpy_pb2_grpc.py +++ b/connpy/grpc_layer/connpy_pb2_grpc.py @@ -2,8 +2,7 @@ """Client and server classes corresponding to protobuf-defined services.""" import grpc import warnings - -import connpy_pb2 as connpy__pb2 +from . import connpy_pb2 as connpy__pb2 from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 GRPC_GENERATED_VERSION = '1.80.0' diff --git a/connpy/grpc_layer/server.py b/connpy/grpc_layer/server.py index 2a9b9b9..955793f 100644 --- a/connpy/grpc_layer/server.py +++ b/connpy/grpc_layer/server.py @@ -388,14 +388,20 @@ class ExecutionServicer(connpy_pb2_grpc.ExecutionServiceServicer): def _worker(): try: + # Set task name in thread state for printer if available + if request.name: + printer.console.print(f"[debug][DEBUG][/debug] Executing task: [bold cyan]{request.name}[/bold cyan]") + self.service.run_commands( nodes_filter=nodes_filter, commands=list(request.commands), folder=request.folder if request.folder else None, prompt=request.prompt if request.prompt else None, parallel=request.parallel, + timeout=request.timeout if request.timeout > 0 else 10, variables=from_struct(request.vars) if request.HasField("vars") else None, - on_node_complete=_on_complete + on_node_complete=_on_complete, + name=request.name if request.name else None ) except Exception as e: # Optionally pass error to stream, but handle_errors decorator covers top-level. @@ -428,20 +434,26 @@ class ExecutionServicer(connpy_pb2_grpc.ExecutionServiceServicer): q = queue.Queue() - def _on_complete(unique, output, status, result): - q.put({"unique_id": unique, "output": output, "status": status, "result": result}) + def _on_complete(unique, node_output, node_status, node_result): + q.put({"unique_id": unique, "output": node_output, "status": node_status, "result": node_result}) def _worker(): try: + # Set task name in thread state for printer if available + if request.name: + printer.console.print(f"[debug][DEBUG][/debug] Executing task: [bold cyan]{request.name}[/bold cyan]") + self.service.test_commands( nodes_filter=nodes_filter, commands=list(request.commands), - expected=request.expected, + expected=list(request.expected), folder=request.folder if request.folder else None, prompt=request.prompt if request.prompt else None, parallel=request.parallel, + timeout=request.timeout if request.timeout > 0 else 10, variables=from_struct(request.vars) if request.HasField("vars") else None, - on_node_complete=_on_complete + on_node_complete=_on_complete, + name=request.name if request.name else None ) except Exception as e: q.put(e) diff --git a/connpy/grpc_layer/stubs.py b/connpy/grpc_layer/stubs.py index 7eb637a..52219b8 100644 --- a/connpy/grpc_layer/stubs.py +++ b/connpy/grpc_layer/stubs.py @@ -420,9 +420,9 @@ class ExecutionStub: folder=folder or "", prompt=prompt or "", parallel=parallel, + timeout=timeout, + name=kwargs.get("name", "") ) - # Note: 'timeout', 'on_node_complete', and 'logger' are currently not - # sent over gRPC in the current proto definition. if variables is not None: req.vars.CopyFrom(to_struct(variables)) @@ -432,7 +432,10 @@ class ExecutionStub: for response in self.stub.run_commands(req): if on_complete: on_complete(response.unique_id, response.output, response.status) - final_results[response.unique_id] = response.output + final_results[response.unique_id] = { + "output": response.output, + "status": response.status + } return final_results @@ -442,10 +445,12 @@ class ExecutionStub: req = connpy_pb2.TestRequest( nodes=nodes_list, commands=commands, - expected=expected, + expected=expected if isinstance(expected, list) else [expected], folder=kwargs.get("folder", ""), prompt=prompt or "", parallel=parallel, + timeout=timeout, + name=kwargs.get("name", "") ) if variables is not None: req.vars.CopyFrom(to_struct(variables)) diff --git a/connpy/printer.py b/connpy/printer.py index 997133c..501b215 100644 --- a/connpy/printer.py +++ b/connpy/printer.py @@ -317,7 +317,7 @@ def test_panel(unique, output, status, result): _get_console().print(Panel(Group(Text(), code_block, test_results), title=title_line, width=cols, border_style=border)) def test_summary(results): - """Print an aggregate summary of multiple test results.""" + """Print an aggregate summary of multiple test results in a single panel.""" from rich.panel import Panel from rich.text import Text from rich.console import Group @@ -328,26 +328,96 @@ def test_summary(results): except OSError: cols = 80 - for node, test_result in results.items(): - status_code = 0 if test_result and all(test_result.values()) else 1 - if status_code == 0: - status_str = "[pass]✓ PASS[/pass]" - border = "pass" - else: - status_str = f"[fail]✗ FAIL[/fail]" - border = "fail" + summary_content = Text() + total_passed = 0 + total_failed = 0 + total_partial = 0 + + if not results: + summary_content.append(" No test results found.\n", style="error") + else: + for node, test_result in results.items(): + summary_content.append(f"• ", style="border") + summary_content.append(f"{node.ljust(40)}", style="bold") - title_line = f"[bold]{node}[/bold] — {status_str}" - - test_output = Text() - test_output.append("TEST RESULTS:\n", style="header") - max_key_len = max(len(k) for k in test_result.keys()) if test_result else 0 - for k, v in (test_result.items() if test_result else []): - mark = "✓" if v else "✗" - style = "success" if v else "error" - test_output.append(f" {k.ljust(max_key_len)} {mark}\n", style=style) + if test_result: + passed_count = sum(1 for v in test_result.values() if v) + total_count = len(test_result) + + if passed_count == total_count: + total_passed += 1 + node_style = "success" + mark = "✓ PASS" + elif passed_count > 0: + total_partial += 1 + node_style = "warning" + mark = f"⚠ PARTIAL ({passed_count}/{total_count})" + else: + total_failed += 1 + node_style = "error" + mark = "✗ FAIL" + + summary_content.append(f" {mark}\n", style=node_style) + for k, v in test_result.items(): + res_mark = "✓" if v else "✗" + res_style = "success" if v else "error" + summary_content.append(f" {k.ljust(38)} {res_mark}\n", style=res_style) + else: + total_failed += 1 + summary_content.append(" ✗ FAIL\n", style="error") + summary_content.append(" No results (execution failed)\n", style="error") + + status_parts = [] + if total_passed: status_parts.append(f"[pass]{total_passed} PASSED[/pass]") + if total_partial: status_parts.append(f"[warning]{total_partial} PARTIAL[/warning]") + if total_failed: status_parts.append(f"[fail]{total_failed} FAILED[/fail]") + + status_str = " | ".join(status_parts) if status_parts else "[error]NO RESULTS[/error]" + title_line = f"AGGREGATE TEST SUMMARY — {status_str}" + + _get_console().print(Panel(Group(Text(), summary_content), title=title_line, width=cols, border_style="border")) + +def run_summary(results): + """Print an aggregate summary of multiple execution results in a single panel.""" + from rich.panel import Panel + from rich.text import Text + from rich.console import Group + import os + + try: + cols, _ = os.get_terminal_size() + except OSError: + cols = 80 + + summary_content = Text() + total_ok = 0 + total_err = 0 + + if not results: + summary_content.append(" No execution results found.\n", style="error") + else: + for node, data in results.items(): + summary_content.append(f"• ", style="border") + summary_content.append(f"{node.ljust(40)}", style="bold") - _get_console().print(Panel(Group(Text(), test_output), title=title_line, width=cols, border_style=border)) + # Check if we have a status dict or just output (for backward compatibility) + status = data.get("status", 0) if isinstance(data, dict) else 0 + + if status == 0: + total_ok += 1 + summary_content.append(f" ✓ DONE\n", style="success") + else: + total_err += 1 + summary_content.append(f" ✗ FAIL({status})\n", style="error") + + status_parts = [] + if total_ok: status_parts.append(f"[success]{total_ok} DONE[/success]") + if total_err: status_parts.append(f"[error]{total_err} FAILED[/error]") + + status_str = " | ".join(status_parts) if status_parts else "[error]NO RESULTS[/error]" + title_line = f"AGGREGATE EXECUTION SUMMARY — {status_str}" + + _get_console().print(Panel(Group(Text(), summary_content), title=title_line, width=cols, border_style="border")) def header(text): """Print a section header.""" diff --git a/connpy/proto/connpy.proto b/connpy/proto/connpy.proto index bf1dbb2..bf8546a 100644 --- a/connpy/proto/connpy.proto +++ b/connpy/proto/connpy.proto @@ -176,16 +176,20 @@ message RunRequest { string prompt = 4; int32 parallel = 5; google.protobuf.Struct vars = 6; + int32 timeout = 7; + string name = 8; } message TestRequest { repeated string nodes = 1; repeated string commands = 2; - string expected = 3; + repeated string expected = 3; string folder = 4; string prompt = 5; int32 parallel = 6; google.protobuf.Struct vars = 7; + int32 timeout = 8; + string name = 9; } message ScriptRequest { diff --git a/connpy/services/execution_service.py b/connpy/services/execution_service.py index 9c34f88..d8b4552 100644 --- a/connpy/services/execution_service.py +++ b/connpy/services/execution_service.py @@ -18,7 +18,8 @@ class ExecutionService(BaseService): folder: Optional[str] = None, prompt: Optional[str] = None, on_node_complete: Optional[Callable] = None, - logger: Optional[Callable] = None + logger: Optional[Callable] = None, + name: Optional[str] = None ) -> Dict[str, str]: """Execute commands on a set of nodes.""" @@ -42,7 +43,15 @@ class ExecutionService(BaseService): logger=logger ) - return results + # Combine output and status for the caller + full_results = {} + for unique in results: + full_results[unique] = { + "output": results[unique], + "status": executor.status.get(unique, 1) + } + + return full_results except Exception as e: raise ConnpyError(f"Execution failed: {e}") @@ -54,9 +63,11 @@ class ExecutionService(BaseService): variables: Optional[Dict[str, Any]] = None, parallel: int = 10, timeout: int = 10, + folder: Optional[str] = None, prompt: Optional[str] = None, on_node_complete: Optional[Callable] = None, - logger: Optional[Callable] = None + logger: Optional[Callable] = None, + name: Optional[str] = None ) -> Dict[str, Dict[str, bool]]: """Run commands and verify expected output on a set of nodes.""" @@ -75,6 +86,7 @@ class ExecutionService(BaseService): vars=variables, parallel=parallel, timeout=timeout, + folder=folder, prompt=prompt, on_complete=on_node_complete, logger=logger @@ -96,37 +108,52 @@ class ExecutionService(BaseService): return self.run_commands(nodes_filter, commands, parallel=parallel) - def run_yaml_playbook(self, playbook_path: str, parallel: int = 10) -> Dict[str, Any]: - """Run a structured Connpy YAML automation playbook.""" - if not os.path.exists(playbook_path): - raise ConnpyError(f"Playbook file not found: {playbook_path}") - - try: - with open(playbook_path, "r") as f: - playbook = yaml.load(f, Loader=yaml.FullLoader) - except Exception as e: - raise ConnpyError(f"Failed to load playbook {playbook_path}: {e}") + def run_yaml_playbook(self, playbook_data: str, parallel: int = 10) -> Dict[str, Any]: + """Run a structured Connpy YAML automation playbook (from path or content).""" + playbook = None + if playbook_data.startswith("---YAML---\n"): + try: + content = playbook_data[len("---YAML---\n"):] + playbook = yaml.load(content, Loader=yaml.FullLoader) + except Exception as e: + raise ConnpyError(f"Failed to parse YAML content: {e}") + else: + if not os.path.exists(playbook_data): + raise ConnpyError(f"Playbook file not found: {playbook_data}") + try: + with open(playbook_data, "r") as f: + playbook = yaml.load(f, Loader=yaml.FullLoader) + except Exception as e: + raise ConnpyError(f"Failed to load playbook {playbook_data}: {e}") # Basic validation if not isinstance(playbook, dict) or "nodes" not in playbook or "commands" not in playbook: raise ConnpyError("Invalid playbook format: missing 'nodes' or 'commands' keys.") action = playbook.get("action", "run") + options = playbook.get("options", {}) + + # Extract all fields similar to RunHandler.cli_run + exec_args = { + "nodes_filter": playbook["nodes"], + "commands": playbook["commands"], + "variables": playbook.get("variables"), + "parallel": options.get("parallel", parallel), + "timeout": playbook.get("timeout", options.get("timeout", 10)), + "prompt": options.get("prompt"), + "name": playbook.get("name", "Task") + } + + # Map 'output' field to folder path if it's not stdout/null + output_cfg = playbook.get("output") + if output_cfg not in [None, "stdout"]: + exec_args["folder"] = output_cfg + if action == "run": - return self.run_commands( - nodes_filter=playbook["nodes"], - commands=playbook["commands"], - parallel=parallel, - timeout=playbook.get("timeout", 10) - ) + return self.run_commands(**exec_args) elif action == "test": - return self.test_commands( - nodes_filter=playbook["nodes"], - commands=playbook["commands"], - expected=playbook.get("expected", []), - parallel=parallel, - timeout=playbook.get("timeout", 10) - ) + exec_args["expected"] = playbook.get("expected", []) + return self.test_commands(**exec_args) else: raise ConnpyError(f"Unsupported playbook action: {action}") diff --git a/docs/connpy/cli/ai_handler.html b/docs/connpy/cli/ai_handler.html index 78549bc..32cd613 100644 --- a/docs/connpy/cli/ai_handler.html +++ b/docs/connpy/cli/ai_handler.html @@ -3,7 +3,7 @@ - + connpy.cli.ai_handler API documentation @@ -371,7 +371,7 @@ el.replaceWith(d); diff --git a/docs/connpy/cli/api_handler.html b/docs/connpy/cli/api_handler.html index 1263f6e..29eb836 100644 --- a/docs/connpy/cli/api_handler.html +++ b/docs/connpy/cli/api_handler.html @@ -3,7 +3,7 @@ - + connpy.cli.api_handler API documentation @@ -193,7 +193,7 @@ el.replaceWith(d); diff --git a/docs/connpy/cli/config_handler.html b/docs/connpy/cli/config_handler.html index a95351c..667773d 100644 --- a/docs/connpy/cli/config_handler.html +++ b/docs/connpy/cli/config_handler.html @@ -3,7 +3,7 @@ - + connpy.cli.config_handler API documentation @@ -482,7 +482,7 @@ el.replaceWith(d); diff --git a/docs/connpy/cli/context_handler.html b/docs/connpy/cli/context_handler.html index a6b3dfb..11e2a86 100644 --- a/docs/connpy/cli/context_handler.html +++ b/docs/connpy/cli/context_handler.html @@ -3,7 +3,7 @@ - + connpy.cli.context_handler API documentation @@ -249,7 +249,7 @@ el.replaceWith(d); diff --git a/docs/connpy/cli/forms.html b/docs/connpy/cli/forms.html index 72ddbba..b4290fa 100644 --- a/docs/connpy/cli/forms.html +++ b/docs/connpy/cli/forms.html @@ -3,7 +3,7 @@ - + connpy.cli.forms API documentation @@ -517,7 +517,7 @@ el.replaceWith(d); diff --git a/docs/connpy/cli/help_text.html b/docs/connpy/cli/help_text.html index d9d311f..e892b41 100644 --- a/docs/connpy/cli/help_text.html +++ b/docs/connpy/cli/help_text.html @@ -3,7 +3,7 @@ - + connpy.cli.help_text API documentation @@ -215,9 +215,7 @@ tasks: nodes: #List of nodes to work on. Mandatory - 'router1@office' #You can add specific nodes - '@aws' #entire folders or subfolders - - '@office': #or filter inside a folder or subfolder - - 'router2' - - 'router7' + - 'router.*@office' #or use regex to filter inside a folder commands: #List of commands to send, use {name} to pass variables - 'term len 0' @@ -243,7 +241,7 @@ tasks: vrouterN@aws: id: 5 - output: /home/user/logs #Type of output, if null you only get Connection and test result. Choices are: null,stdout,/path/to/folder. Folder path only works on 'run' action. + output: /home/user/logs #Type of output, if null you only get Connection and test result. Choices are: null,stdout,/path/to/folder. Folder path works on both 'run' and 'test' actions. options: prompt: r'>$|#$|\$$|>.$|#.$|\$.$' #Optional prompt to check on your devices, default should work on most devices. @@ -255,9 +253,6 @@ tasks: nodes: - 'router1@office' - '@aws' - - '@office': - - 'router2' - - 'router7' commands: - 'ping 10.100.100.{id}' expected: '!' #Expected text to find when running test action. Mandatory for 'test' @@ -304,7 +299,7 @@ tasks: diff --git a/docs/connpy/cli/helpers.html b/docs/connpy/cli/helpers.html index c0a11ca..052dd92 100644 --- a/docs/connpy/cli/helpers.html +++ b/docs/connpy/cli/helpers.html @@ -3,7 +3,7 @@ - + connpy.cli.helpers API documentation @@ -207,7 +207,7 @@ el.replaceWith(d); diff --git a/docs/connpy/cli/import_export_handler.html b/docs/connpy/cli/import_export_handler.html index 6f6aa1b..cbb12ec 100644 --- a/docs/connpy/cli/import_export_handler.html +++ b/docs/connpy/cli/import_export_handler.html @@ -3,7 +3,7 @@ - + connpy.cli.import_export_handler API documentation @@ -272,7 +272,7 @@ el.replaceWith(d); diff --git a/docs/connpy/cli/index.html b/docs/connpy/cli/index.html index b8d5d23..2bf030c 100644 --- a/docs/connpy/cli/index.html +++ b/docs/connpy/cli/index.html @@ -3,7 +3,7 @@ - + connpy.cli API documentation @@ -137,7 +137,7 @@ el.replaceWith(d); diff --git a/docs/connpy/cli/node_handler.html b/docs/connpy/cli/node_handler.html index 992ba84..9123836 100644 --- a/docs/connpy/cli/node_handler.html +++ b/docs/connpy/cli/node_handler.html @@ -3,7 +3,7 @@ - + connpy.cli.node_handler API documentation @@ -606,7 +606,7 @@ el.replaceWith(d); diff --git a/docs/connpy/cli/plugin_handler.html b/docs/connpy/cli/plugin_handler.html index 17142e7..318bde6 100644 --- a/docs/connpy/cli/plugin_handler.html +++ b/docs/connpy/cli/plugin_handler.html @@ -3,7 +3,7 @@ - + connpy.cli.plugin_handler API documentation @@ -385,7 +385,7 @@ el.replaceWith(d); diff --git a/docs/connpy/cli/profile_handler.html b/docs/connpy/cli/profile_handler.html index 0d6680f..67daeaf 100644 --- a/docs/connpy/cli/profile_handler.html +++ b/docs/connpy/cli/profile_handler.html @@ -3,7 +3,7 @@ - + connpy.cli.profile_handler API documentation @@ -314,7 +314,7 @@ el.replaceWith(d); diff --git a/docs/connpy/cli/run_handler.html b/docs/connpy/cli/run_handler.html index 1bc049a..7213d59 100644 --- a/docs/connpy/cli/run_handler.html +++ b/docs/connpy/cli/run_handler.html @@ -3,7 +3,7 @@ - + connpy.cli.run_handler API documentation @@ -58,6 +58,7 @@ el.replaceWith(d);
class RunHandler:
     def __init__(self, app):
         self.app = app
+        self.print_lock = threading.Lock()
 
     def dispatch(self, args):
         if len(args.data) > 1:
@@ -68,22 +69,43 @@ el.replaceWith(d);
     def node_run(self, args):
         nodes_filter = args.data[0]
         commands = [" ".join(args.data[1:])]
-        
+
         try:
             header_printed = False
-            # Inline execution with streaming results
-            def _on_node_complete(unique, node_output, node_status):
-                nonlocal header_printed
-                if not header_printed:
-                    printer.console.print(Rule("OUTPUT", style="header"))
-                    header_printed = True
-                printer.node_panel(unique, node_output, node_status)
-                
-            self.app.services.execution.run_commands(
-                nodes_filter=nodes_filter,
-                commands=commands,
-                on_node_complete=_on_node_complete
-            )
+
+            if hasattr(args, 'test_expected') and args.test_expected:
+                # Mode: Test
+                def _on_node_complete(unique, node_output, node_status, node_result):
+                    nonlocal header_printed
+                    with self.print_lock:
+                        if not header_printed:
+                            printer.console.print(Rule("OUTPUT", style="header"))
+                            header_printed = True
+                    printer.test_panel(unique, node_output, node_status, node_result)
+
+                results = self.app.services.execution.test_commands(
+                    nodes_filter=nodes_filter,
+                    commands=commands,
+                    expected=args.test_expected,
+                    on_node_complete=_on_node_complete
+                )
+                printer.test_summary(results)
+            else:
+                # Mode: Normal Run
+                def _on_node_complete(unique, node_output, node_status):
+                    nonlocal header_printed
+                    with self.print_lock:
+                        if not header_printed:
+                            printer.console.print(Rule("OUTPUT", style="header"))
+                            header_printed = True
+                    printer.node_panel(unique, node_output, node_status)
+
+                results = self.app.services.execution.run_commands(
+                    nodes_filter=nodes_filter,
+                    commands=commands,
+                    on_node_complete=_on_node_complete
+                )
+                printer.run_summary(results)
 
         except ConnpyError as e:
             printer.error(str(e))
@@ -104,36 +126,44 @@ el.replaceWith(d);
         try:
             with open(path, "r") as f:
                 playbook = yaml.load(f, Loader=yaml.FullLoader)
-                
+
             for task in playbook.get("tasks", []):
                 self.cli_run(task)
-                
+
         except Exception as e:
             printer.error(f"Failed to run playbook {path}: {e}")
             sys.exit(10)
 
     def cli_run(self, script):
+        name = script.get("name", "Task")
         try:
             action = script["action"]
             nodelist = script["nodes"]
             commands = script["commands"]
             variables = script.get("variables")
             output_cfg = script["output"]
-            name = script.get("name", "Task")
             options = script.get("options", {})
         except KeyError as e:
-            printer.error(f"'{e.args[0]}' is mandatory in script")
+            printer.error(f"[{name}] '{e.args[0]}' is mandatory in script")
             sys.exit(11)
 
         stdout = (output_cfg == "stdout")
         folder = output_cfg if output_cfg not in [None, "stdout"] else None
         prompt = options.get("prompt")
-        printer.header(name.upper())
-        
+
         try:
+            header_printed = False
             if action == "run":
                 # If stdout is true, we stream results as they arrive
-                on_complete = printer.node_panel if stdout else None
+                def _on_run_complete(unique, node_output, node_status):
+                    nonlocal header_printed
+                    if stdout:
+                        with self.print_lock:
+                            if not header_printed:
+                                printer.console.print(Rule(name.upper(), style="header"))
+                                header_printed = True
+                        printer.node_panel(unique, node_output, node_status)
+
                 results = self.app.services.execution.run_commands(
                     nodes_filter=nodelist,
                     commands=commands,
@@ -142,16 +172,31 @@ el.replaceWith(d);
                     timeout=options.get("timeout", 10),
                     folder=folder,
                     prompt=prompt,
-                    on_node_complete=on_complete
+                    on_node_complete=_on_run_complete
                 )
-                # If not streaming, we could print a summary table here if needed
-                if not stdout:
-                    for unique, output in results.items():
+                # Final Summary
+                if not stdout and not folder:
+                    with self.print_lock:
+                        printer.console.print(Rule(name.upper(), style="header"))
+                    for unique, data in results.items():
+                        output = data["output"] if isinstance(data, dict) else data
                         printer.node_panel(unique, output, 0)
-                        
+                
+                # ALWAYS show the aggregate execution summary at the end
+                printer.run_summary(results)
+
             elif action == "test":
                 expected = script.get("expected", [])
-                on_complete = printer.test_panel if stdout else None
+                # Show test_panel per node ONLY if stdout is True
+                def _on_test_complete(unique, node_output, node_status, node_result):
+                    nonlocal header_printed
+                    if stdout:
+                        with self.print_lock:
+                            if not header_printed:
+                                printer.console.print(Rule(name.upper(), style="header"))
+                                header_printed = True
+                        printer.test_panel(unique, node_output, node_status, node_result)
+
                 results = self.app.services.execution.test_commands(
                     nodes_filter=nodelist,
                     commands=commands,
@@ -159,12 +204,13 @@ el.replaceWith(d);
                     variables=variables,
                     parallel=options.get("parallel", 10),
                     timeout=options.get("timeout", 10),
+                    folder=folder,
                     prompt=prompt,
-                    on_node_complete=on_complete
+                    on_node_complete=_on_test_complete
                 )
-                if not stdout:
-                    printer.test_summary(results)
-                
+                # ALWAYS show the aggregate summary at the end
+                printer.test_summary(results)
+
         except ConnpyError as e:
             printer.error(str(e))
@@ -180,27 +226,35 @@ el.replaceWith(d); Expand source code
def cli_run(self, script):
+    name = script.get("name", "Task")
     try:
         action = script["action"]
         nodelist = script["nodes"]
         commands = script["commands"]
         variables = script.get("variables")
         output_cfg = script["output"]
-        name = script.get("name", "Task")
         options = script.get("options", {})
     except KeyError as e:
-        printer.error(f"'{e.args[0]}' is mandatory in script")
+        printer.error(f"[{name}] '{e.args[0]}' is mandatory in script")
         sys.exit(11)
 
     stdout = (output_cfg == "stdout")
     folder = output_cfg if output_cfg not in [None, "stdout"] else None
     prompt = options.get("prompt")
-    printer.header(name.upper())
-    
+
     try:
+        header_printed = False
         if action == "run":
             # If stdout is true, we stream results as they arrive
-            on_complete = printer.node_panel if stdout else None
+            def _on_run_complete(unique, node_output, node_status):
+                nonlocal header_printed
+                if stdout:
+                    with self.print_lock:
+                        if not header_printed:
+                            printer.console.print(Rule(name.upper(), style="header"))
+                            header_printed = True
+                    printer.node_panel(unique, node_output, node_status)
+
             results = self.app.services.execution.run_commands(
                 nodes_filter=nodelist,
                 commands=commands,
@@ -209,16 +263,31 @@ el.replaceWith(d);
                 timeout=options.get("timeout", 10),
                 folder=folder,
                 prompt=prompt,
-                on_node_complete=on_complete
+                on_node_complete=_on_run_complete
             )
-            # If not streaming, we could print a summary table here if needed
-            if not stdout:
-                for unique, output in results.items():
+            # Final Summary
+            if not stdout and not folder:
+                with self.print_lock:
+                    printer.console.print(Rule(name.upper(), style="header"))
+                for unique, data in results.items():
+                    output = data["output"] if isinstance(data, dict) else data
                     printer.node_panel(unique, output, 0)
-                    
+            
+            # ALWAYS show the aggregate execution summary at the end
+            printer.run_summary(results)
+
         elif action == "test":
             expected = script.get("expected", [])
-            on_complete = printer.test_panel if stdout else None
+            # Show test_panel per node ONLY if stdout is True
+            def _on_test_complete(unique, node_output, node_status, node_result):
+                nonlocal header_printed
+                if stdout:
+                    with self.print_lock:
+                        if not header_printed:
+                            printer.console.print(Rule(name.upper(), style="header"))
+                            header_printed = True
+                    printer.test_panel(unique, node_output, node_status, node_result)
+
             results = self.app.services.execution.test_commands(
                 nodes_filter=nodelist,
                 commands=commands,
@@ -226,12 +295,13 @@ el.replaceWith(d);
                 variables=variables,
                 parallel=options.get("parallel", 10),
                 timeout=options.get("timeout", 10),
+                folder=folder,
                 prompt=prompt,
-                on_node_complete=on_complete
+                on_node_complete=_on_test_complete
             )
-            if not stdout:
-                printer.test_summary(results)
-            
+            # ALWAYS show the aggregate summary at the end
+            printer.test_summary(results)
+
     except ConnpyError as e:
         printer.error(str(e))
@@ -264,22 +334,43 @@ el.replaceWith(d);
def node_run(self, args):
     nodes_filter = args.data[0]
     commands = [" ".join(args.data[1:])]
-    
+
     try:
         header_printed = False
-        # Inline execution with streaming results
-        def _on_node_complete(unique, node_output, node_status):
-            nonlocal header_printed
-            if not header_printed:
-                printer.console.print(Rule("OUTPUT", style="header"))
-                header_printed = True
-            printer.node_panel(unique, node_output, node_status)
-            
-        self.app.services.execution.run_commands(
-            nodes_filter=nodes_filter,
-            commands=commands,
-            on_node_complete=_on_node_complete
-        )
+
+        if hasattr(args, 'test_expected') and args.test_expected:
+            # Mode: Test
+            def _on_node_complete(unique, node_output, node_status, node_result):
+                nonlocal header_printed
+                with self.print_lock:
+                    if not header_printed:
+                        printer.console.print(Rule("OUTPUT", style="header"))
+                        header_printed = True
+                printer.test_panel(unique, node_output, node_status, node_result)
+
+            results = self.app.services.execution.test_commands(
+                nodes_filter=nodes_filter,
+                commands=commands,
+                expected=args.test_expected,
+                on_node_complete=_on_node_complete
+            )
+            printer.test_summary(results)
+        else:
+            # Mode: Normal Run
+            def _on_node_complete(unique, node_output, node_status):
+                nonlocal header_printed
+                with self.print_lock:
+                    if not header_printed:
+                        printer.console.print(Rule("OUTPUT", style="header"))
+                        header_printed = True
+                printer.node_panel(unique, node_output, node_status)
+
+            results = self.app.services.execution.run_commands(
+                nodes_filter=nodes_filter,
+                commands=commands,
+                on_node_complete=_on_node_complete
+            )
+            printer.run_summary(results)
 
     except ConnpyError as e:
         printer.error(str(e))
@@ -320,10 +411,10 @@ el.replaceWith(d);
     try:
         with open(path, "r") as f:
             playbook = yaml.load(f, Loader=yaml.FullLoader)
-            
+
         for task in playbook.get("tasks", []):
             self.cli_run(task)
-            
+
     except Exception as e:
         printer.error(f"Failed to run playbook {path}: {e}")
         sys.exit(10)
@@ -363,7 +454,7 @@ el.replaceWith(d); diff --git a/docs/connpy/cli/sync_handler.html b/docs/connpy/cli/sync_handler.html index 4ddd115..4c751be 100644 --- a/docs/connpy/cli/sync_handler.html +++ b/docs/connpy/cli/sync_handler.html @@ -3,7 +3,7 @@ - + connpy.cli.sync_handler API documentation @@ -427,7 +427,7 @@ el.replaceWith(d); diff --git a/docs/connpy/cli/validators.html b/docs/connpy/cli/validators.html index 3bbf7cd..ee2f6b6 100644 --- a/docs/connpy/cli/validators.html +++ b/docs/connpy/cli/validators.html @@ -3,7 +3,7 @@ - + connpy.cli.validators API documentation @@ -508,7 +508,7 @@ el.replaceWith(d); diff --git a/docs/connpy/grpc_layer/connpy_pb2.html b/docs/connpy/grpc_layer/connpy_pb2.html index 0511c34..3f4a0e0 100644 --- a/docs/connpy/grpc_layer/connpy_pb2.html +++ b/docs/connpy/grpc_layer/connpy_pb2.html @@ -3,7 +3,7 @@ - + connpy.grpc_layer.connpy_pb2 API documentation @@ -45,6 +45,560 @@ el.replaceWith(d);
+

Classes

+
+
+class AIResponse +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class AskRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class BoolResponse +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class BulkRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class DeleteRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class ExportRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class FilterRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class FullReplaceRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class IdRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class IntRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class InteractRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class InteractResponse +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class ListRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class MessageValue +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class MoveRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class NodeRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class NodeRunResult +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class PluginRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class ProfileRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class ProviderRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class RunRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class ScriptRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class StringRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class StringResponse +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class StructRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class StructResponse +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class TestRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class UpdateRequest +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
+class ValueResponse +(*args, **kwargs) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google._upb._message.Message
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+

The type of the None singleton.

+
+
+
+
diff --git a/docs/connpy/grpc_layer/connpy_pb2_grpc.html b/docs/connpy/grpc_layer/connpy_pb2_grpc.html index 5421090..7554fb0 100644 --- a/docs/connpy/grpc_layer/connpy_pb2_grpc.html +++ b/docs/connpy/grpc_layer/connpy_pb2_grpc.html @@ -3,7 +3,7 @@ - + connpy.grpc_layer.connpy_pb2_grpc API documentation @@ -5735,7 +5735,7 @@ def stop_api(request, diff --git a/docs/connpy/grpc_layer/index.html b/docs/connpy/grpc_layer/index.html index 83e1647..ad05c93 100644 --- a/docs/connpy/grpc_layer/index.html +++ b/docs/connpy/grpc_layer/index.html @@ -3,7 +3,7 @@ - + connpy.grpc_layer API documentation @@ -102,7 +102,7 @@ el.replaceWith(d); diff --git a/docs/connpy/grpc_layer/remote_plugin_pb2.html b/docs/connpy/grpc_layer/remote_plugin_pb2.html index 6e7bb97..c841aa0 100644 --- a/docs/connpy/grpc_layer/remote_plugin_pb2.html +++ b/docs/connpy/grpc_layer/remote_plugin_pb2.html @@ -3,7 +3,7 @@ - + connpy.grpc_layer.remote_plugin_pb2 API documentation @@ -62,7 +62,7 @@ el.replaceWith(d);
var DESCRIPTOR
-
+

The type of the None singleton.

@@ -81,7 +81,7 @@ el.replaceWith(d);
var DESCRIPTOR
-
+

The type of the None singleton.

@@ -100,7 +100,7 @@ el.replaceWith(d);
var DESCRIPTOR
-
+

The type of the None singleton.

@@ -119,7 +119,7 @@ el.replaceWith(d);
var DESCRIPTOR
-
+

The type of the None singleton.

@@ -168,7 +168,7 @@ el.replaceWith(d); diff --git a/docs/connpy/grpc_layer/remote_plugin_pb2_grpc.html b/docs/connpy/grpc_layer/remote_plugin_pb2_grpc.html index 6372fcd..61ed251 100644 --- a/docs/connpy/grpc_layer/remote_plugin_pb2_grpc.html +++ b/docs/connpy/grpc_layer/remote_plugin_pb2_grpc.html @@ -3,7 +3,7 @@ - + connpy.grpc_layer.remote_plugin_pb2_grpc API documentation @@ -366,7 +366,7 @@ def invoke_plugin(request, diff --git a/docs/connpy/grpc_layer/server.html b/docs/connpy/grpc_layer/server.html index e4db61b..5729d79 100644 --- a/docs/connpy/grpc_layer/server.html +++ b/docs/connpy/grpc_layer/server.html @@ -3,7 +3,7 @@ - + connpy.grpc_layer.server API documentation @@ -367,14 +367,20 @@ el.replaceWith(d); def _worker(): try: + # Set task name in thread state for printer if available + if request.name: + printer.console.print(f"[debug][DEBUG][/debug] Executing task: [bold cyan]{request.name}[/bold cyan]") + self.service.run_commands( nodes_filter=nodes_filter, commands=list(request.commands), folder=request.folder if request.folder else None, prompt=request.prompt if request.prompt else None, parallel=request.parallel, + timeout=request.timeout if request.timeout > 0 else 10, variables=from_struct(request.vars) if request.HasField("vars") else None, - on_node_complete=_on_complete + on_node_complete=_on_complete, + name=request.name if request.name else None ) except Exception as e: # Optionally pass error to stream, but handle_errors decorator covers top-level. @@ -407,20 +413,26 @@ el.replaceWith(d); q = queue.Queue() - def _on_complete(unique, output, status, result): - q.put({"unique_id": unique, "output": output, "status": status, "result": result}) + def _on_complete(unique, node_output, node_status, node_result): + q.put({"unique_id": unique, "output": node_output, "status": node_status, "result": node_result}) def _worker(): try: + # Set task name in thread state for printer if available + if request.name: + printer.console.print(f"[debug][DEBUG][/debug] Executing task: [bold cyan]{request.name}[/bold cyan]") + self.service.test_commands( nodes_filter=nodes_filter, commands=list(request.commands), - expected=request.expected, + expected=list(request.expected), folder=request.folder if request.folder else None, prompt=request.prompt if request.prompt else None, parallel=request.parallel, + timeout=request.timeout if request.timeout > 0 else 10, variables=from_struct(request.vars) if request.HasField("vars") else None, - on_node_complete=_on_complete + on_node_complete=_on_complete, + name=request.name if request.name else None ) except Exception as e: q.put(e) @@ -1313,7 +1325,7 @@ interceptor chooses to service this RPC, or None otherwise.

diff --git a/docs/connpy/grpc_layer/stubs.html b/docs/connpy/grpc_layer/stubs.html index 315bad2..2af011c 100644 --- a/docs/connpy/grpc_layer/stubs.html +++ b/docs/connpy/grpc_layer/stubs.html @@ -3,7 +3,7 @@ - + connpy.grpc_layer.stubs API documentation @@ -642,9 +642,9 @@ def update_setting(self, key, value): folder=folder or "", prompt=prompt or "", parallel=parallel, + timeout=timeout, + name=kwargs.get("name", "") ) - # Note: 'timeout', 'on_node_complete', and 'logger' are currently not - # sent over gRPC in the current proto definition. if variables is not None: req.vars.CopyFrom(to_struct(variables)) @@ -654,7 +654,10 @@ def update_setting(self, key, value): for response in self.stub.run_commands(req): if on_complete: on_complete(response.unique_id, response.output, response.status) - final_results[response.unique_id] = response.output + final_results[response.unique_id] = { + "output": response.output, + "status": response.status + } return final_results @@ -664,10 +667,12 @@ def update_setting(self, key, value): req = connpy_pb2.TestRequest( nodes=nodes_list, commands=commands, - expected=expected, + expected=expected if isinstance(expected, list) else [expected], folder=kwargs.get("folder", ""), prompt=prompt or "", parallel=parallel, + timeout=timeout, + name=kwargs.get("name", "") ) if variables is not None: req.vars.CopyFrom(to_struct(variables)) @@ -728,9 +733,9 @@ def run_commands(self, nodes_filter, commands, variables=None, parallel=10, time folder=folder or "", prompt=prompt or "", parallel=parallel, + timeout=timeout, + name=kwargs.get("name", "") ) - # Note: 'timeout', 'on_node_complete', and 'logger' are currently not - # sent over gRPC in the current proto definition. if variables is not None: req.vars.CopyFrom(to_struct(variables)) @@ -740,7 +745,10 @@ def run_commands(self, nodes_filter, commands, variables=None, parallel=10, time for response in self.stub.run_commands(req): if on_complete: on_complete(response.unique_id, response.output, response.status) - final_results[response.unique_id] = response.output + final_results[response.unique_id] = { + "output": response.output, + "status": response.status + } return final_results @@ -775,10 +783,12 @@ def test_commands(self, nodes_filter, commands, expected, variables=None, parall req = connpy_pb2.TestRequest( nodes=nodes_list, commands=commands, - expected=expected, + expected=expected if isinstance(expected, list) else [expected], folder=kwargs.get("folder", ""), prompt=prompt or "", parallel=parallel, + timeout=timeout, + name=kwargs.get("name", "") ) if variables is not None: req.vars.CopyFrom(to_struct(variables)) @@ -2102,7 +2112,7 @@ def stop_api(self): diff --git a/docs/connpy/grpc_layer/utils.html b/docs/connpy/grpc_layer/utils.html index 571fbd2..da5286b 100644 --- a/docs/connpy/grpc_layer/utils.html +++ b/docs/connpy/grpc_layer/utils.html @@ -3,7 +3,7 @@ - + connpy.grpc_layer.utils API documentation @@ -138,7 +138,7 @@ el.replaceWith(d); diff --git a/docs/connpy/index.html b/docs/connpy/index.html index aa41e31..41827af 100644 --- a/docs/connpy/index.html +++ b/docs/connpy/index.html @@ -3,7 +3,7 @@ - + connpy API documentation @@ -2077,7 +2077,7 @@ class ai:
var SAFE_COMMANDS
-
+

The type of the None singleton.

Instance variables

@@ -3170,15 +3170,7 @@ class configfile: if isinstance(uniques, str): uniques = [uniques] for i in uniques: - if isinstance(i, dict): - name = list(i.keys())[0] - mylist = i[name] - if not self.config["case"]: - name = name.lower() - mylist = [item.lower() for item in mylist] - this = self.getitem(name, mylist, extract = extract) - nodes.update(this) - elif i.startswith("@"): + if i.startswith("@"): if not self.config["case"]: i = i.lower() this = self.getitem(i, extract = extract) @@ -3257,13 +3249,17 @@ class configfile: layer3 = [k + "@" + s + "@" + f for k,v in self.connections[f][s].items() if isinstance(v, dict) and v.get("type") == "connection"] nodes.extend(layer3) if filter: + flat_filter = [] if isinstance(filter, str): - nodes = [item for item in nodes if re.search(filter, item)] + flat_filter = [filter] elif isinstance(filter, list): - nodes = [item for item in nodes if any(re.search(pattern, item) for pattern in filter)] + for item in filter: + if isinstance(item, str): + flat_filter.append(item) else: - printer.error("Invalid filter: must be a string or a list of strings.") + printer.error("Filter must be a string or a list of strings") sys.exit(1) + nodes = [item for item in nodes if any(re.search(pattern, item) for pattern in flat_filter)] return nodes @MethodHook @@ -3281,15 +3277,18 @@ class configfile: layer3 = {k + "@" + s + "@" + f:v for k,v in self.connections[f][s].items() if isinstance(v, dict) and v.get("type") == "connection"} nodes.update(layer3) if filter: + flat_filter = [] if isinstance(filter, str): - filter = "^(?!.*@).+$" if filter == "@" else filter - nodes = {k: v for k, v in nodes.items() if re.search(filter, k)} + flat_filter = [filter] elif isinstance(filter, list): - filter = ["^(?!.*@).+$" if item == "@" else item for item in filter] - nodes = {k: v for k, v in nodes.items() if any(re.search(pattern, k) for pattern in filter)} + for item in filter: + if isinstance(item, str): + flat_filter.append(item) else: - printer.error("Invalid filter: must be a string or a list of strings.") + printer.error("Filter must be a string or a list of strings") sys.exit(1) + flat_filter = ["^(?!.*@).+$" if item == "@" else item for item in flat_filter] + nodes = {k: v for k, v in nodes.items() if any(re.search(pattern, k) for pattern in flat_filter)} if extract: for node, keys in nodes.items(): for key, value in keys.items(): @@ -3586,15 +3585,7 @@ def getitems(self, uniques, extract = False): if isinstance(uniques, str): uniques = [uniques] for i in uniques: - if isinstance(i, dict): - name = list(i.keys())[0] - mylist = i[name] - if not self.config["case"]: - name = name.lower() - mylist = [item.lower() for item in mylist] - this = self.getitem(name, mylist, extract = extract) - nodes.update(this) - elif i.startswith("@"): + if i.startswith("@"): if not self.config["case"]: i = i.lower() this = self.getitem(i, extract = extract) @@ -3759,6 +3750,10 @@ class node: self.jumphost = f"-o ProxyCommand=\"{jumphost_cmd}\"" else: self.jumphost = "" + + self.output = "" + self.status = 1 + self.result = {} @MethodHook def _passtx(self, passwords, *, keyfile=None): @@ -4159,7 +4154,12 @@ class node: self.child.logfile_read = self.mylog for c in commands: if vars is not None: - c = c.format(**vars) + try: + c = c.format(**vars) + except KeyError as e: + self.output = f"Error: Variable {e} not defined in task or inventory" + self.status = 1 + return self.output result = self.child.expect(expects, timeout = timeout) self.child.sendline(c) if result == 2: @@ -4193,7 +4193,7 @@ class node: return connect @MethodHook - def test(self, commands, expected, vars = None,*, prompt = r'>$|#$|\$$|>.$|#.$|\$.$', timeout = 10, logger = None): + def test(self, commands, expected, vars = None,*, folder = '', prompt = r'>$|#$|\$$|>.$|#.$|\$.$', timeout = 10, logger = None): ''' Run a command or list of commands on the node, then check if expected value appears on the output after the last command. @@ -4219,6 +4219,9 @@ class node: ### Optional Named Parameters: + - folder (str): Path where output log should be stored, leave + empty to not store logs. + - prompt (str): Prompt to be expected after a command is finished running. Usually linux uses ">" or EOF while routers use ">" or "#". The default value should @@ -4233,6 +4236,7 @@ class node: false if prompt is found before. ''' + now = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") connect = self._connect(timeout = timeout, logger = logger) if connect == True: if logger: @@ -4250,6 +4254,7 @@ class node: if "prompt" in self.tags: prompt = self.tags["prompt"] expects = [prompt, pexpect.EOF, pexpect.TIMEOUT] + output = '' if not isinstance(commands, list): commands = [commands] @@ -4261,7 +4266,12 @@ class node: self.child.logfile_read = self.mylog for c in commands: if vars is not None: - c = c.format(**vars) + try: + c = c.format(**vars) + except KeyError as e: + self.output = f"Error: Variable {e} not defined in task or inventory" + self.status = 1 + return self.output result = self.child.expect(expects, timeout = timeout) self.child.sendline(c) if result == 2: @@ -4270,6 +4280,12 @@ class node: result = self.child.expect(expects, timeout = timeout) self.child.close() output = self._logclean(self.mylog.getvalue().decode(), True) + if logger: + logger("output", output) + if folder != '': + with open(folder + "/" + self.unique + "_" + now + ".txt", "w") as f: + f.write(output) + f.close() self.output = output if result in [0, 1]: # lastcommand = commands[-1] @@ -4654,7 +4670,12 @@ def run(self, commands, vars = None,*, folder = '', prompt = r'>$ self.child.logfile_read = self.mylog for c in commands: if vars is not None: - c = c.format(**vars) + try: + c = c.format(**vars) + except KeyError as e: + self.output = f"Error: Variable {e} not defined in task or inventory" + self.status = 1 + return self.output result = self.child.expect(expects, timeout = timeout) self.child.sendline(c) if result == 2: @@ -4721,7 +4742,7 @@ def run(self, commands, vars = None,*, folder = '', prompt = r'>$
-def test(self,
commands,
expected,
vars=None,
*,
prompt='>$|#$|\\$$|>.$|#.$|\\$.$',
timeout=10,
logger=None)
+def test(self,
commands,
expected,
vars=None,
*,
folder='',
prompt='>$|#$|\\$$|>.$|#.$|\\$.$',
timeout=10,
logger=None)
@@ -4729,7 +4750,7 @@ def run(self, commands, vars = None,*, folder = '', prompt = r'>$ Expand source code
@MethodHook
-def test(self, commands, expected, vars = None,*, prompt = r'>$|#$|\$$|>.$|#.$|\$.$', timeout = 10, logger = None):
+def test(self, commands, expected, vars = None,*, folder = '', prompt = r'>$|#$|\$$|>.$|#.$|\$.$', timeout = 10, logger = None):
     '''
     Run a command or list of commands on the node, then check if expected value appears on the output after the last command.
 
@@ -4755,6 +4776,9 @@ def test(self, commands, expected, vars = None,*, prompt = r'>$|#$|\$$|&g
 
     ### Optional Named Parameters: 
 
+        - folder (str): Path where output log should be stored, leave 
+                        empty to not store logs.
+
         - prompt (str): Prompt to be expected after a command is finished
                         running. Usually linux uses  ">" or EOF while 
                         routers use ">" or "#". The default value should 
@@ -4769,6 +4793,7 @@ def test(self, commands, expected, vars = None,*, prompt = r'>$|#$|\$$|&g
               false if prompt is found before.
 
     '''
+    now = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
     connect = self._connect(timeout = timeout, logger = logger)
     if connect == True:
         if logger:
@@ -4786,6 +4811,7 @@ def test(self, commands, expected, vars = None,*, prompt = r'>$|#$|\$$|&g
         if "prompt" in self.tags:
             prompt = self.tags["prompt"]
         expects = [prompt, pexpect.EOF, pexpect.TIMEOUT]
+
         output = ''
         if not isinstance(commands, list):
             commands = [commands]
@@ -4797,7 +4823,12 @@ def test(self, commands, expected, vars = None,*, prompt = r'>$|#$|\$$|&g
         self.child.logfile_read = self.mylog
         for c in commands:
             if vars is not None:
-                c = c.format(**vars)
+                try:
+                    c = c.format(**vars)
+                except KeyError as e:
+                    self.output = f"Error: Variable {e} not defined in task or inventory"
+                    self.status = 1
+                    return self.output
             result = self.child.expect(expects, timeout = timeout)
             self.child.sendline(c)
             if result == 2:
@@ -4806,6 +4837,12 @@ def test(self, commands, expected, vars = None,*, prompt = r'>$|#$|\$$|&g
             result = self.child.expect(expects, timeout = timeout)
         self.child.close()
         output = self._logclean(self.mylog.getvalue().decode(), True)
+        if logger:
+            logger("output", output)
+        if folder != '':
+            with open(folder + "/" + self.unique + "_" + now + ".txt", "w") as f:
+                f.write(output)
+                f.close()
         self.output = output
         if result in [0, 1]:
             # lastcommand = commands[-1]
@@ -4856,7 +4893,10 @@ def test(self, commands, expected, vars = None,*, prompt = r'>$|#$|\$$|&g
                 Values: strings.
 

Optional Named Parameters:

-
- prompt (str): Prompt to be expected after a command is finished
+
- folder (str): Path where output log should be stored, leave 
+                empty to not store logs.
+
+- prompt (str): Prompt to be expected after a command is finished
                 running. Usually linux uses  ">" or EOF while 
                 routers use ">" or "#". The default value should 
                 work for most nodes. Change it if your connection 
@@ -5022,8 +5062,15 @@ class nodes:
                 nodesargs[n.unique]["vars"] = {}
                 if "__global__" in vars.keys():
                     nodesargs[n.unique]["vars"].update(vars["__global__"])
-                if n.unique in vars.keys():
-                    nodesargs[n.unique]["vars"].update(vars[n.unique])
+                for var_key, var_val in vars.items():
+                    if var_key == "__global__":
+                        continue
+                    try:
+                        if re.search(var_key, n.unique, re.IGNORECASE):
+                            nodesargs[n.unique]["vars"].update(var_val)
+                    except re.error:
+                        if var_key == n.unique:
+                            nodesargs[n.unique]["vars"].update(var_val)
             
             # Pass the logger to the node
             nodesargs[n.unique]["logger"] = logger
@@ -5048,7 +5095,7 @@ class nodes:
         return output
 
     @MethodHook
-    def test(self, commands, expected, vars = None,*, prompt = None, parallel = 10, timeout = None, on_complete = None, logger = None):
+    def test(self, commands, expected, vars = None,*, folder = None, prompt = None, parallel = 10, timeout = None, on_complete = None, logger = None):
         '''
         Run a command or list of commands on all the nodes in nodelist, then check if expected value appears on the output after the last command.
 
@@ -5103,6 +5150,9 @@ class nodes:
         nodesargs = {}
         args["commands"] = commands
         args["expected"] = expected
+        if folder != None:
+            args["folder"] = folder
+            Path(folder).mkdir(parents=True, exist_ok=True)
         if prompt != None:
             args["prompt"] = prompt
         if timeout != None:
@@ -5124,8 +5174,15 @@ class nodes:
                 nodesargs[n.unique]["vars"] = {}
                 if "__global__" in vars.keys():
                     nodesargs[n.unique]["vars"].update(vars["__global__"])
-                if n.unique in vars.keys():
-                    nodesargs[n.unique]["vars"].update(vars[n.unique])
+                for var_key, var_val in vars.items():
+                    if var_key == "__global__":
+                        continue
+                    try:
+                        if re.search(var_key, n.unique, re.IGNORECASE):
+                            nodesargs[n.unique]["vars"].update(var_val)
+                    except re.error:
+                        if var_key == n.unique:
+                            nodesargs[n.unique]["vars"].update(var_val)
             nodesargs[n.unique]["logger"] = logger
             
             if on_complete:
@@ -5275,8 +5332,15 @@ def run(self, commands, vars = None,*, folder = None, prompt = None, stdout = No
             nodesargs[n.unique]["vars"] = {}
             if "__global__" in vars.keys():
                 nodesargs[n.unique]["vars"].update(vars["__global__"])
-            if n.unique in vars.keys():
-                nodesargs[n.unique]["vars"].update(vars[n.unique])
+            for var_key, var_val in vars.items():
+                if var_key == "__global__":
+                    continue
+                try:
+                    if re.search(var_key, n.unique, re.IGNORECASE):
+                        nodesargs[n.unique]["vars"].update(var_val)
+                except re.error:
+                    if var_key == n.unique:
+                        nodesargs[n.unique]["vars"].update(var_val)
         
         # Pass the logger to the node
         nodesargs[n.unique]["logger"] = logger
@@ -5346,7 +5410,7 @@ def run(self, commands, vars = None,*, folder = None, prompt = None, stdout = No
 
-def test(self,
commands,
expected,
vars=None,
*,
prompt=None,
parallel=10,
timeout=None,
on_complete=None,
logger=None)
+def test(self,
commands,
expected,
vars=None,
*,
folder=None,
prompt=None,
parallel=10,
timeout=None,
on_complete=None,
logger=None)
@@ -5354,7 +5418,7 @@ def run(self, commands, vars = None,*, folder = None, prompt = None, stdout = No Expand source code
@MethodHook
-def test(self, commands, expected, vars = None,*, prompt = None, parallel = 10, timeout = None, on_complete = None, logger = None):
+def test(self, commands, expected, vars = None,*, folder = None, prompt = None, parallel = 10, timeout = None, on_complete = None, logger = None):
     '''
     Run a command or list of commands on all the nodes in nodelist, then check if expected value appears on the output after the last command.
 
@@ -5409,6 +5473,9 @@ def test(self, commands, expected, vars = None,*, prompt = None, parallel = 10,
     nodesargs = {}
     args["commands"] = commands
     args["expected"] = expected
+    if folder != None:
+        args["folder"] = folder
+        Path(folder).mkdir(parents=True, exist_ok=True)
     if prompt != None:
         args["prompt"] = prompt
     if timeout != None:
@@ -5430,8 +5497,15 @@ def test(self, commands, expected, vars = None,*, prompt = None, parallel = 10,
             nodesargs[n.unique]["vars"] = {}
             if "__global__" in vars.keys():
                 nodesargs[n.unique]["vars"].update(vars["__global__"])
-            if n.unique in vars.keys():
-                nodesargs[n.unique]["vars"].update(vars[n.unique])
+            for var_key, var_val in vars.items():
+                if var_key == "__global__":
+                    continue
+                try:
+                    if re.search(var_key, n.unique, re.IGNORECASE):
+                        nodesargs[n.unique]["vars"].update(var_val)
+                except re.error:
+                    if var_key == n.unique:
+                        nodesargs[n.unique]["vars"].update(var_val)
         nodesargs[n.unique]["logger"] = logger
         
         if on_complete:
@@ -5626,7 +5700,7 @@ def test(self, commands, expected, vars = None,*, prompt = None, parallel = 10,
 
 
 
 
 
diff --git a/docs/connpy/proto/index.html b/docs/connpy/proto/index.html
index 573e196..0fc7ddf 100644
--- a/docs/connpy/proto/index.html
+++ b/docs/connpy/proto/index.html
@@ -3,7 +3,7 @@
 
 
 
-
+
 connpy.proto API documentation
 
 
@@ -60,7 +60,7 @@ el.replaceWith(d);
 
 
 
 
 
diff --git a/docs/connpy/services/ai_service.html b/docs/connpy/services/ai_service.html
index 77ae7d1..0689488 100644
--- a/docs/connpy/services/ai_service.html
+++ b/docs/connpy/services/ai_service.html
@@ -3,7 +3,7 @@
 
 
 
-
+
 connpy.services.ai_service API documentation
 
 
@@ -265,7 +265,7 @@ el.replaceWith(d);
 
 
 
 
 
diff --git a/docs/connpy/services/base.html b/docs/connpy/services/base.html
index 2ff6902..e72b7ab 100644
--- a/docs/connpy/services/base.html
+++ b/docs/connpy/services/base.html
@@ -3,7 +3,7 @@
 
 
 
-
+
 connpy.services.base API documentation
 
 
@@ -152,7 +152,7 @@ el.replaceWith(d);
 
 
 
 
 
diff --git a/docs/connpy/services/config_service.html b/docs/connpy/services/config_service.html
index 78fffb1..df8016e 100644
--- a/docs/connpy/services/config_service.html
+++ b/docs/connpy/services/config_service.html
@@ -3,7 +3,7 @@
 
 
 
-
+
 connpy.services.config_service API documentation
 
 
@@ -311,7 +311,7 @@ el.replaceWith(d);
 
 
 
 
 
diff --git a/docs/connpy/services/context_service.html b/docs/connpy/services/context_service.html
index 2161ebb..0a772f7 100644
--- a/docs/connpy/services/context_service.html
+++ b/docs/connpy/services/context_service.html
@@ -3,7 +3,7 @@
 
 
 
-
+
 connpy.services.context_service API documentation
 
 
@@ -370,7 +370,7 @@ def current_context(self) -> str:
 
 
 
 
 
diff --git a/docs/connpy/services/exceptions.html b/docs/connpy/services/exceptions.html
index 164cec5..459d464 100644
--- a/docs/connpy/services/exceptions.html
+++ b/docs/connpy/services/exceptions.html
@@ -3,7 +3,7 @@
 
 
 
-
+
 connpy.services.exceptions API documentation
 
 
@@ -268,7 +268,7 @@ el.replaceWith(d);
 
 
 
 
 
diff --git a/docs/connpy/services/execution_service.html b/docs/connpy/services/execution_service.html
index 9864158..3740b1d 100644
--- a/docs/connpy/services/execution_service.html
+++ b/docs/connpy/services/execution_service.html
@@ -3,7 +3,7 @@
 
 
 
-
+
 connpy.services.execution_service API documentation
 
 
@@ -68,7 +68,8 @@ el.replaceWith(d);
         folder: Optional[str] = None,
         prompt: Optional[str] = None,
         on_node_complete: Optional[Callable] = None,
-        logger: Optional[Callable] = None
+        logger: Optional[Callable] = None,
+        name: Optional[str] = None
     ) -> Dict[str, str]:
 
         """Execute commands on a set of nodes."""
@@ -92,7 +93,15 @@ el.replaceWith(d);
                 logger=logger
             )
 
-            return results
+            # Combine output and status for the caller
+            full_results = {}
+            for unique in results:
+                full_results[unique] = {
+                    "output": results[unique],
+                    "status": executor.status.get(unique, 1)
+                }
+
+            return full_results
         except Exception as e:
             raise ConnpyError(f"Execution failed: {e}")
 
@@ -104,9 +113,11 @@ el.replaceWith(d);
         variables: Optional[Dict[str, Any]] = None,
         parallel: int = 10,
         timeout: int = 10,
+        folder: Optional[str] = None,
         prompt: Optional[str] = None,
         on_node_complete: Optional[Callable] = None,
-        logger: Optional[Callable] = None
+        logger: Optional[Callable] = None,
+        name: Optional[str] = None
     ) -> Dict[str, Dict[str, bool]]:
 
         """Run commands and verify expected output on a set of nodes."""
@@ -125,6 +136,7 @@ el.replaceWith(d);
                 vars=variables,
                 parallel=parallel,
                 timeout=timeout,
+                folder=folder,
                 prompt=prompt,
                 on_complete=on_node_complete,
                 logger=logger
@@ -146,37 +158,52 @@ el.replaceWith(d);
             
         return self.run_commands(nodes_filter, commands, parallel=parallel)
 
-    def run_yaml_playbook(self, playbook_path: str, parallel: int = 10) -> Dict[str, Any]:
-        """Run a structured Connpy YAML automation playbook."""
-        if not os.path.exists(playbook_path):
-            raise ConnpyError(f"Playbook file not found: {playbook_path}")
-            
-        try:
-            with open(playbook_path, "r") as f:
-                playbook = yaml.load(f, Loader=yaml.FullLoader)
-        except Exception as e:
-            raise ConnpyError(f"Failed to load playbook {playbook_path}: {e}")
+    def run_yaml_playbook(self, playbook_data: str, parallel: int = 10) -> Dict[str, Any]:
+        """Run a structured Connpy YAML automation playbook (from path or content)."""
+        playbook = None
+        if playbook_data.startswith("---YAML---\n"):
+            try:
+                content = playbook_data[len("---YAML---\n"):]
+                playbook = yaml.load(content, Loader=yaml.FullLoader)
+            except Exception as e:
+                raise ConnpyError(f"Failed to parse YAML content: {e}")
+        else:
+            if not os.path.exists(playbook_data):
+                raise ConnpyError(f"Playbook file not found: {playbook_data}")
+            try:
+                with open(playbook_data, "r") as f:
+                    playbook = yaml.load(f, Loader=yaml.FullLoader)
+            except Exception as e:
+                raise ConnpyError(f"Failed to load playbook {playbook_data}: {e}")
             
         # Basic validation
         if not isinstance(playbook, dict) or "nodes" not in playbook or "commands" not in playbook:
             raise ConnpyError("Invalid playbook format: missing 'nodes' or 'commands' keys.")
             
         action = playbook.get("action", "run")
+        options = playbook.get("options", {})
+        
+        # Extract all fields similar to RunHandler.cli_run
+        exec_args = {
+            "nodes_filter": playbook["nodes"],
+            "commands": playbook["commands"],
+            "variables": playbook.get("variables"),
+            "parallel": options.get("parallel", parallel),
+            "timeout": playbook.get("timeout", options.get("timeout", 10)),
+            "prompt": options.get("prompt"),
+            "name": playbook.get("name", "Task")
+        }
+
+        # Map 'output' field to folder path if it's not stdout/null
+        output_cfg = playbook.get("output")
+        if output_cfg not in [None, "stdout"]:
+            exec_args["folder"] = output_cfg
+
         if action == "run":
-            return self.run_commands(
-                nodes_filter=playbook["nodes"],
-                commands=playbook["commands"],
-                parallel=parallel,
-                timeout=playbook.get("timeout", 10)
-            )
+            return self.run_commands(**exec_args)
         elif action == "test":
-            return self.test_commands(
-                nodes_filter=playbook["nodes"],
-                commands=playbook["commands"],
-                expected=playbook.get("expected", []),
-                parallel=parallel,
-                timeout=playbook.get("timeout", 10)
-            )
+            exec_args["expected"] = playbook.get("expected", [])
+            return self.test_commands(**exec_args)
         else:
             raise ConnpyError(f"Unsupported playbook action: {action}")
@@ -217,7 +244,7 @@ el.replaceWith(d);

Run a plain-text script containing one command per line.

-def run_commands(self,
nodes_filter: str,
commands: List[str],
variables: Dict[str, Any] | None = None,
parallel: int = 10,
timeout: int = 10,
folder: str | None = None,
prompt: str | None = None,
on_node_complete: Callable | None = None,
logger: Callable | None = None) ‑> Dict[str, str]
+def run_commands(self,
nodes_filter: str,
commands: List[str],
variables: Dict[str, Any] | None = None,
parallel: int = 10,
timeout: int = 10,
folder: str | None = None,
prompt: str | None = None,
on_node_complete: Callable | None = None,
logger: Callable | None = None,
name: str | None = None) ‑> Dict[str, str]
@@ -234,7 +261,8 @@ el.replaceWith(d); folder: Optional[str] = None, prompt: Optional[str] = None, on_node_complete: Optional[Callable] = None, - logger: Optional[Callable] = None + logger: Optional[Callable] = None, + name: Optional[str] = None ) -> Dict[str, str]: """Execute commands on a set of nodes.""" @@ -258,58 +286,81 @@ el.replaceWith(d); logger=logger ) - return results + # Combine output and status for the caller + full_results = {} + for unique in results: + full_results[unique] = { + "output": results[unique], + "status": executor.status.get(unique, 1) + } + + return full_results except Exception as e: raise ConnpyError(f"Execution failed: {e}")

Execute commands on a set of nodes.

-def run_yaml_playbook(self, playbook_path: str, parallel: int = 10) ‑> Dict[str, Any] +def run_yaml_playbook(self, playbook_data: str, parallel: int = 10) ‑> Dict[str, Any]
Expand source code -
def run_yaml_playbook(self, playbook_path: str, parallel: int = 10) -> Dict[str, Any]:
-    """Run a structured Connpy YAML automation playbook."""
-    if not os.path.exists(playbook_path):
-        raise ConnpyError(f"Playbook file not found: {playbook_path}")
-        
-    try:
-        with open(playbook_path, "r") as f:
-            playbook = yaml.load(f, Loader=yaml.FullLoader)
-    except Exception as e:
-        raise ConnpyError(f"Failed to load playbook {playbook_path}: {e}")
+
def run_yaml_playbook(self, playbook_data: str, parallel: int = 10) -> Dict[str, Any]:
+    """Run a structured Connpy YAML automation playbook (from path or content)."""
+    playbook = None
+    if playbook_data.startswith("---YAML---\n"):
+        try:
+            content = playbook_data[len("---YAML---\n"):]
+            playbook = yaml.load(content, Loader=yaml.FullLoader)
+        except Exception as e:
+            raise ConnpyError(f"Failed to parse YAML content: {e}")
+    else:
+        if not os.path.exists(playbook_data):
+            raise ConnpyError(f"Playbook file not found: {playbook_data}")
+        try:
+            with open(playbook_data, "r") as f:
+                playbook = yaml.load(f, Loader=yaml.FullLoader)
+        except Exception as e:
+            raise ConnpyError(f"Failed to load playbook {playbook_data}: {e}")
         
     # Basic validation
     if not isinstance(playbook, dict) or "nodes" not in playbook or "commands" not in playbook:
         raise ConnpyError("Invalid playbook format: missing 'nodes' or 'commands' keys.")
         
     action = playbook.get("action", "run")
+    options = playbook.get("options", {})
+    
+    # Extract all fields similar to RunHandler.cli_run
+    exec_args = {
+        "nodes_filter": playbook["nodes"],
+        "commands": playbook["commands"],
+        "variables": playbook.get("variables"),
+        "parallel": options.get("parallel", parallel),
+        "timeout": playbook.get("timeout", options.get("timeout", 10)),
+        "prompt": options.get("prompt"),
+        "name": playbook.get("name", "Task")
+    }
+
+    # Map 'output' field to folder path if it's not stdout/null
+    output_cfg = playbook.get("output")
+    if output_cfg not in [None, "stdout"]:
+        exec_args["folder"] = output_cfg
+
     if action == "run":
-        return self.run_commands(
-            nodes_filter=playbook["nodes"],
-            commands=playbook["commands"],
-            parallel=parallel,
-            timeout=playbook.get("timeout", 10)
-        )
+        return self.run_commands(**exec_args)
     elif action == "test":
-        return self.test_commands(
-            nodes_filter=playbook["nodes"],
-            commands=playbook["commands"],
-            expected=playbook.get("expected", []),
-            parallel=parallel,
-            timeout=playbook.get("timeout", 10)
-        )
+        exec_args["expected"] = playbook.get("expected", [])
+        return self.test_commands(**exec_args)
     else:
         raise ConnpyError(f"Unsupported playbook action: {action}")
-

Run a structured Connpy YAML automation playbook.

+

Run a structured Connpy YAML automation playbook (from path or content).

-def test_commands(self,
nodes_filter: str,
commands: List[str],
expected: List[str],
variables: Dict[str, Any] | None = None,
parallel: int = 10,
timeout: int = 10,
prompt: str | None = None,
on_node_complete: Callable | None = None,
logger: Callable | None = None) ‑> Dict[str, Dict[str, bool]]
+def test_commands(self,
nodes_filter: str,
commands: List[str],
expected: List[str],
variables: Dict[str, Any] | None = None,
parallel: int = 10,
timeout: int = 10,
folder: str | None = None,
prompt: str | None = None,
on_node_complete: Callable | None = None,
logger: Callable | None = None,
name: str | None = None) ‑> Dict[str, Dict[str, bool]]
@@ -324,9 +375,11 @@ el.replaceWith(d); variables: Optional[Dict[str, Any]] = None, parallel: int = 10, timeout: int = 10, + folder: Optional[str] = None, prompt: Optional[str] = None, on_node_complete: Optional[Callable] = None, - logger: Optional[Callable] = None + logger: Optional[Callable] = None, + name: Optional[str] = None ) -> Dict[str, Dict[str, bool]]: """Run commands and verify expected output on a set of nodes.""" @@ -345,6 +398,7 @@ el.replaceWith(d); vars=variables, parallel=parallel, timeout=timeout, + folder=folder, prompt=prompt, on_complete=on_node_complete, logger=logger @@ -395,7 +449,7 @@ el.replaceWith(d); diff --git a/docs/connpy/services/import_export_service.html b/docs/connpy/services/import_export_service.html index c1f8ead..9e3fa76 100644 --- a/docs/connpy/services/import_export_service.html +++ b/docs/connpy/services/import_export_service.html @@ -3,7 +3,7 @@ - + connpy.services.import_export_service API documentation @@ -279,7 +279,7 @@ el.replaceWith(d); diff --git a/docs/connpy/services/index.html b/docs/connpy/services/index.html index 7324357..0cf3b3b 100644 --- a/docs/connpy/services/index.html +++ b/docs/connpy/services/index.html @@ -3,7 +3,7 @@ - + connpy.services API documentation @@ -594,7 +594,8 @@ el.replaceWith(d); folder: Optional[str] = None, prompt: Optional[str] = None, on_node_complete: Optional[Callable] = None, - logger: Optional[Callable] = None + logger: Optional[Callable] = None, + name: Optional[str] = None ) -> Dict[str, str]: """Execute commands on a set of nodes.""" @@ -618,7 +619,15 @@ el.replaceWith(d); logger=logger ) - return results + # Combine output and status for the caller + full_results = {} + for unique in results: + full_results[unique] = { + "output": results[unique], + "status": executor.status.get(unique, 1) + } + + return full_results except Exception as e: raise ConnpyError(f"Execution failed: {e}") @@ -630,9 +639,11 @@ el.replaceWith(d); variables: Optional[Dict[str, Any]] = None, parallel: int = 10, timeout: int = 10, + folder: Optional[str] = None, prompt: Optional[str] = None, on_node_complete: Optional[Callable] = None, - logger: Optional[Callable] = None + logger: Optional[Callable] = None, + name: Optional[str] = None ) -> Dict[str, Dict[str, bool]]: """Run commands and verify expected output on a set of nodes.""" @@ -651,6 +662,7 @@ el.replaceWith(d); vars=variables, parallel=parallel, timeout=timeout, + folder=folder, prompt=prompt, on_complete=on_node_complete, logger=logger @@ -672,37 +684,52 @@ el.replaceWith(d); return self.run_commands(nodes_filter, commands, parallel=parallel) - def run_yaml_playbook(self, playbook_path: str, parallel: int = 10) -> Dict[str, Any]: - """Run a structured Connpy YAML automation playbook.""" - if not os.path.exists(playbook_path): - raise ConnpyError(f"Playbook file not found: {playbook_path}") - - try: - with open(playbook_path, "r") as f: - playbook = yaml.load(f, Loader=yaml.FullLoader) - except Exception as e: - raise ConnpyError(f"Failed to load playbook {playbook_path}: {e}") + def run_yaml_playbook(self, playbook_data: str, parallel: int = 10) -> Dict[str, Any]: + """Run a structured Connpy YAML automation playbook (from path or content).""" + playbook = None + if playbook_data.startswith("---YAML---\n"): + try: + content = playbook_data[len("---YAML---\n"):] + playbook = yaml.load(content, Loader=yaml.FullLoader) + except Exception as e: + raise ConnpyError(f"Failed to parse YAML content: {e}") + else: + if not os.path.exists(playbook_data): + raise ConnpyError(f"Playbook file not found: {playbook_data}") + try: + with open(playbook_data, "r") as f: + playbook = yaml.load(f, Loader=yaml.FullLoader) + except Exception as e: + raise ConnpyError(f"Failed to load playbook {playbook_data}: {e}") # Basic validation if not isinstance(playbook, dict) or "nodes" not in playbook or "commands" not in playbook: raise ConnpyError("Invalid playbook format: missing 'nodes' or 'commands' keys.") action = playbook.get("action", "run") + options = playbook.get("options", {}) + + # Extract all fields similar to RunHandler.cli_run + exec_args = { + "nodes_filter": playbook["nodes"], + "commands": playbook["commands"], + "variables": playbook.get("variables"), + "parallel": options.get("parallel", parallel), + "timeout": playbook.get("timeout", options.get("timeout", 10)), + "prompt": options.get("prompt"), + "name": playbook.get("name", "Task") + } + + # Map 'output' field to folder path if it's not stdout/null + output_cfg = playbook.get("output") + if output_cfg not in [None, "stdout"]: + exec_args["folder"] = output_cfg + if action == "run": - return self.run_commands( - nodes_filter=playbook["nodes"], - commands=playbook["commands"], - parallel=parallel, - timeout=playbook.get("timeout", 10) - ) + return self.run_commands(**exec_args) elif action == "test": - return self.test_commands( - nodes_filter=playbook["nodes"], - commands=playbook["commands"], - expected=playbook.get("expected", []), - parallel=parallel, - timeout=playbook.get("timeout", 10) - ) + exec_args["expected"] = playbook.get("expected", []) + return self.test_commands(**exec_args) else: raise ConnpyError(f"Unsupported playbook action: {action}")
@@ -743,7 +770,7 @@ el.replaceWith(d);

Run a plain-text script containing one command per line.

-def run_commands(self,
nodes_filter: str,
commands: List[str],
variables: Dict[str, Any] | None = None,
parallel: int = 10,
timeout: int = 10,
folder: str | None = None,
prompt: str | None = None,
on_node_complete: Callable | None = None,
logger: Callable | None = None) ‑> Dict[str, str]
+def run_commands(self,
nodes_filter: str,
commands: List[str],
variables: Dict[str, Any] | None = None,
parallel: int = 10,
timeout: int = 10,
folder: str | None = None,
prompt: str | None = None,
on_node_complete: Callable | None = None,
logger: Callable | None = None,
name: str | None = None) ‑> Dict[str, str]
@@ -760,7 +787,8 @@ el.replaceWith(d); folder: Optional[str] = None, prompt: Optional[str] = None, on_node_complete: Optional[Callable] = None, - logger: Optional[Callable] = None + logger: Optional[Callable] = None, + name: Optional[str] = None ) -> Dict[str, str]: """Execute commands on a set of nodes.""" @@ -784,58 +812,81 @@ el.replaceWith(d); logger=logger ) - return results + # Combine output and status for the caller + full_results = {} + for unique in results: + full_results[unique] = { + "output": results[unique], + "status": executor.status.get(unique, 1) + } + + return full_results except Exception as e: raise ConnpyError(f"Execution failed: {e}")

Execute commands on a set of nodes.

-def run_yaml_playbook(self, playbook_path: str, parallel: int = 10) ‑> Dict[str, Any] +def run_yaml_playbook(self, playbook_data: str, parallel: int = 10) ‑> Dict[str, Any]
Expand source code -
def run_yaml_playbook(self, playbook_path: str, parallel: int = 10) -> Dict[str, Any]:
-    """Run a structured Connpy YAML automation playbook."""
-    if not os.path.exists(playbook_path):
-        raise ConnpyError(f"Playbook file not found: {playbook_path}")
-        
-    try:
-        with open(playbook_path, "r") as f:
-            playbook = yaml.load(f, Loader=yaml.FullLoader)
-    except Exception as e:
-        raise ConnpyError(f"Failed to load playbook {playbook_path}: {e}")
+
def run_yaml_playbook(self, playbook_data: str, parallel: int = 10) -> Dict[str, Any]:
+    """Run a structured Connpy YAML automation playbook (from path or content)."""
+    playbook = None
+    if playbook_data.startswith("---YAML---\n"):
+        try:
+            content = playbook_data[len("---YAML---\n"):]
+            playbook = yaml.load(content, Loader=yaml.FullLoader)
+        except Exception as e:
+            raise ConnpyError(f"Failed to parse YAML content: {e}")
+    else:
+        if not os.path.exists(playbook_data):
+            raise ConnpyError(f"Playbook file not found: {playbook_data}")
+        try:
+            with open(playbook_data, "r") as f:
+                playbook = yaml.load(f, Loader=yaml.FullLoader)
+        except Exception as e:
+            raise ConnpyError(f"Failed to load playbook {playbook_data}: {e}")
         
     # Basic validation
     if not isinstance(playbook, dict) or "nodes" not in playbook or "commands" not in playbook:
         raise ConnpyError("Invalid playbook format: missing 'nodes' or 'commands' keys.")
         
     action = playbook.get("action", "run")
+    options = playbook.get("options", {})
+    
+    # Extract all fields similar to RunHandler.cli_run
+    exec_args = {
+        "nodes_filter": playbook["nodes"],
+        "commands": playbook["commands"],
+        "variables": playbook.get("variables"),
+        "parallel": options.get("parallel", parallel),
+        "timeout": playbook.get("timeout", options.get("timeout", 10)),
+        "prompt": options.get("prompt"),
+        "name": playbook.get("name", "Task")
+    }
+
+    # Map 'output' field to folder path if it's not stdout/null
+    output_cfg = playbook.get("output")
+    if output_cfg not in [None, "stdout"]:
+        exec_args["folder"] = output_cfg
+
     if action == "run":
-        return self.run_commands(
-            nodes_filter=playbook["nodes"],
-            commands=playbook["commands"],
-            parallel=parallel,
-            timeout=playbook.get("timeout", 10)
-        )
+        return self.run_commands(**exec_args)
     elif action == "test":
-        return self.test_commands(
-            nodes_filter=playbook["nodes"],
-            commands=playbook["commands"],
-            expected=playbook.get("expected", []),
-            parallel=parallel,
-            timeout=playbook.get("timeout", 10)
-        )
+        exec_args["expected"] = playbook.get("expected", [])
+        return self.test_commands(**exec_args)
     else:
         raise ConnpyError(f"Unsupported playbook action: {action}")
-

Run a structured Connpy YAML automation playbook.

+

Run a structured Connpy YAML automation playbook (from path or content).

-def test_commands(self,
nodes_filter: str,
commands: List[str],
expected: List[str],
variables: Dict[str, Any] | None = None,
parallel: int = 10,
timeout: int = 10,
prompt: str | None = None,
on_node_complete: Callable | None = None,
logger: Callable | None = None) ‑> Dict[str, Dict[str, bool]]
+def test_commands(self,
nodes_filter: str,
commands: List[str],
expected: List[str],
variables: Dict[str, Any] | None = None,
parallel: int = 10,
timeout: int = 10,
folder: str | None = None,
prompt: str | None = None,
on_node_complete: Callable | None = None,
logger: Callable | None = None,
name: str | None = None) ‑> Dict[str, Dict[str, bool]]
@@ -850,9 +901,11 @@ el.replaceWith(d); variables: Optional[Dict[str, Any]] = None, parallel: int = 10, timeout: int = 10, + folder: Optional[str] = None, prompt: Optional[str] = None, on_node_complete: Optional[Callable] = None, - logger: Optional[Callable] = None + logger: Optional[Callable] = None, + name: Optional[str] = None ) -> Dict[str, Dict[str, bool]]: """Run commands and verify expected output on a set of nodes.""" @@ -871,6 +924,7 @@ el.replaceWith(d); vars=variables, parallel=parallel, timeout=timeout, + folder=folder, prompt=prompt, on_complete=on_node_complete, logger=logger @@ -3215,7 +3269,7 @@ el.replaceWith(d); diff --git a/docs/connpy/services/node_service.html b/docs/connpy/services/node_service.html index ecb7904..52e23b1 100644 --- a/docs/connpy/services/node_service.html +++ b/docs/connpy/services/node_service.html @@ -3,7 +3,7 @@ - + connpy.services.node_service API documentation @@ -760,7 +760,7 @@ el.replaceWith(d); diff --git a/docs/connpy/services/plugin_service.html b/docs/connpy/services/plugin_service.html index 962b098..f9e7ddb 100644 --- a/docs/connpy/services/plugin_service.html +++ b/docs/connpy/services/plugin_service.html @@ -3,7 +3,7 @@ - + connpy.services.plugin_service API documentation @@ -671,7 +671,7 @@ el.replaceWith(d); diff --git a/docs/connpy/services/profile_service.html b/docs/connpy/services/profile_service.html index e3f746c..568aec0 100644 --- a/docs/connpy/services/profile_service.html +++ b/docs/connpy/services/profile_service.html @@ -3,7 +3,7 @@ - + connpy.services.profile_service API documentation @@ -429,7 +429,7 @@ el.replaceWith(d); diff --git a/docs/connpy/services/provider.html b/docs/connpy/services/provider.html index aac535c..fa72924 100644 --- a/docs/connpy/services/provider.html +++ b/docs/connpy/services/provider.html @@ -3,7 +3,7 @@ - + connpy.services.provider API documentation @@ -164,7 +164,7 @@ el.replaceWith(d); diff --git a/docs/connpy/services/sync_service.html b/docs/connpy/services/sync_service.html index aac676f..602c65a 100644 --- a/docs/connpy/services/sync_service.html +++ b/docs/connpy/services/sync_service.html @@ -3,7 +3,7 @@ - + connpy.services.sync_service API documentation @@ -964,7 +964,7 @@ el.replaceWith(d); diff --git a/docs/connpy/services/system_service.html b/docs/connpy/services/system_service.html index 95f059e..ded62e1 100644 --- a/docs/connpy/services/system_service.html +++ b/docs/connpy/services/system_service.html @@ -3,7 +3,7 @@ - + connpy.services.system_service API documentation @@ -325,7 +325,7 @@ el.replaceWith(d); diff --git a/docs/connpy/tests/conftest.html b/docs/connpy/tests/conftest.html index bc7eb23..560bf76 100644 --- a/docs/connpy/tests/conftest.html +++ b/docs/connpy/tests/conftest.html @@ -3,7 +3,7 @@ - + connpy.tests.conftest API documentation @@ -258,7 +258,7 @@ def tmp_config_dir(tmp_path): diff --git a/docs/connpy/tests/index.html b/docs/connpy/tests/index.html index 3459003..f60c26a 100644 --- a/docs/connpy/tests/index.html +++ b/docs/connpy/tests/index.html @@ -3,7 +3,7 @@ - + connpy.tests API documentation @@ -152,7 +152,7 @@ el.replaceWith(d); diff --git a/docs/connpy/tests/test_ai.html b/docs/connpy/tests/test_ai.html index 5f7835c..25a9960 100644 --- a/docs/connpy/tests/test_ai.html +++ b/docs/connpy/tests/test_ai.html @@ -3,7 +3,7 @@ - + connpy.tests.test_ai API documentation @@ -1731,7 +1731,7 @@ def myai(self, ai_config, mock_litellm): diff --git a/docs/connpy/tests/test_capture.html b/docs/connpy/tests/test_capture.html index 02093c4..74f5599 100644 --- a/docs/connpy/tests/test_capture.html +++ b/docs/connpy/tests/test_capture.html @@ -3,7 +3,7 @@ - + connpy.tests.test_capture API documentation @@ -245,7 +245,7 @@ def mock_connapp(): diff --git a/docs/connpy/tests/test_completion.html b/docs/connpy/tests/test_completion.html index 7b647dc..df93b0b 100644 --- a/docs/connpy/tests/test_completion.html +++ b/docs/connpy/tests/test_completion.html @@ -3,7 +3,7 @@ - + connpy.tests.test_completion API documentation @@ -257,7 +257,7 @@ el.replaceWith(d); diff --git a/docs/connpy/tests/test_configfile.html b/docs/connpy/tests/test_configfile.html index e5f058b..8fdf443 100644 --- a/docs/connpy/tests/test_configfile.html +++ b/docs/connpy/tests/test_configfile.html @@ -3,7 +3,7 @@ - + connpy.tests.test_configfile API documentation @@ -2005,7 +2005,7 @@ el.replaceWith(d); diff --git a/docs/connpy/tests/test_connapp.html b/docs/connpy/tests/test_connapp.html index 283c4f1..6b36833 100644 --- a/docs/connpy/tests/test_connapp.html +++ b/docs/connpy/tests/test_connapp.html @@ -3,7 +3,7 @@ - + connpy.tests.test_connapp API documentation @@ -699,7 +699,7 @@ def test_run(mock_run_commands, app): diff --git a/docs/connpy/tests/test_core.html b/docs/connpy/tests/test_core.html index de71a53..f988fd0 100644 --- a/docs/connpy/tests/test_core.html +++ b/docs/connpy/tests/test_core.html @@ -3,7 +3,7 @@ - + connpy.tests.test_core API documentation @@ -1369,7 +1369,7 @@ el.replaceWith(d); diff --git a/docs/connpy/tests/test_execution_service.html b/docs/connpy/tests/test_execution_service.html index 9aa4e6a..fae19f0 100644 --- a/docs/connpy/tests/test_execution_service.html +++ b/docs/connpy/tests/test_execution_service.html @@ -3,7 +3,7 @@ - + connpy.tests.test_execution_service API documentation @@ -142,7 +142,7 @@ Regression: ExecutionService.test_commands currently ignores on_node_complete. diff --git a/docs/connpy/tests/test_grpc_layer.html b/docs/connpy/tests/test_grpc_layer.html index c2774bd..cc00645 100644 --- a/docs/connpy/tests/test_grpc_layer.html +++ b/docs/connpy/tests/test_grpc_layer.html @@ -3,7 +3,7 @@ - + connpy.tests.test_grpc_layer API documentation @@ -709,7 +709,7 @@ def test_connect_dynamic_msg_formatting_ssm(self, mock_select, mock_read, mock_s diff --git a/docs/connpy/tests/test_hooks.html b/docs/connpy/tests/test_hooks.html index d953297..32ce5b0 100644 --- a/docs/connpy/tests/test_hooks.html +++ b/docs/connpy/tests/test_hooks.html @@ -3,7 +3,7 @@ - + connpy.tests.test_hooks API documentation @@ -673,7 +673,7 @@ el.replaceWith(d); diff --git a/docs/connpy/tests/test_node_service.html b/docs/connpy/tests/test_node_service.html index 2ae41f3..dfee076 100644 --- a/docs/connpy/tests/test_node_service.html +++ b/docs/connpy/tests/test_node_service.html @@ -3,7 +3,7 @@ - + connpy.tests.test_node_service API documentation @@ -178,7 +178,7 @@ Regression: connapp._mod calls add_node instead of update_node.

diff --git a/docs/connpy/tests/test_plugins.html b/docs/connpy/tests/test_plugins.html index 50e72d6..9417e56 100644 --- a/docs/connpy/tests/test_plugins.html +++ b/docs/connpy/tests/test_plugins.html @@ -3,7 +3,7 @@ - + connpy.tests.test_plugins API documentation @@ -917,7 +917,7 @@ el.replaceWith(d); diff --git a/docs/connpy/tests/test_printer.html b/docs/connpy/tests/test_printer.html index 8d216ba..fd232c8 100644 --- a/docs/connpy/tests/test_printer.html +++ b/docs/connpy/tests/test_printer.html @@ -3,7 +3,7 @@ - + connpy.tests.test_printer API documentation @@ -459,7 +459,7 @@ el.replaceWith(d); diff --git a/docs/connpy/tests/test_printer_concurrency.html b/docs/connpy/tests/test_printer_concurrency.html index 2624923..9d5a1a2 100644 --- a/docs/connpy/tests/test_printer_concurrency.html +++ b/docs/connpy/tests/test_printer_concurrency.html @@ -3,7 +3,7 @@ - + connpy.tests.test_printer_concurrency API documentation @@ -148,7 +148,7 @@ el.replaceWith(d); diff --git a/docs/connpy/tests/test_profile_service.html b/docs/connpy/tests/test_profile_service.html index bb9a8fe..83124f1 100644 --- a/docs/connpy/tests/test_profile_service.html +++ b/docs/connpy/tests/test_profile_service.html @@ -3,7 +3,7 @@ - + connpy.tests.test_profile_service API documentation @@ -192,7 +192,7 @@ Regression: ProfileService currently doesn't resolve inheritance within profiles diff --git a/docs/connpy/tests/test_provider.html b/docs/connpy/tests/test_provider.html index 7d1d16d..3bac6b9 100644 --- a/docs/connpy/tests/test_provider.html +++ b/docs/connpy/tests/test_provider.html @@ -3,7 +3,7 @@ - + connpy.tests.test_provider API documentation @@ -139,7 +139,7 @@ el.replaceWith(d); diff --git a/docs/connpy/tests/test_sync.html b/docs/connpy/tests/test_sync.html index ffab87a..c9a37a6 100644 --- a/docs/connpy/tests/test_sync.html +++ b/docs/connpy/tests/test_sync.html @@ -3,7 +3,7 @@ - + connpy.tests.test_sync API documentation @@ -354,7 +354,7 @@ def test_perform_restore(self, mock_remove, mock_dirname, mock_exists, MockZipFi diff --git a/docs/connpy/tunnels.html b/docs/connpy/tunnels.html index b9921d9..383cbe7 100644 --- a/docs/connpy/tunnels.html +++ b/docs/connpy/tunnels.html @@ -3,7 +3,7 @@ - + connpy.tunnels API documentation @@ -460,7 +460,7 @@ Bridges the blocking gRPC iterators with the async _async_interact_loop.