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 @@
- +