- 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 nodes: #List of nodes to work on. Mandatory
- 'router1@office' #You can add specific nodes - 'router1@office' #You can add specific nodes
- '@aws' #entire folders or subfolders - '@aws' #entire folders or subfolders
- '@office': #or filter inside a folder or subfolder - '@office': #filter inside a folder or subfolder
- 'router2' - 'router2'
- 'router7' - 'router7'
- 'router[0-9]' # Or use regular expressions
commands: #List of commands to send, use {name} to pass variables commands: #List of commands to send, use {name} to pass variables
- 'term len 0' - '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: try:
self.model = self.config.config["openai"]["model"] self.model = self.config.config["openai"]["model"]
except: except:
self.model = "gpt-3.5-turbo-0613" self.model = "gpt-3.5-turbo"
self.temp = temp self.temp = temp
self.__prompt = {} self.__prompt = {}
self.__prompt["original_system"] = """ 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["original_function"]["parameters"]["required"] = ["type", "filter"]
self.__prompt["command_system"] = """ 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. 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., []). 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. 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"]= """ self.__prompt["command_user"]= """
input: show me the full configuration for all this devices: input: show me the full configuration for all this devices:
Devices: OS:
router1: cisco ios 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"] = {}
self.__prompt["command_function"]["name"] = "get_commands" self.__prompt["command_function"]["name"] = "get_commands"
self.__prompt["command_function"]["descriptions"] = """ 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. 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., []). 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 myfunction = False
return myfunction 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. #Parse response for command request to openAI GPT.
info_dict = {} info_dict = {}
info_dict["commands"] = [] info_dict["commands"] = []
info_dict["variables"] = {} info_dict["variables"] = {}
info_dict["variables"]["__global__"] = {} info_dict["variables"]["__global__"] = {}
for key, value in raw_response.items(): for key, value in node_list.items():
key = key.strip()
newvalue = {} 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 newvalue[f"command{i}"] = e
if f"{{command{i}}}" not in info_dict["commands"]: if f"{{command{i}}}" not in info_dict["commands"]:
info_dict["commands"].append(f"{{command{i}}}") 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. #Send the request for commands for each device to openAI GPT.
output_list = [] output_list = []
command_function = deepcopy(self.__prompt["command_function"]) command_function = deepcopy(self.__prompt["command_function"])
node_list = {}
for key, value in nodes.items(): for key, value in nodes.items():
tags = value.get('tags', {}) tags = value.get('tags', {})
try: try:
if os_value := tags.get('os'): if os_value := tags.get('os'):
output_list.append(f"{key}: {os_value}") node_list[key] = os_value
command_function["parameters"]["properties"][key] = {} output_list.append(f"{os_value}")
command_function["parameters"]["properties"][key]["type"] = "array" command_function["parameters"]["properties"][os_value] = {}
command_function["parameters"]["properties"][key]["description"] = f"OS: {os_value}" command_function["parameters"]["properties"][os_value]["type"] = "array"
command_function["parameters"]["properties"][key]["items"] = {} command_function["parameters"]["properties"][os_value]["description"] = f"OS: {os_value}"
command_function["parameters"]["properties"][key]["items"]["type"] = "string" command_function["parameters"]["properties"][os_value]["items"] = {}
command_function["parameters"]["properties"][os_value]["items"]["type"] = "string"
except: except:
pass pass
output_str = "\n".join(output_list) output_str = "\n".join(list(set(output_list)))
command_input = f"input: {user_input}\n\nDevices:\n{output_str}" command_input = f"input: {user_input}\n\nOS:\n{output_str}"
message = [] message = []
message.append({"role": "system", "content": dedent(self.__prompt["command_system"]).strip()}) message.append({"role": "system", "content": dedent(self.__prompt["command_system"]).strip()})
message.append({"role": "user", "content": dedent(self.__prompt["command_user"]).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 = {} output = {}
result = response["choices"][0]["message"].to_dict() result = response["choices"][0]["message"].to_dict()
json_result = json.loads(result["function_call"]["arguments"]) 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 return output
def _get_filter(self, user_input, chat_history = None): def _get_filter(self, user_input, chat_history = None):

View File

@ -72,7 +72,7 @@ class connapp:
#NODEPARSER #NODEPARSER
nodeparser = subparsers.add_parser("node",usage=self._help("usage"), help=self._help("node"),epilog=self._help("end"), formatter_class=argparse.RawTextHelpFormatter) 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() 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("-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("-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") 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 #LISTPARSER
lsparser = subparsers.add_parser("list", aliases=["ls"], help="List profiles, nodes or folders") 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("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) lsparser.set_defaults(func=self._func_others)
#BULKPARSER #BULKPARSER
bulkparser = subparsers.add_parser("bulk", help="Add nodes in bulk") bulkparser = subparsers.add_parser("bulk", help="Add nodes in bulk")
@ -206,24 +208,31 @@ class connapp:
elif args.data.startswith("@"): elif args.data.startswith("@"):
matches = list(filter(lambda k: k == args.data, self.folders)) matches = list(filter(lambda k: k == args.data, self.folders))
else: else:
matches = list(filter(lambda k: k == args.data, self.nodes)) matches = self.config._getallnodes(args.data)
if len(matches) == 0: if len(matches) == 0:
print("{} not found".format(args.data)) print("{} not found".format(args.data))
exit(2) 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) confirm = inquirer.prompt(question)
if confirm == None: if confirm == None:
exit(7) exit(7)
if confirm["delete"]: if confirm["delete"]:
uniques = self.config._explode_unique(matches[0])
if args.data.startswith("@"): if args.data.startswith("@"):
uniques = self.config._explode_unique(matches[0])
self.config._folder_del(**uniques) self.config._folder_del(**uniques)
else: 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) self.config._saveconfig(self.config.file)
if len(matches) == 1:
print("{} deleted succesfully".format(matches[0])) print("{} deleted succesfully".format(matches[0]))
else:
print(f"{len(matches)} nodes deleted succesfully")
def _add(self, args): def _add(self, args):
args.data = self._type_node(args.data)
if args.data == None: if args.data == None:
print("Missing argument node") print("Missing argument node")
exit(3) exit(3)
@ -302,7 +311,6 @@ class connapp:
if args.data == None: if args.data == None:
print("Missing argument node") print("Missing argument node")
exit(3) exit(3)
# matches = list(filter(lambda k: k == args.data, self.nodes))
matches = self.config._getallnodes(args.data) matches = self.config._getallnodes(args.data)
if len(matches) == 0: if len(matches) == 0:
print("No connection found with filter: {}".format(args.data)) print("No connection found with filter: {}".format(args.data))
@ -438,7 +446,30 @@ class connapp:
return actions.get(args.command)(args) return actions.get(args.command)(args)
def _ls(self, 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): def _mvcp(self, args):
if not self.case: if not self.case:
@ -757,14 +788,13 @@ class connapp:
def _node_run(self, args): def _node_run(self, args):
command = " ".join(args.data[1:]) command = " ".join(args.data[1:])
matches = list(filter(lambda k: k == args.data[0], self.nodes)) script = {}
if len(matches) == 0: script["name"] = "Output"
print("{} not found".format(args.data[0])) script["action"] = "run"
exit(2) script["nodes"] = args.data[0]
node = self.config.getitem(matches[0]) script["commands"] = [command]
node = self.node(matches[0],**node, config = self.config) script["output"] = "stdout"
node.run(command) self._cli_run(script)
print(node.output)
def _yaml_generate(self, args): def _yaml_generate(self, args):
if os.path.exists(args.data[0]): if os.path.exists(args.data[0]):
@ -800,7 +830,11 @@ class connapp:
except KeyError as e: except KeyError as e:
print("'{}' is mandatory".format(e.args[0])) print("'{}' is mandatory".format(e.args[0]))
exit(11) 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 stdout = False
if output is None: if output is None:
pass pass
@ -818,9 +852,12 @@ class connapp:
args.update(thisoptions) args.update(thisoptions)
except: except:
options = None options = None
try:
size = str(os.get_terminal_size()) size = str(os.get_terminal_size())
p = re.search(r'.*columns=([0-9]+)', size) p = re.search(r'.*columns=([0-9]+)', size)
columns = int(p.group(1)) columns = int(p.group(1))
except:
columns = 80
if action == "run": if action == "run":
nodes.run(**args) nodes.run(**args)
print(script["name"].upper() + "-" * (columns - len(script["name"]))) 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_.$@#-]+$")): def _type_node(self, arg_value, pat=re.compile(r"^[0-9a-zA-Z_.$@#-]+$")):
if not pat.match(arg_value): if not pat.match(arg_value):
raise argparse.ArgumentTypeError raise ValueError(f"Argument error: {arg_value}")
return arg_value return arg_value
def _type_profile(self, arg_value, pat=re.compile(r"^[0-9a-zA-Z_.$#-]+$")): def _type_profile(self, arg_value, pat=re.compile(r"^[0-9a-zA-Z_.$#-]+$")):
if not pat.match(arg_value): if not pat.match(arg_value):
raise argparse.ArgumentTypeError raise ValueError
return arg_value return arg_value
def _help(self, type): def _help(self, type):

View File

@ -436,7 +436,7 @@ class node:
passwords = self._passtx(self.password) passwords = self._passtx(self.password)
else: else:
passwords = [] 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": elif self.protocol == "telnet":
cmd = "telnet " + self.host cmd = "telnet " + self.host
if self.port != '': if self.port != '':
@ -449,7 +449,7 @@ class node:
passwords = self._passtx(self.password) passwords = self._passtx(self.password)
else: else:
passwords = [] 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: else:
raise ValueError("Invalid protocol: " + self.protocol) raise ValueError("Invalid protocol: " + self.protocol)
attempts = 1 attempts = 1
@ -476,9 +476,6 @@ class node:
else: else:
self.missingtext = True self.missingtext = True
break 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]: if results in [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15]:
child.terminate() child.terminate()
if results == 12 and attempts != max_attempts: if results == 12 and attempts != max_attempts:
@ -486,7 +483,11 @@ class node:
endloop = True endloop = True
break break
else: 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 results == 8:
if len(passwords) > 0: if len(passwords) > 0:
child.sendline(passwords[i]) child.sendline(passwords[i])

View File

@ -639,7 +639,7 @@ __pdoc__ = {
try: try:
self.model = self.config.config["openai"]["model"] self.model = self.config.config["openai"]["model"]
except: except:
self.model = "gpt-3.5-turbo-0613" self.model = "gpt-3.5-turbo"
self.temp = temp self.temp = temp
self.__prompt = {} self.__prompt = {}
self.__prompt["original_system"] = """ 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["original_function"]["parameters"]["required"] = ["type", "filter"]
self.__prompt["command_system"] = """ 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. 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., []). 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. 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"]= """ self.__prompt["command_user"]= """
input: show me the full configuration for all this devices: input: show me the full configuration for all this devices:
Devices: OS:
router1: cisco ios 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"] = {}
self.__prompt["command_function"]["name"] = "get_commands" self.__prompt["command_function"]["name"] = "get_commands"
self.__prompt["command_function"]["descriptions"] = """ 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. 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., []). 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 myfunction = False
return myfunction 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. #Parse response for command request to openAI GPT.
info_dict = {} info_dict = {}
info_dict["commands"] = [] info_dict["commands"] = []
info_dict["variables"] = {} info_dict["variables"] = {}
info_dict["variables"]["__global__"] = {} info_dict["variables"]["__global__"] = {}
for key, value in raw_response.items(): for key, value in node_list.items():
key = key.strip()
newvalue = {} 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 newvalue[f"command{i}"] = e
if f"{{command{i}}}" not in info_dict["commands"]: if f"{{command{i}}}" not in info_dict["commands"]:
info_dict["commands"].append(f"{{command{i}}}") 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. #Send the request for commands for each device to openAI GPT.
output_list = [] output_list = []
command_function = deepcopy(self.__prompt["command_function"]) command_function = deepcopy(self.__prompt["command_function"])
node_list = {}
for key, value in nodes.items(): for key, value in nodes.items():
tags = value.get('tags', {}) tags = value.get('tags', {})
try: try:
if os_value := tags.get('os'): if os_value := tags.get('os'):
output_list.append(f"{key}: {os_value}") node_list[key] = os_value
command_function["parameters"]["properties"][key] = {} output_list.append(f"{os_value}")
command_function["parameters"]["properties"][key]["type"] = "array" command_function["parameters"]["properties"][os_value] = {}
command_function["parameters"]["properties"][key]["description"] = f"OS: {os_value}" command_function["parameters"]["properties"][os_value]["type"] = "array"
command_function["parameters"]["properties"][key]["items"] = {} command_function["parameters"]["properties"][os_value]["description"] = f"OS: {os_value}"
command_function["parameters"]["properties"][key]["items"]["type"] = "string" command_function["parameters"]["properties"][os_value]["items"] = {}
command_function["parameters"]["properties"][os_value]["items"]["type"] = "string"
except: except:
pass pass
output_str = "\n".join(output_list) output_str = "\n".join(list(set(output_list)))
command_input = f"input: {user_input}\n\nDevices:\n{output_str}" command_input = f"input: {user_input}\n\nOS:\n{output_str}"
message = [] message = []
message.append({"role": "system", "content": dedent(self.__prompt["command_system"]).strip()}) message.append({"role": "system", "content": dedent(self.__prompt["command_system"]).strip()})
message.append({"role": "user", "content": dedent(self.__prompt["command_user"]).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 = {} output = {}
result = response["choices"][0]["message"].to_dict() result = response["choices"][0]["message"].to_dict()
json_result = json.loads(result["function_call"]["arguments"]) 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 return output
def _get_filter(self, user_input, chat_history = None): 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
nodeparser = subparsers.add_parser("node",usage=self._help("usage"), help=self._help("node"),epilog=self._help("end"), formatter_class=argparse.RawTextHelpFormatter) 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() 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("-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("-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") 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 #LISTPARSER
lsparser = subparsers.add_parser("list", aliases=["ls"], help="List profiles, nodes or folders") 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("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) lsparser.set_defaults(func=self._func_others)
#BULKPARSER #BULKPARSER
bulkparser = subparsers.add_parser("bulk", help="Add nodes in bulk") 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("@"): elif args.data.startswith("@"):
matches = list(filter(lambda k: k == args.data, self.folders)) matches = list(filter(lambda k: k == args.data, self.folders))
else: else:
matches = list(filter(lambda k: k == args.data, self.nodes)) matches = self.config._getallnodes(args.data)
if len(matches) == 0: if len(matches) == 0:
print("{} not found".format(args.data)) print("{} not found".format(args.data))
exit(2) 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) confirm = inquirer.prompt(question)
if confirm == None: if confirm == None:
exit(7) exit(7)
if confirm["delete"]: if confirm["delete"]:
uniques = self.config._explode_unique(matches[0])
if args.data.startswith("@"): if args.data.startswith("@"):
uniques = self.config._explode_unique(matches[0])
self.config._folder_del(**uniques) self.config._folder_del(**uniques)
else: 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) self.config._saveconfig(self.config.file)
if len(matches) == 1:
print("{} deleted succesfully".format(matches[0])) print("{} deleted succesfully".format(matches[0]))
else:
print(f"{len(matches)} nodes deleted succesfully")
def _add(self, args): def _add(self, args):
args.data = self._type_node(args.data)
if args.data == None: if args.data == None:
print("Missing argument node") print("Missing argument node")
exit(3) exit(3)
@ -2091,7 +2102,6 @@ Categorize the user's request based on the operation they want to perform on
if args.data == None: if args.data == None:
print("Missing argument node") print("Missing argument node")
exit(3) exit(3)
# matches = list(filter(lambda k: k == args.data, self.nodes))
matches = self.config._getallnodes(args.data) matches = self.config._getallnodes(args.data)
if len(matches) == 0: if len(matches) == 0:
print("No connection found with filter: {}".format(args.data)) 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) return actions.get(args.command)(args)
def _ls(self, 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): def _mvcp(self, args):
if not self.case: 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): def _node_run(self, args):
command = " ".join(args.data[1:]) command = " ".join(args.data[1:])
matches = list(filter(lambda k: k == args.data[0], self.nodes)) script = {}
if len(matches) == 0: script["name"] = "Output"
print("{} not found".format(args.data[0])) script["action"] = "run"
exit(2) script["nodes"] = args.data[0]
node = self.config.getitem(matches[0]) script["commands"] = [command]
node = self.node(matches[0],**node, config = self.config) script["output"] = "stdout"
node.run(command) self._cli_run(script)
print(node.output)
def _yaml_generate(self, args): def _yaml_generate(self, args):
if os.path.exists(args.data[0]): 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: except KeyError as e:
print("'{}' is mandatory".format(e.args[0])) print("'{}' is mandatory".format(e.args[0]))
exit(11) 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 stdout = False
if output is None: if output is None:
pass pass
@ -2607,9 +2643,12 @@ Categorize the user's request based on the operation they want to perform on
args.update(thisoptions) args.update(thisoptions)
except: except:
options = None options = None
try:
size = str(os.get_terminal_size()) size = str(os.get_terminal_size())
p = re.search(r'.*columns=([0-9]+)', size) p = re.search(r'.*columns=([0-9]+)', size)
columns = int(p.group(1)) columns = int(p.group(1))
except:
columns = 80
if action == "run": if action == "run":
nodes.run(**args) nodes.run(**args)
print(script["name"].upper() + "-" * (columns - len(script["name"]))) 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_.$@#-]+$")): def _type_node(self, arg_value, pat=re.compile(r"^[0-9a-zA-Z_.$@#-]+$")):
if not pat.match(arg_value): if not pat.match(arg_value):
raise argparse.ArgumentTypeError raise ValueError(f"Argument error: {arg_value}")
return arg_value return arg_value
def _type_profile(self, arg_value, pat=re.compile(r"^[0-9a-zA-Z_.$#-]+$")): def _type_profile(self, arg_value, pat=re.compile(r"^[0-9a-zA-Z_.$#-]+$")):
if not pat.match(arg_value): if not pat.match(arg_value):
raise argparse.ArgumentTypeError raise ValueError
return arg_value return arg_value
def _help(self, type): def _help(self, type):
@ -3190,7 +3229,7 @@ tasks:
#NODEPARSER #NODEPARSER
nodeparser = subparsers.add_parser("node",usage=self._help("usage"), help=self._help("node"),epilog=self._help("end"), formatter_class=argparse.RawTextHelpFormatter) 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() 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("-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("-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") 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 #LISTPARSER
lsparser = subparsers.add_parser("list", aliases=["ls"], help="List profiles, nodes or folders") 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("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) lsparser.set_defaults(func=self._func_others)
#BULKPARSER #BULKPARSER
bulkparser = subparsers.add_parser("bulk", help="Add nodes in bulk") bulkparser = subparsers.add_parser("bulk", help="Add nodes in bulk")
@ -3747,7 +3788,7 @@ tasks:
passwords = self._passtx(self.password) passwords = self._passtx(self.password)
else: else:
passwords = [] 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": elif self.protocol == "telnet":
cmd = "telnet " + self.host cmd = "telnet " + self.host
if self.port != '': if self.port != '':
@ -3760,7 +3801,7 @@ tasks:
passwords = self._passtx(self.password) passwords = self._passtx(self.password)
else: else:
passwords = [] 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: else:
raise ValueError("Invalid protocol: " + self.protocol) raise ValueError("Invalid protocol: " + self.protocol)
attempts = 1 attempts = 1
@ -3787,9 +3828,6 @@ tasks:
else: else:
self.missingtext = True self.missingtext = True
break 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]: if results in [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15]:
child.terminate() child.terminate()
if results == 12 and attempts != max_attempts: if results == 12 and attempts != max_attempts:
@ -3797,7 +3835,11 @@ tasks:
endloop = True endloop = True
break break
else: 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 results == 8:
if len(passwords) > 0: if len(passwords) > 0:
child.sendline(passwords[i]) child.sendline(passwords[i])