diff --git a/automation-template.yaml b/automation-template.yaml index 2275a3e..ad88023 100644 --- a/automation-template.yaml +++ b/automation-template.yaml @@ -7,9 +7,10 @@ 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 + - '@office': #filter inside a folder or subfolder - 'router2' - 'router7' + - 'router[0-9]' # Or use regular expressions commands: #List of commands to send, use {name} to pass variables - 'term len 0' diff --git a/connpy/_version.py b/connpy/_version.py index 5519f1f..4170515 100644 --- a/connpy/_version.py +++ b/connpy/_version.py @@ -1,2 +1,2 @@ -__version__ = "3.5.0" +__version__ = "3.6.0" diff --git a/connpy/ai.py b/connpy/ai.py index 97b7617..c981532 100755 --- a/connpy/ai.py +++ b/connpy/ai.py @@ -66,7 +66,7 @@ class ai: try: self.model = self.config.config["openai"]["model"] except: - self.model = "gpt-3.5-turbo-0613" + self.model = "gpt-3.5-turbo" self.temp = temp self.__prompt = {} self.__prompt["original_system"] = """ @@ -125,7 +125,7 @@ Categorize the user's request based on the operation they want to perform on the """ self.__prompt["original_function"]["parameters"]["required"] = ["type", "filter"] self.__prompt["command_system"] = """ - For each device listed below, provide the command(s) needed to perform the specified action, depending on the device OS (e.g., Cisco IOSXR router, Linux server). + For each OS listed below, provide the command(s) needed to perform the specified action, depending on the device OS (e.g., Cisco IOSXR router, Linux server). The application knows how to connect to devices via SSH, so you only need to provide the command(s) to run after connecting. If the commands needed are not for the specific OS type, just send an empty list (e.g., []). Note: Preserving the integrity of user-provided commands is of utmost importance. If a user has provided a specific command to run, include that command exactly as it was given, even if it's not recognized or understood. Under no circumstances should you modify or alter user-provided commands. @@ -133,14 +133,14 @@ Categorize the user's request based on the operation they want to perform on the self.__prompt["command_user"]= """ input: show me the full configuration for all this devices: - Devices: - router1: cisco ios + OS: + cisco ios: """ - self.__prompt["command_assistant"] = {"name": "get_commands", "arguments": "{\n \"router1\": \"show running-configuration\"\n}"} + self.__prompt["command_assistant"] = {"name": "get_commands", "arguments": "{\n \"cisco ios\": \"show running-configuration\"\n}"} self.__prompt["command_function"] = {} self.__prompt["command_function"]["name"] = "get_commands" self.__prompt["command_function"]["descriptions"] = """ - For each device listed below, provide the command(s) needed to perform the specified action, depending on the device OS (e.g., Cisco IOSXR router, Linux server). + For each OS listed below, provide the command(s) needed to perform the specified action, depending on the device OS (e.g., Cisco IOSXR router, Linux server). The application knows how to connect to devices via SSH, so you only need to provide the command(s) to run after connecting. If the commands needed are not for the specific OS type, just send an empty list (e.g., []). """ @@ -201,16 +201,16 @@ Categorize the user's request based on the operation they want to perform on the myfunction = False return myfunction - def _clean_command_response(self, raw_response): + def _clean_command_response(self, raw_response, node_list): #Parse response for command request to openAI GPT. info_dict = {} info_dict["commands"] = [] info_dict["variables"] = {} info_dict["variables"]["__global__"] = {} - for key, value in raw_response.items(): - key = key.strip() + for key, value in node_list.items(): newvalue = {} - for i,e in enumerate(value, start=1): + commands = raw_response[value] + for i,e in enumerate(commands, start=1): newvalue[f"command{i}"] = e if f"{{command{i}}}" not in info_dict["commands"]: info_dict["commands"].append(f"{{command{i}}}") @@ -222,20 +222,22 @@ Categorize the user's request based on the operation they want to perform on the #Send the request for commands for each device to openAI GPT. output_list = [] command_function = deepcopy(self.__prompt["command_function"]) + node_list = {} for key, value in nodes.items(): tags = value.get('tags', {}) try: if os_value := tags.get('os'): - output_list.append(f"{key}: {os_value}") - command_function["parameters"]["properties"][key] = {} - command_function["parameters"]["properties"][key]["type"] = "array" - command_function["parameters"]["properties"][key]["description"] = f"OS: {os_value}" - command_function["parameters"]["properties"][key]["items"] = {} - command_function["parameters"]["properties"][key]["items"]["type"] = "string" + node_list[key] = os_value + output_list.append(f"{os_value}") + command_function["parameters"]["properties"][os_value] = {} + command_function["parameters"]["properties"][os_value]["type"] = "array" + command_function["parameters"]["properties"][os_value]["description"] = f"OS: {os_value}" + command_function["parameters"]["properties"][os_value]["items"] = {} + command_function["parameters"]["properties"][os_value]["items"]["type"] = "string" except: pass - output_str = "\n".join(output_list) - command_input = f"input: {user_input}\n\nDevices:\n{output_str}" + output_str = "\n".join(list(set(output_list))) + command_input = f"input: {user_input}\n\nOS:\n{output_str}" message = [] message.append({"role": "system", "content": dedent(self.__prompt["command_system"]).strip()}) message.append({"role": "user", "content": dedent(self.__prompt["command_user"]).strip()}) @@ -252,7 +254,7 @@ Categorize the user's request based on the operation they want to perform on the output = {} result = response["choices"][0]["message"].to_dict() json_result = json.loads(result["function_call"]["arguments"]) - output["response"] = self._clean_command_response(json_result) + output["response"] = self._clean_command_response(json_result, node_list) return output def _get_filter(self, user_input, chat_history = None): diff --git a/connpy/connapp.py b/connpy/connapp.py index ec4846e..5492e44 100755 --- a/connpy/connapp.py +++ b/connpy/connapp.py @@ -72,7 +72,7 @@ class connapp: #NODEPARSER nodeparser = subparsers.add_parser("node",usage=self._help("usage"), help=self._help("node"),epilog=self._help("end"), formatter_class=argparse.RawTextHelpFormatter) nodecrud = nodeparser.add_mutually_exclusive_group() - nodeparser.add_argument("node", metavar="node|folder", nargs='?', default=None, action=self._store_type, type=self._type_node, help=self._help("node")) + nodeparser.add_argument("node", metavar="node|folder", nargs='?', default=None, action=self._store_type, help=self._help("node")) nodecrud.add_argument("-v","--version", dest="action", action="store_const", help="Show version", const="version", default="connect") nodecrud.add_argument("-a","--add", dest="action", action="store_const", help="Add new node[@subfolder][@folder] or [@subfolder]@folder", const="add", default="connect") nodecrud.add_argument("-r","--del", "--rm", dest="action", action="store_const", help="Delete node[@subfolder][@folder] or [@subfolder]@folder", const="del", default="connect") @@ -100,6 +100,8 @@ class connapp: #LISTPARSER lsparser = subparsers.add_parser("list", aliases=["ls"], help="List profiles, nodes or folders") lsparser.add_argument("ls", action=self._store_type, choices=["profiles","nodes","folders"], help="List profiles, nodes or folders", default=False) + lsparser.add_argument("--filter", nargs=1, help="Filter results") + lsparser.add_argument("--format", nargs=1, help="Format of the output of nodes using {name}, {NAME}, {location}, {LOCATION}, {host} and {HOST}") lsparser.set_defaults(func=self._func_others) #BULKPARSER bulkparser = subparsers.add_parser("bulk", help="Add nodes in bulk") @@ -206,24 +208,31 @@ class connapp: elif args.data.startswith("@"): matches = list(filter(lambda k: k == args.data, self.folders)) else: - matches = list(filter(lambda k: k == args.data, self.nodes)) + matches = self.config._getallnodes(args.data) if len(matches) == 0: print("{} not found".format(args.data)) exit(2) - question = [inquirer.Confirm("delete", message="Are you sure you want to delete {}?".format(matches[0]))] + print("Removing: {}".format(matches)) + question = [inquirer.Confirm("delete", message="Are you sure you want to continue?")] confirm = inquirer.prompt(question) if confirm == None: exit(7) if confirm["delete"]: - uniques = self.config._explode_unique(matches[0]) if args.data.startswith("@"): + uniques = self.config._explode_unique(matches[0]) self.config._folder_del(**uniques) else: - self.config._connections_del(**uniques) + for node in matches: + nodeuniques = self.config._explode_unique(node) + self.config._connections_del(**nodeuniques) self.config._saveconfig(self.config.file) - print("{} deleted succesfully".format(matches[0])) + if len(matches) == 1: + print("{} deleted succesfully".format(matches[0])) + else: + print(f"{len(matches)} nodes deleted succesfully") def _add(self, args): + args.data = self._type_node(args.data) if args.data == None: print("Missing argument node") exit(3) @@ -302,7 +311,6 @@ class connapp: if args.data == None: print("Missing argument node") exit(3) - # matches = list(filter(lambda k: k == args.data, self.nodes)) matches = self.config._getallnodes(args.data) if len(matches) == 0: print("No connection found with filter: {}".format(args.data)) @@ -438,7 +446,30 @@ class connapp: return actions.get(args.command)(args) def _ls(self, args): - print(*getattr(self, args.data), sep="\n") + items = getattr(self, args.data) + if args.filter: + items = [ item for item in items if re.search(args.filter[0], item)] + if args.format and args.data == "nodes": + newitems = [] + for i in items: + formated = {} + info = self.config.getitem(i) + if "@" in i: + name_part, location_part = i.split("@", 1) + formated["location"] = "@" + location_part + else: + name_part = i + formated["location"] = "" + formated["name"] = name_part + formated["host"] = info["host"] + items_copy = list(formated.items()) + for key, value in items_copy: + upper_key = key.upper() + upper_value = value.upper() + formated[upper_key] = upper_value + newitems.append(args.format[0].format(**formated)) + items = newitems + print(*items, sep="\n") def _mvcp(self, args): if not self.case: @@ -757,14 +788,13 @@ class connapp: def _node_run(self, args): command = " ".join(args.data[1:]) - matches = list(filter(lambda k: k == args.data[0], self.nodes)) - if len(matches) == 0: - print("{} not found".format(args.data[0])) - exit(2) - node = self.config.getitem(matches[0]) - node = self.node(matches[0],**node, config = self.config) - node.run(command) - print(node.output) + script = {} + script["name"] = "Output" + script["action"] = "run" + script["nodes"] = args.data[0] + script["commands"] = [command] + script["output"] = "stdout" + self._cli_run(script) def _yaml_generate(self, args): if os.path.exists(args.data[0]): @@ -800,7 +830,11 @@ class connapp: except KeyError as e: print("'{}' is mandatory".format(e.args[0])) exit(11) - nodes = self.connnodes(self.config.getitems(nodelist), config = self.config) + nodes = self.config._getallnodes(nodelist) + if len(nodes) == 0: + print("{} don't match any node".format(nodelist)) + exit(2) + nodes = self.connnodes(self.config.getitems(nodes), config = self.config) stdout = False if output is None: pass @@ -818,9 +852,12 @@ class connapp: args.update(thisoptions) except: options = None - size = str(os.get_terminal_size()) - p = re.search(r'.*columns=([0-9]+)', size) - columns = int(p.group(1)) + try: + size = str(os.get_terminal_size()) + p = re.search(r'.*columns=([0-9]+)', size) + columns = int(p.group(1)) + except: + columns = 80 if action == "run": nodes.run(**args) print(script["name"].upper() + "-" * (columns - len(script["name"]))) @@ -1162,12 +1199,12 @@ class connapp: def _type_node(self, arg_value, pat=re.compile(r"^[0-9a-zA-Z_.$@#-]+$")): if not pat.match(arg_value): - raise argparse.ArgumentTypeError + raise ValueError(f"Argument error: {arg_value}") return arg_value def _type_profile(self, arg_value, pat=re.compile(r"^[0-9a-zA-Z_.$#-]+$")): if not pat.match(arg_value): - raise argparse.ArgumentTypeError + raise ValueError return arg_value def _help(self, type): diff --git a/connpy/core.py b/connpy/core.py index 05cb5e8..c6f1dd6 100755 --- a/connpy/core.py +++ b/connpy/core.py @@ -436,7 +436,7 @@ class node: passwords = self._passtx(self.password) else: passwords = [] - expects = ['yes/no', 'refused', 'supported', 'cipher', 'ssh-keygen.*\"', 'timeout', 'unavailable', 'closed', '[p|P]assword:|[u|U]sername:', r'>$|#$|\$$|>.$|#.$|\$.$', 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching"] + expects = ['yes/no', 'refused', 'supported', 'cipher', 'ssh-keygen.*\"', 'timeout|timed.out', 'unavailable', 'closed', '[p|P]assword:|[u|U]sername:', r'>$|#$|\$$|>.$|#.$|\$.$', 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching"] elif self.protocol == "telnet": cmd = "telnet " + self.host if self.port != '': @@ -449,7 +449,7 @@ class node: passwords = self._passtx(self.password) else: passwords = [] - expects = ['[u|U]sername:', 'refused', 'supported', 'cipher', 'ssh-keygen.*\"', 'timeout', 'unavailable', 'closed', '[p|P]assword:', r'>$|#$|\$$|>.$|#.$|\$.$', 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching"] + expects = ['[u|U]sername:', 'refused', 'supported', 'cipher', 'ssh-keygen.*\"', 'timeout|timed.out', 'unavailable', 'closed', '[p|P]assword:', r'>$|#$|\$$|>.$|#.$|\$.$', 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching"] else: raise ValueError("Invalid protocol: " + self.protocol) attempts = 1 @@ -476,9 +476,6 @@ class node: else: self.missingtext = True break - if results == 4: - child.terminate() - return "Connection failed code:" + str(results) + "\n" + child.after.decode() if results in [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15]: child.terminate() if results == 12 and attempts != max_attempts: @@ -486,7 +483,11 @@ class node: endloop = True break else: - return "Connection failed code:" + str(results) + if results == 12: + after = "Connection timeout" + else: + after = child.after.decode() + return ("Connection failed code:" + str(results) + "\n" + child.before.decode() + after + child.readline().decode()).rstrip() if results == 8: if len(passwords) > 0: child.sendline(passwords[i]) diff --git a/docs/connpy/index.html b/docs/connpy/index.html index b48023e..aed6d86 100644 --- a/docs/connpy/index.html +++ b/docs/connpy/index.html @@ -639,7 +639,7 @@ __pdoc__ = { try: self.model = self.config.config["openai"]["model"] except: - self.model = "gpt-3.5-turbo-0613" + self.model = "gpt-3.5-turbo" self.temp = temp self.__prompt = {} self.__prompt["original_system"] = """ @@ -698,7 +698,7 @@ Categorize the user's request based on the operation they want to perform on """ self.__prompt["original_function"]["parameters"]["required"] = ["type", "filter"] self.__prompt["command_system"] = """ - For each device listed below, provide the command(s) needed to perform the specified action, depending on the device OS (e.g., Cisco IOSXR router, Linux server). + For each OS listed below, provide the command(s) needed to perform the specified action, depending on the device OS (e.g., Cisco IOSXR router, Linux server). The application knows how to connect to devices via SSH, so you only need to provide the command(s) to run after connecting. If the commands needed are not for the specific OS type, just send an empty list (e.g., []). Note: Preserving the integrity of user-provided commands is of utmost importance. If a user has provided a specific command to run, include that command exactly as it was given, even if it's not recognized or understood. Under no circumstances should you modify or alter user-provided commands. @@ -706,14 +706,14 @@ Categorize the user's request based on the operation they want to perform on self.__prompt["command_user"]= """ input: show me the full configuration for all this devices: - Devices: - router1: cisco ios + OS: + cisco ios: """ - self.__prompt["command_assistant"] = {"name": "get_commands", "arguments": "{\n \"router1\": \"show running-configuration\"\n}"} + self.__prompt["command_assistant"] = {"name": "get_commands", "arguments": "{\n \"cisco ios\": \"show running-configuration\"\n}"} self.__prompt["command_function"] = {} self.__prompt["command_function"]["name"] = "get_commands" self.__prompt["command_function"]["descriptions"] = """ - For each device listed below, provide the command(s) needed to perform the specified action, depending on the device OS (e.g., Cisco IOSXR router, Linux server). + For each OS listed below, provide the command(s) needed to perform the specified action, depending on the device OS (e.g., Cisco IOSXR router, Linux server). The application knows how to connect to devices via SSH, so you only need to provide the command(s) to run after connecting. If the commands needed are not for the specific OS type, just send an empty list (e.g., []). """ @@ -774,16 +774,16 @@ Categorize the user's request based on the operation they want to perform on myfunction = False return myfunction - def _clean_command_response(self, raw_response): + def _clean_command_response(self, raw_response, node_list): #Parse response for command request to openAI GPT. info_dict = {} info_dict["commands"] = [] info_dict["variables"] = {} info_dict["variables"]["__global__"] = {} - for key, value in raw_response.items(): - key = key.strip() + for key, value in node_list.items(): newvalue = {} - for i,e in enumerate(value, start=1): + commands = raw_response[value] + for i,e in enumerate(commands, start=1): newvalue[f"command{i}"] = e if f"{{command{i}}}" not in info_dict["commands"]: info_dict["commands"].append(f"{{command{i}}}") @@ -795,20 +795,22 @@ Categorize the user's request based on the operation they want to perform on #Send the request for commands for each device to openAI GPT. output_list = [] command_function = deepcopy(self.__prompt["command_function"]) + node_list = {} for key, value in nodes.items(): tags = value.get('tags', {}) try: if os_value := tags.get('os'): - output_list.append(f"{key}: {os_value}") - command_function["parameters"]["properties"][key] = {} - command_function["parameters"]["properties"][key]["type"] = "array" - command_function["parameters"]["properties"][key]["description"] = f"OS: {os_value}" - command_function["parameters"]["properties"][key]["items"] = {} - command_function["parameters"]["properties"][key]["items"]["type"] = "string" + node_list[key] = os_value + output_list.append(f"{os_value}") + command_function["parameters"]["properties"][os_value] = {} + command_function["parameters"]["properties"][os_value]["type"] = "array" + command_function["parameters"]["properties"][os_value]["description"] = f"OS: {os_value}" + command_function["parameters"]["properties"][os_value]["items"] = {} + command_function["parameters"]["properties"][os_value]["items"]["type"] = "string" except: pass - output_str = "\n".join(output_list) - command_input = f"input: {user_input}\n\nDevices:\n{output_str}" + output_str = "\n".join(list(set(output_list))) + command_input = f"input: {user_input}\n\nOS:\n{output_str}" message = [] message.append({"role": "system", "content": dedent(self.__prompt["command_system"]).strip()}) message.append({"role": "user", "content": dedent(self.__prompt["command_user"]).strip()}) @@ -825,7 +827,7 @@ Categorize the user's request based on the operation they want to perform on output = {} result = response["choices"][0]["message"].to_dict() json_result = json.loads(result["function_call"]["arguments"]) - output["response"] = self._clean_command_response(json_result) + output["response"] = self._clean_command_response(json_result, node_list) return output def _get_filter(self, user_input, chat_history = None): @@ -1861,7 +1863,7 @@ Categorize the user's request based on the operation they want to perform on #NODEPARSER nodeparser = subparsers.add_parser("node",usage=self._help("usage"), help=self._help("node"),epilog=self._help("end"), formatter_class=argparse.RawTextHelpFormatter) nodecrud = nodeparser.add_mutually_exclusive_group() - nodeparser.add_argument("node", metavar="node|folder", nargs='?', default=None, action=self._store_type, type=self._type_node, help=self._help("node")) + nodeparser.add_argument("node", metavar="node|folder", nargs='?', default=None, action=self._store_type, help=self._help("node")) nodecrud.add_argument("-v","--version", dest="action", action="store_const", help="Show version", const="version", default="connect") nodecrud.add_argument("-a","--add", dest="action", action="store_const", help="Add new node[@subfolder][@folder] or [@subfolder]@folder", const="add", default="connect") nodecrud.add_argument("-r","--del", "--rm", dest="action", action="store_const", help="Delete node[@subfolder][@folder] or [@subfolder]@folder", const="del", default="connect") @@ -1889,6 +1891,8 @@ Categorize the user's request based on the operation they want to perform on #LISTPARSER lsparser = subparsers.add_parser("list", aliases=["ls"], help="List profiles, nodes or folders") lsparser.add_argument("ls", action=self._store_type, choices=["profiles","nodes","folders"], help="List profiles, nodes or folders", default=False) + lsparser.add_argument("--filter", nargs=1, help="Filter results") + lsparser.add_argument("--format", nargs=1, help="Format of the output of nodes using {name}, {NAME}, {location}, {LOCATION}, {host} and {HOST}") lsparser.set_defaults(func=self._func_others) #BULKPARSER bulkparser = subparsers.add_parser("bulk", help="Add nodes in bulk") @@ -1995,24 +1999,31 @@ Categorize the user's request based on the operation they want to perform on elif args.data.startswith("@"): matches = list(filter(lambda k: k == args.data, self.folders)) else: - matches = list(filter(lambda k: k == args.data, self.nodes)) + matches = self.config._getallnodes(args.data) if len(matches) == 0: print("{} not found".format(args.data)) exit(2) - question = [inquirer.Confirm("delete", message="Are you sure you want to delete {}?".format(matches[0]))] + print("Removing: {}".format(matches)) + question = [inquirer.Confirm("delete", message="Are you sure you want to continue?")] confirm = inquirer.prompt(question) if confirm == None: exit(7) if confirm["delete"]: - uniques = self.config._explode_unique(matches[0]) if args.data.startswith("@"): + uniques = self.config._explode_unique(matches[0]) self.config._folder_del(**uniques) else: - self.config._connections_del(**uniques) + for node in matches: + nodeuniques = self.config._explode_unique(node) + self.config._connections_del(**nodeuniques) self.config._saveconfig(self.config.file) - print("{} deleted succesfully".format(matches[0])) + if len(matches) == 1: + print("{} deleted succesfully".format(matches[0])) + else: + print(f"{len(matches)} nodes deleted succesfully") def _add(self, args): + args.data = self._type_node(args.data) if args.data == None: print("Missing argument node") exit(3) @@ -2091,7 +2102,6 @@ Categorize the user's request based on the operation they want to perform on if args.data == None: print("Missing argument node") exit(3) - # matches = list(filter(lambda k: k == args.data, self.nodes)) matches = self.config._getallnodes(args.data) if len(matches) == 0: print("No connection found with filter: {}".format(args.data)) @@ -2227,7 +2237,30 @@ Categorize the user's request based on the operation they want to perform on return actions.get(args.command)(args) def _ls(self, args): - print(*getattr(self, args.data), sep="\n") + items = getattr(self, args.data) + if args.filter: + items = [ item for item in items if re.search(args.filter[0], item)] + if args.format and args.data == "nodes": + newitems = [] + for i in items: + formated = {} + info = self.config.getitem(i) + if "@" in i: + name_part, location_part = i.split("@", 1) + formated["location"] = "@" + location_part + else: + name_part = i + formated["location"] = "" + formated["name"] = name_part + formated["host"] = info["host"] + items_copy = list(formated.items()) + for key, value in items_copy: + upper_key = key.upper() + upper_value = value.upper() + formated[upper_key] = upper_value + newitems.append(args.format[0].format(**formated)) + items = newitems + print(*items, sep="\n") def _mvcp(self, args): if not self.case: @@ -2546,14 +2579,13 @@ Categorize the user's request based on the operation they want to perform on def _node_run(self, args): command = " ".join(args.data[1:]) - matches = list(filter(lambda k: k == args.data[0], self.nodes)) - if len(matches) == 0: - print("{} not found".format(args.data[0])) - exit(2) - node = self.config.getitem(matches[0]) - node = self.node(matches[0],**node, config = self.config) - node.run(command) - print(node.output) + script = {} + script["name"] = "Output" + script["action"] = "run" + script["nodes"] = args.data[0] + script["commands"] = [command] + script["output"] = "stdout" + self._cli_run(script) def _yaml_generate(self, args): if os.path.exists(args.data[0]): @@ -2589,7 +2621,11 @@ Categorize the user's request based on the operation they want to perform on except KeyError as e: print("'{}' is mandatory".format(e.args[0])) exit(11) - nodes = self.connnodes(self.config.getitems(nodelist), config = self.config) + nodes = self.config._getallnodes(nodelist) + if len(nodes) == 0: + print("{} don't match any node".format(nodelist)) + exit(2) + nodes = self.connnodes(self.config.getitems(nodes), config = self.config) stdout = False if output is None: pass @@ -2607,9 +2643,12 @@ Categorize the user's request based on the operation they want to perform on args.update(thisoptions) except: options = None - size = str(os.get_terminal_size()) - p = re.search(r'.*columns=([0-9]+)', size) - columns = int(p.group(1)) + try: + size = str(os.get_terminal_size()) + p = re.search(r'.*columns=([0-9]+)', size) + columns = int(p.group(1)) + except: + columns = 80 if action == "run": nodes.run(**args) print(script["name"].upper() + "-" * (columns - len(script["name"]))) @@ -2951,12 +2990,12 @@ Categorize the user's request based on the operation they want to perform on def _type_node(self, arg_value, pat=re.compile(r"^[0-9a-zA-Z_.$@#-]+$")): if not pat.match(arg_value): - raise argparse.ArgumentTypeError + raise ValueError(f"Argument error: {arg_value}") return arg_value def _type_profile(self, arg_value, pat=re.compile(r"^[0-9a-zA-Z_.$#-]+$")): if not pat.match(arg_value): - raise argparse.ArgumentTypeError + raise ValueError return arg_value def _help(self, type): @@ -3190,7 +3229,7 @@ tasks: #NODEPARSER nodeparser = subparsers.add_parser("node",usage=self._help("usage"), help=self._help("node"),epilog=self._help("end"), formatter_class=argparse.RawTextHelpFormatter) nodecrud = nodeparser.add_mutually_exclusive_group() - nodeparser.add_argument("node", metavar="node|folder", nargs='?', default=None, action=self._store_type, type=self._type_node, help=self._help("node")) + nodeparser.add_argument("node", metavar="node|folder", nargs='?', default=None, action=self._store_type, help=self._help("node")) nodecrud.add_argument("-v","--version", dest="action", action="store_const", help="Show version", const="version", default="connect") nodecrud.add_argument("-a","--add", dest="action", action="store_const", help="Add new node[@subfolder][@folder] or [@subfolder]@folder", const="add", default="connect") nodecrud.add_argument("-r","--del", "--rm", dest="action", action="store_const", help="Delete node[@subfolder][@folder] or [@subfolder]@folder", const="del", default="connect") @@ -3218,6 +3257,8 @@ tasks: #LISTPARSER lsparser = subparsers.add_parser("list", aliases=["ls"], help="List profiles, nodes or folders") lsparser.add_argument("ls", action=self._store_type, choices=["profiles","nodes","folders"], help="List profiles, nodes or folders", default=False) + lsparser.add_argument("--filter", nargs=1, help="Filter results") + lsparser.add_argument("--format", nargs=1, help="Format of the output of nodes using {name}, {NAME}, {location}, {LOCATION}, {host} and {HOST}") lsparser.set_defaults(func=self._func_others) #BULKPARSER bulkparser = subparsers.add_parser("bulk", help="Add nodes in bulk") @@ -3747,7 +3788,7 @@ tasks: passwords = self._passtx(self.password) else: passwords = [] - expects = ['yes/no', 'refused', 'supported', 'cipher', 'ssh-keygen.*\"', 'timeout', 'unavailable', 'closed', '[p|P]assword:|[u|U]sername:', r'>$|#$|\$$|>.$|#.$|\$.$', 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching"] + expects = ['yes/no', 'refused', 'supported', 'cipher', 'ssh-keygen.*\"', 'timeout|timed.out', 'unavailable', 'closed', '[p|P]assword:|[u|U]sername:', r'>$|#$|\$$|>.$|#.$|\$.$', 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching"] elif self.protocol == "telnet": cmd = "telnet " + self.host if self.port != '': @@ -3760,7 +3801,7 @@ tasks: passwords = self._passtx(self.password) else: passwords = [] - expects = ['[u|U]sername:', 'refused', 'supported', 'cipher', 'ssh-keygen.*\"', 'timeout', 'unavailable', 'closed', '[p|P]assword:', r'>$|#$|\$$|>.$|#.$|\$.$', 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching"] + expects = ['[u|U]sername:', 'refused', 'supported', 'cipher', 'ssh-keygen.*\"', 'timeout|timed.out', 'unavailable', 'closed', '[p|P]assword:', r'>$|#$|\$$|>.$|#.$|\$.$', 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching"] else: raise ValueError("Invalid protocol: " + self.protocol) attempts = 1 @@ -3787,9 +3828,6 @@ tasks: else: self.missingtext = True break - if results == 4: - child.terminate() - return "Connection failed code:" + str(results) + "\n" + child.after.decode() if results in [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15]: child.terminate() if results == 12 and attempts != max_attempts: @@ -3797,7 +3835,11 @@ tasks: endloop = True break else: - return "Connection failed code:" + str(results) + if results == 12: + after = "Connection timeout" + else: + after = child.after.decode() + return ("Connection failed code:" + str(results) + "\n" + child.before.decode() + after + child.readline().decode()).rstrip() if results == 8: if len(passwords) > 0: child.sendline(passwords[i])