main app done

This commit is contained in:
fluzzi 2022-03-25 12:25:59 -03:00
parent 43e8325890
commit 1c6bdddbdc
5 changed files with 189 additions and 132 deletions

View File

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

View File

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

View File

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

View File

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

13
test.py
View File

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