diff --git a/conn/configfile.py b/conn/configfile.py index 9b52ffc..28dd0c8 100755 --- a/conn/configfile.py +++ b/conn/configfile.py @@ -4,6 +4,7 @@ import yaml import os import re from Crypto.PublicKey import RSA +from pathlib import Path #functions and classes @@ -15,8 +16,8 @@ class configfile: self.defaultdir = home + '/.config/conn' self.defaultfile = self.defaultdir + '/config.yaml' self.defaultkey = self.defaultdir + '/.osk' + Path(self.defaultdir).mkdir(parents=True, exist_ok=True) if conf == None: - self.dir = self.defaultdir self.file = self.defaultfile else: self.file = conf @@ -42,11 +43,12 @@ class configfile: return yaml.load(ymlconf.read(), Loader=yaml.CLoader) def createconfig(self, conf): - defaultconfig = {'config': {'case': False, 'frun': False, 'idletime': 30}, 'connections': {}, 'profiles': { "default": { "host":"", "protocol":"ssh", "port":"", "user":"", "password":"", "options":"", "logs":"" }}} + defaultconfig = {'config': {'case': False, 'idletime': 30}, 'connections': {}, 'profiles': { "default": { "host":"", "protocol":"ssh", "port":"", "user":"", "password":"", "options":"", "logs":"" }}} if not os.path.exists(conf): with open(conf, "w") as f: yaml.dump(defaultconfig, f, explicit_start=True, Dumper=yaml.CDumper) f.close() + os.chmod(conf, 0o600) ymlconf = open(conf) return yaml.load(ymlconf.read(), Loader=yaml.CLoader) @@ -64,6 +66,7 @@ class configfile: with open(keyfile,'wb') as f: f.write(key.export_key('PEM')) f.close() + os.chmod(keyfile, 0o600) def _explode_unique(self, unique): uniques = unique.split("@") @@ -84,6 +87,23 @@ class configfile: return False return result + def getitem(self, unique): + uniques = self._explode_unique(unique) + if unique.startswith("@"): + if uniques.keys() >= {"folder", "subfolder"}: + folder = self.connections[uniques["folder"]][uniques["subfolder"]] + else: + folder = self.connections[uniques["folder"]] + return folder + else: + if uniques.keys() >= {"folder", "subfolder"}: + node = self.connections[uniques["folder"]][uniques["subfolder"]][uniques["id"]] + elif "folder" in uniques.keys(): + node = self.connections[uniques["folder"]][uniques["id"]] + else: + node = self.connections[uniques["id"]] + return node + def _connections_add(self,*, id, host, folder='', subfolder='', options='', logs='', password='', port='', protocol='', user='', type = "connection" ): if folder == '': self.connections[id] = {"host": host, "options": options, "logs": logs, "password": password, "port": port, "protocol": protocol, "user": user, "type": type} diff --git a/conn/connapp.py b/conn/connapp.py index ac71091..ee394c8 100755 --- a/conn/connapp.py +++ b/conn/connapp.py @@ -20,6 +20,7 @@ class connapp: self.nodes = self._getallnodes() self.folders = self._getallfolders() self.profiles = list(self.config.profiles.keys()) + self.case = self.config.config["case"] #DEFAULTPARSER defaultparser = argparse.ArgumentParser(prog = "conn", description = "SSH and Telnet connection manager", formatter_class=argparse.RawTextHelpFormatter) subparsers = defaultparser.add_subparsers(title="Commands") @@ -27,19 +28,20 @@ class connapp: nodeparser = subparsers.add_parser("node", help=self._help("node"),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")) - nodecrud.add_argument("--add", dest="action", action="store_const", help="Add new node[@subfolder][@folder]", const="add", default="connect") - nodecrud.add_argument("--del", "--rm", dest="action", action="store_const", help="Delete node[@subfolder][@folder]", const="del", default="connect") + nodecrud.add_argument("--add", dest="action", action="store_const", help="Add new node[@subfolder][@folder] or [@subfolder]@folder", const="add", default="connect") + nodecrud.add_argument("--del", "--rm", dest="action", action="store_const", help="Delete node[@subfolder][@folder] or [@subfolder]@folder", const="del", default="connect") nodecrud.add_argument("--mod", "--edit", dest="action", action="store_const", help="Modify node[@subfolder][@folder]", const="mod", default="connect") nodecrud.add_argument("--show", dest="action", action="store_const", help="Show node[@subfolder][@folder]", const="show", default="connect") + nodecrud.add_argument("--debug", "-d", dest="action", action="store_const", help="Display all conections steps", const="debug", default="connect") nodeparser.set_defaults(func=self._func_node) #PROFILEPARSER profileparser = subparsers.add_parser("profile", help="Manage profiles") profileparser.add_argument("profile", nargs=1, action=self.store_type, type=self._type_profile, help="Name of profile to manage") profilecrud = profileparser.add_mutually_exclusive_group(required=True) - profilecrud.add_argument("--add", dest="action", action="store_const", help="Add new profile", const="add", default="connect") - profilecrud.add_argument("--del", "--rm", dest="action", action="store_const", help="Delete profile", const="del", default="connect") - profilecrud.add_argument("--mod", "--edit", dest="action", action="store_const", help="Modify profile", const="mod", default="connect") - profilecrud.add_argument("--show", dest="action", action="store_const", help="Show profile", const="show", default="connect") + profilecrud.add_argument("--add", dest="action", action="store_const", help="Add new profile", const="add") + profilecrud.add_argument("--del", "--rm", dest="action", action="store_const", help="Delete profile", const="del") + profilecrud.add_argument("--mod", "--edit", dest="action", action="store_const", help="Modify profile", const="mod") + profilecrud.add_argument("--show", dest="action", action="store_const", help="Show profile", const="show") profileparser.set_defaults(func=self._func_profile) #MOVEPARSER moveparser = subparsers.add_parser("move", aliases=["mv"], help="Move node") @@ -57,8 +59,13 @@ class connapp: bulkparser = subparsers.add_parser("bulk", help="Add nodes in bulk") bulkparser.add_argument("bulk", const="bulk", nargs=0, action=self.store_type, help="Add nodes in bulk") bulkparser.set_defaults(func=self._func_others) + #CONFIGPARSER + configparser = subparsers.add_parser("config", help="Manage app config") + configparser.add_argument("--allow-uppercase", dest="case", nargs=1, action=self.store_type, help="Allow case sensitive names", choices=["true","false"]) + configparser.add_argument("--keepalive", dest="idletime", nargs=1, action=self.store_type, help="Set keepalive time in seconds, 0 to disable", type=int, metavar="INT") + configparser.set_defaults(func=self._func_others) #Set default subparser and tune arguments - commands = ["node", "-h", "--help", "profile", "mv", "move","copy", "cp", "bulk", "ls", "list"] + commands = ["node", "-h", "--help", "profile", "mv", "move","copy", "cp", "bulk", "ls", "list", "config"] profilecmds = ["--add", "--del", "--rm", "--mod", "--edit", "--show"] if len(sys.argv) >= 3 and sys.argv[2] == "profile" and sys.argv[1] in profilecmds: sys.argv[2] = sys.argv[1] @@ -68,8 +75,16 @@ class connapp: args = defaultparser.parse_args() args.func(args) + class store_type(argparse.Action): + def __call__(self, parser, args, values, option_string=None): + setattr(args, "data", values) + delattr(args,self.dest) + setattr(args, "command", self.dest) + def _func_node(self, args): - if args.action == "connect": + if not self.case and args.data != None: + args.data = args.data.lower() + if args.action == "connect" or args.action == "debug": if args.data == None: matches = self.nodes else: @@ -78,26 +93,29 @@ class connapp: else: matches = list(filter(lambda k: k.startswith(args.data), self.nodes)) if len(matches) == 0: - print("ERROR NO MACHEA NI FOLDER NI NODE") - return + print("{} not found".format(args.data)) + exit(1) elif len(matches) > 1: matches[0] = self._choose(matches,"node", "connect") if matches[0] == None: - return - node = self._get_item(matches[0]) + exit(6) + node = self.config.getitem(matches[0]) node = self.node(matches[0],**node, config = self.config) - node.interact() + if args.action == "debug": + node.interact(debug = True) + else: + node.interact() elif args.action == "del": if args.data == None: - print("MISSING ARGUMENT NODE") - return + print("Missing argument node") + exit(2) 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)) if len(matches) == 0: - print("ERROR NO MACHEO NI FOLDER NI NODE") - return + print("{} not found".format(args.data)) + exit(1) question = [inquirer.Confirm("delete", message="Are you sure you want to delete {}?".format(matches[0]))] confirm = inquirer.prompt(question) if confirm["delete"]: @@ -110,28 +128,33 @@ class connapp: print("{} deleted succesfully".format(matches[0])) elif args.action == "add": if args.data == None: - print("MISSING ARGUMENT NODE") - return + print("Missing argument node") + exit(2) elif args.data.startswith("@"): type = "folder" matches = list(filter(lambda k: k == args.data, self.folders)) + reversematches = list(filter(lambda k: "@" + k == args.data, self.nodes)) else: type = "node" matches = list(filter(lambda k: k == args.data, self.nodes)) + reversematches = list(filter(lambda k: k == "@" + args.data, self.folders)) if len(matches) > 0: - print(matches[0] + " ALLREADY EXIST") - return + print("{} already exist".format(matches[0])) + exit(3) + if len(reversematches) > 0: + print("{} already exist".format(reversematches[0])) + exit(3) else: if type == "folder": uniques = self.config._explode_unique(args.data) if uniques == False: print("Invalid folder {}".format(args.data)) - return + exit(4) if "subfolder" in uniques.keys(): parent = "@" + uniques["folder"] if parent not in self.folders: - print("FOLDER {} DONT EXIST".format(uniques["folder"])) - return + print("Folder {} not found".format(uniques["folder"])) + exit(1) self.config._folder_add(**uniques) self.config.saveconfig(self.config.file) print("{} added succesfully".format(args.data)) @@ -140,48 +163,48 @@ class connapp: nodefolder = args.data.partition("@") nodefolder = "@" + nodefolder[2] if nodefolder not in self.folders and nodefolder != "@": - print(nodefolder + " DONT EXIST") - return + print(nodefolder + " not found") + exit(1) uniques = self.config._explode_unique(args.data) if uniques == False: print("Invalid node {}".format(args.data)) - return False + exit(4) print("You can use the configured setting in a profile using @profilename.") print("You can also leave empty any value except hostname/IP.") print("You can pass 1 or more passwords using comma separated @profiles") print("You can use this variables on logging file name: ${id} ${unique} ${host} ${port} ${user} ${protocol}") newnode = self._questions_nodes(args.data, uniques) if newnode == False: - return + exit(6) self.config._connections_add(**newnode) self.config.saveconfig(self.config.file) print("{} added succesfully".format(args.data)) elif args.action == "show": if args.data == None: - print("MISSING ARGUMENT NODE") - return + print("Missing argument node") + exit(2) matches = list(filter(lambda k: k == args.data, self.nodes)) if len(matches) == 0: - print("ERROR NO MACHEO NODE") - return - node = self._get_item(matches[0]) + print("{} not found".format(args.data)) + exit(1) + node = self.config.getitem(matches[0]) print(yaml.dump(node, Dumper=yaml.CDumper)) elif args.action == "mod": if args.data == None: - print("MISSING ARGUMENT NODE") - return + print("Missing argument node") + exit(2) matches = list(filter(lambda k: k == args.data, self.nodes)) if len(matches) == 0: - print("ERROR NO MACHEO NODE") - return - node = self._get_item(matches[0]) + print("{} not found".format(args.data)) + exit(1) + node = self.config.getitem(matches[0]) edits = self._questions_edit() if edits == None: - return + exit(6) uniques = self.config._explode_unique(args.data) updatenode = self._questions_nodes(args.data, uniques, edit=edits) if not updatenode: - return + exit(6) uniques.update(node) if sorted(updatenode.items()) == sorted(uniques.items()): print("Nothing to do here") @@ -193,14 +216,16 @@ class connapp: def _func_profile(self, args): + if not self.case: + args.data[0] = args.data[0].lower() if args.action == "del": matches = list(filter(lambda k: k == args.data[0], self.profiles)) if len(matches) == 0: - print("ERROR NO MACHEO PROFILE") - return + print("{} not found".format(args.data[0])) + exit(1) if matches[0] == "default": - print("CANT DELETE DEFAULT PROFILE") - return + print("Can't delete default profile") + exit(5) question = [inquirer.Confirm("delete", message="Are you sure you want to delete {}?".format(matches[0]))] confirm = inquirer.prompt(question) if confirm["delete"]: @@ -210,35 +235,35 @@ class connapp: elif args.action == "show": matches = list(filter(lambda k: k == args.data[0], self.profiles)) if len(matches) == 0: - print("ERROR NO MACHEO PROFILE") - return + print("{} not found".format(args.data[0])) + exit(1) profile = self.config.profiles[matches[0]] print(yaml.dump(profile, Dumper=yaml.CDumper)) elif args.action == "add": matches = list(filter(lambda k: k == args.data[0], self.profiles)) if len(matches) > 0: print("Profile {} Already exist".format(matches[0])) - return + exit(3) newprofile = self._questions_profiles(args.data[0]) if newprofile == False: - return + exit(6) self.config._profiles_add(**newprofile) self.config.saveconfig(self.config.file) print("{} added succesfully".format(args.data[0])) elif args.action == "mod": matches = list(filter(lambda k: k == args.data[0], self.profiles)) if len(matches) == 0: - print("ERROR NO MACHEO PROFILE") - return + print("{} not found".format(args.data[0])) + exit(1) profile = self.config.profiles[matches[0]] oldprofile = {"id": matches[0]} oldprofile.update(profile) edits = self._questions_edit() if edits == None: - return + exit(6) updateprofile = self._questions_profiles(matches[0], edit=edits) if not updateprofile: - return + exit(6) if sorted(updateprofile.items()) == sorted(oldprofile.items()): print("Nothing to do here") return @@ -251,25 +276,28 @@ class connapp: if args.command == "ls": print(*getattr(self, args.data), sep="\n") elif args.command == "move" or args.command == "cp": + if not self.case: + args.data[0] = args.data[0].lower() + args.data[1] = args.data[1].lower() source = list(filter(lambda k: k == args.data[0], self.nodes)) dest = list(filter(lambda k: k == args.data[1], self.nodes)) if len(source) != 1: - print("ERROR NO MACHEO NODE {}".format(args.data[0])) - return + print("{} not found".format(args.data[0])) + exit(1) if len(dest) > 0: - print("{} ALREADY EXIST".format(args.data[1])) - return + print("Node {} Already exist".format(args.data[1])) + exit(3) nodefolder = args.data[1].partition("@") nodefolder = "@" + nodefolder[2] if nodefolder not in self.folders and nodefolder != "@": - print(nodefolder + " DONT EXIST") - return + print("{} not found".format(nodefolder)) + exit(1) olduniques = self.config._explode_unique(args.data[0]) newuniques = self.config._explode_unique(args.data[1]) if newuniques == False: print("Invalid node {}".format(args.data[1])) - return False - node = self._get_item(source[0]) + exit(4) + node = self.config.getitem(source[0]) newnode = {**newuniques, **node} self.config._connections_add(**newnode) if args.command == "move": @@ -280,11 +308,60 @@ class connapp: if args.command == "cp": print("{} copied succesfully to {}".format(args.data[0],args.data[1])) elif args.command == "bulk": - test = self._questions_bulk() - print(test) + newnodes = self._questions_bulk() + if newnodes == False: + exit(6) + if not self.case: + newnodes["location"] = newnodes["location"].lower() + newnodes["ids"] = newnodes["ids"].lower() + ids = newnodes["ids"].split(",") + hosts = newnodes["host"].split(",") + count = 0 + for n in ids: + unique = n + newnodes["location"] + matches = list(filter(lambda k: k == unique, self.nodes)) + reversematches = list(filter(lambda k: k == "@" + unique, self.folders)) + if len(matches) > 0: + print("Node {} already exist, ignoring it".format(unique)) + continue + if len(reversematches) > 0: + print("Folder with name {} already exist, ignoring it".format(unique)) + continue + newnode = {"id": n} + if newnodes["location"] != "": + location = self.config._explode_unique(newnodes["location"]) + newnode.update(location) + if len(hosts) > 1: + index = ids.index(n) + newnode["host"] = hosts[index] + else: + newnode["host"] = hosts[0] + newnode["protocol"] = newnodes["protocol"] + newnode["port"] = newnodes["port"] + newnode["options"] = newnodes["options"] + newnode["logs"] = newnodes["logs"] + newnode["user"] = newnodes["user"] + newnode["password"] = newnodes["password"] + count +=1 + self.config._connections_add(**newnode) + self.nodes = self._getallnodes() + if count > 0: + self.config.saveconfig(self.config.file) + print("Succesfully added {} nodes".format(count)) + else: + print("0 nodes added") else: - print(args.command) - print(vars(args)) + if args.command == "case": + if args.data[0] == "true": + args.data[0] = True + elif args.data[0] == "false": + args.data[0] = False + if args.command == "idletime": + if args.data[0] < 0: + args.data[0] = 0 + self.config.config[args.command] = args.data[0] + self.config.saveconfig(self.config.file) + print("Config saved") def _choose(self, list, name, action): questions = [inquirer.List(name, message="Pick {} to {}:".format(name,action), choices=list)] @@ -362,7 +439,8 @@ class connapp: return True def _bulk_folder_validation(self, answers, current): - + if not self.case: + current = current.lower() matches = list(filter(lambda k: k == current, self.folders)) if current != "" and len(matches) == 0: raise inquirer.errors.ValidationError("", reason="Location {} don't exist".format(current)) @@ -394,7 +472,7 @@ class connapp: def _questions_nodes(self, unique, uniques = None, edit = None): try: - defaults = self._get_item(unique) + defaults = self.config.getitem(unique) except: defaults = { "host":"", "protocol":"", "port":"", "user":"", "options":"", "logs":"" } node = {} @@ -448,23 +526,6 @@ class connapp: result["type"] = "connection" return result - def _get_item(self, unique): - uniques = self.config._explode_unique(unique) - if unique.startswith("@"): - if uniques.keys() >= {"folder", "subfolder"}: - folder = self.config.connections[uniques["folder"]][uniques["subfolder"]] - else: - folder = self.config.connections[uniques["folder"]] - return folder - else: - if uniques.keys() >= {"folder", "subfolder"}: - node = self.config.connections[uniques["folder"]][uniques["subfolder"]][uniques["id"]] - elif "folder" in uniques.keys(): - node = self.config.connections[uniques["folder"]][uniques["id"]] - else: - node = self.config.connections[uniques["id"]] - return node - def _questions_profiles(self, unique, edit = None): try: defaults = self.config.profiles[unique] @@ -550,12 +611,6 @@ class connapp: raise argparse.ArgumentTypeError return arg_value - class store_type(argparse.Action): - def __call__(self, parser, args, values, option_string=None): - setattr(args, "data", values) - delattr(args,self.dest) - setattr(args, "command", self.dest) - def _help(self, type): if type == "node": return "node[@subfolder][@folder]\nConnect to specific node or show all matching nodes\n[@subfolder][@folder]\nShow all available connections globaly or in specified path" diff --git a/conn/core.py b/conn/core.py index 03e0b12..aaa1415 100755 --- a/conn/core.py +++ b/conn/core.py @@ -45,11 +45,10 @@ class node: self.password = [password] def __passtx(self, passwords, *, keyfile=None): - keyfile = self.key dpass = [] if keyfile is None: keyfile = self.key - else: + if keyfile is not None: key = RSA.import_key(open(keyfile).read()) decryptor = PKCS1_OAEP.new(key) for passwd in passwords: @@ -60,8 +59,7 @@ class node: decrypted = decryptor.decrypt(ast.literal_eval(str(passwd))).decode("utf-8") dpass.append(decrypted) except: - print("Missing or wrong key") - exit(1) + raise ValueError("Missing or corrupted key") return dpass @@ -108,15 +106,18 @@ class node: connect = self._connect(debug = debug) if connect == True: print("Connected to " + self.unique + " at " + self.host + (":" if self.port != '' else '') + self.port + " via: " + self.protocol) - if debug: - self.child.logfile_read = None - elif 'logfile' in dir(self): + if 'logfile' in dir(self): self.child.logfile_read = open(self.logfile, "wb") + elif debug: + self.child.logfile_read = None if 'missingtext' in dir(self): print(self.child.after.decode(), end='') self.child.interact() if "logfile" in dir(self) and not debug: self._logclean(self.logfile) + else: + print(connect) + exit(7) def run(self, commands,*, folder = '', prompt = '>$|#$|\$.$', stdout = False): connect = self._connect() @@ -135,10 +136,9 @@ class node: output = output + self.child.before.decode() + self.child.after.decode() self.child.expect(prompt) output = output + self.child.before.decode() + self.child.after.decode() - if folder == '': - if stdout == True: - print(output) - else: + if stdout == True: + print(output) + if folder != '': with open(folder + "/" + self.unique, "w") as f: f.write(output) f.close() @@ -146,8 +146,6 @@ class node: self.output = output return output - - def _connect(self, debug = False): if self.protocol == "ssh": cmd = "ssh" @@ -182,8 +180,7 @@ class node: passwords = [] expects = ['[u|U]sername:', 'refused', 'supported', 'cipher', 'sage', 'timeout', 'unavailable', 'closed', '[p|P]assword:', '>$|#$|\$.$', 'suspend', pexpect.EOF, "No route to host"] else: - print("Invalid protocol: " + self.protocol) - return + raise ValueError("Invalid protocol: " + self.protocol) child = pexpect.spawn(cmd) if debug: child.logfile_read = sys.stdout.buffer @@ -206,9 +203,8 @@ class node: self.missingtext = True break case 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12: - print("Connection failed code:" + str(results)) child.close() - return + return "Connection failed code:" + str(results) case 8: if len(passwords) > 0: child.sendline(passwords[i]) diff --git a/conn/tools.py b/conn/tools.py deleted file mode 100755 index bab2574..0000000 --- a/conn/tools.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -#Imports -import os -from Crypto.PublicKey import RSA -from Crypto.Cipher import PKCS1_OAEP -import ast - -#functions and classes - -def encrypt(password, keyfile=None): - if keyfile is None: - home = os.path.expanduser("~") - keyfile = home + '/.config/conn/.osk' - key = RSA.import_key(open(keyfile).read()) - publickey = key.publickey() - encryptor = PKCS1_OAEP.new(publickey) - password = encryptor.encrypt(password.encode("utf-8")) - return password - - - diff --git a/test.py b/test.py index 240ea23..382167f 100755 --- a/test.py +++ b/test.py @@ -19,15 +19,15 @@ conf = conn.configfile("test.yaml") # conf.saveconfig("test.yaml") # *** # test = conn.node("test", "10.21.96.45") -# xr=conn.node("xr@home", **conf.connections["home"]["xr"], config=conf) +# xr=conn.node("xr@home", **conf.getitem("xr@home"), config=conf) # ios=conn.node("ios@home", **conf.connections["home"]["ios"], config=conf) # norman = conn.node("norman@home", **conf.connections["home"]["norman"], config=conf) # eve = conn.node("eve@home", **conf.connections["home"]["eve"], config=conf) # router228 = conn.node("router228@bbva", **conf.connections["bbva"]["router228"], config=conf) # router228.interact() # router228.run(["term len 0","show ip int br"]) -# xroutput = xr.run(["show ip bgp", "show ip bgp summ"], folder="test") -# ios.run("show run") +# xroutput = xr.run("show run") +# ios.run("show run", folder=".",stdout=True) # norman.run(["ls -la", "pwd"]) # test = eve.run(["ls -la", "pwd"]) # print(norman.output) @@ -36,3 +36,10 @@ conf = conn.configfile("test.yaml") # test.interact() # *** conn.connapp(conf, conn.node) +# *** +# list = ["xr@home","ios@home","router228@bbva","router142@bbva"] +# for i in list: + # data = conf.getitem(i) + # routeri = conn.node(i,**data,config=conf) + # routeri.run(["term len 0","show run"], folder="test") +