- Fix not allowing to use some regex symbols when passing arguments.
    - Fix AI requests timing out when list of nodes is big.
    - Fix error when forwarding connpy run commands to a file

Features:
    - Improve AI response time changing list of devices to list of OS,
      reducing the lenght of request.
    - Update GPT model to last one.
    - Add filtering option to list command, Also format can be passed to
      format the output as needed.
    - Allow to use regular expresions to match nodes in: run command
      (using yaml file or directly), remove command.
    - When there is a connectivity error, now it shows the error number
      plus the protocol error.
This commit is contained in:
Federico Luzzi 2023-10-26 17:33:44 -03:00
parent d96910092b
commit 00905575fc
6 changed files with 180 additions and 97 deletions

View File

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

View File

@ -1,2 +1,2 @@
__version__ = "3.5.0"
__version__ = "3.6.0"

View File

@ -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):

View File

@ -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):

View File

@ -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])

View File

@ -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])