diff --git a/connpy/_version.py b/connpy/_version.py index 5065167..f265519 100644 --- a/connpy/_version.py +++ b/connpy/_version.py @@ -1,2 +1,2 @@ -__version__ = "3.6.4" +__version__ = "3.7.0b1" diff --git a/connpy/configfile.py b/connpy/configfile.py index 68ef61a..074eceb 100755 --- a/connpy/configfile.py +++ b/connpy/configfile.py @@ -92,7 +92,7 @@ class configfile: def _createconfig(self, conf): #Create config file - defaultconfig = {'config': {'case': False, 'idletime': 30, 'fzf': False}, 'connections': {}, 'profiles': { "default": { "host":"", "protocol":"ssh", "port":"", "user":"", "password":"", "options":"", "logs":"", "tags": "" }}} + defaultconfig = {'config': {'case': False, 'idletime': 30, 'fzf': False}, 'connections': {}, 'profiles': { "default": { "host":"", "protocol":"ssh", "port":"", "user":"", "password":"", "options":"", "logs":"", "tags": "", "jumphost":""}}} if not os.path.exists(conf): with open(conf, "w") as f: json.dump(defaultconfig, f, indent = 4) @@ -238,14 +238,14 @@ class configfile: return nodes - def _connections_add(self,*, id, host, folder='', subfolder='', options='', logs='', password='', port='', protocol='', user='', tags='', type = "connection" ): + def _connections_add(self,*, id, host, folder='', subfolder='', options='', logs='', password='', port='', protocol='', user='', tags='', jumphost='', type = "connection" ): #Add connection from config if folder == '': - self.connections[id] = {"host": host, "options": options, "logs": logs, "password": password, "port": port, "protocol": protocol, "user": user, "tags": tags,"type": type} + self.connections[id] = {"host": host, "options": options, "logs": logs, "password": password, "port": port, "protocol": protocol, "user": user, "tags": tags,"jumphost": jumphost,"type": type} elif folder != '' and subfolder == '': - self.connections[folder][id] = {"host": host, "options": options, "logs": logs, "password": password, "port": port, "protocol": protocol, "user": user, "tags": tags, "type": type} + self.connections[folder][id] = {"host": host, "options": options, "logs": logs, "password": password, "port": port, "protocol": protocol, "user": user, "tags": tags, "jumphost": jumphost, "type": type} elif folder != '' and subfolder != '': - self.connections[folder][subfolder][id] = {"host": host, "options": options, "logs": logs, "password": password, "port": port, "protocol": protocol, "user": user, "tags": tags, "type": type} + self.connections[folder][subfolder][id] = {"host": host, "options": options, "logs": logs, "password": password, "port": port, "protocol": protocol, "user": user, "tags": tags, "jumphost": jumphost, "type": type} def _connections_del(self,*, id, folder='', subfolder=''): @@ -274,9 +274,9 @@ class configfile: del self.connections[folder][subfolder] - def _profiles_add(self,*, id, host = '', options='', logs='', password='', port='', protocol='', user='', tags='' ): + def _profiles_add(self,*, id, host = '', options='', logs='', password='', port='', protocol='', user='', tags='', jumphost='' ): #Add profile from config - self.profiles[id] = {"host": host, "options": options, "logs": logs, "password": password, "port": port, "protocol": protocol, "user": user, "tags": tags} + self.profiles[id] = {"host": host, "options": options, "logs": logs, "password": password, "port": port, "protocol": protocol, "user": user, "tags": tags, "jumphost": jumphost} def _profiles_del(self,*, id ): diff --git a/connpy/connapp.py b/connpy/connapp.py index 035a722..db4dd7e 100755 --- a/connpy/connapp.py +++ b/connpy/connapp.py @@ -539,6 +539,7 @@ class connapp: newnode["options"] = newnodes["options"] newnode["logs"] = newnodes["logs"] newnode["tags"] = newnodes["tags"] + newnode["jumphost"] = newnodes["jumphost"] newnode["user"] = newnodes["user"] newnode["password"] = newnodes["password"] count +=1 @@ -992,6 +993,23 @@ class connapp: raise inquirer.errors.ValidationError("", reason="Tags should be a python dictionary.".format(current)) return True + def _jumphost_validation(self, answers, current): + #Validation for Jumphost in inquirer when managing nodes + if current.startswith("@"): + if current[1:] not in self.profiles: + raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current)) + elif current != "": + if current not in self.nodes : + raise inquirer.errors.ValidationError("", reason="Node {} don't exist.".format(current)) + return True + + def _profile_jumphost_validation(self, answers, current): + #Validation for Jumphost in inquirer when managing profiles + if current != "": + if current not in self.nodes : + raise inquirer.errors.ValidationError("", reason="Node {} don't exist.".format(current)) + return True + def _default_validation(self, answers, current): #Default validation type used in multiples questions in inquirer if current.startswith("@"): @@ -1039,6 +1057,7 @@ class connapp: questions.append(inquirer.Confirm("options", message="Edit Options?")) questions.append(inquirer.Confirm("logs", message="Edit logging path/file?")) questions.append(inquirer.Confirm("tags", message="Edit tags?")) + questions.append(inquirer.Confirm("jumphost", message="Edit jumphost?")) questions.append(inquirer.Confirm("user", message="Edit User?")) questions.append(inquirer.Confirm("password", message="Edit password?")) answers = inquirer.prompt(questions) @@ -1050,11 +1069,13 @@ class connapp: defaults = self.config.getitem(unique) if "tags" not in defaults: defaults["tags"] = "" + if "jumphost" not in defaults: + defaults["jumphost"] = "" except: - defaults = { "host":"", "protocol":"", "port":"", "user":"", "options":"", "logs":"" , "tags":"", "password":""} + defaults = { "host":"", "protocol":"", "port":"", "user":"", "options":"", "logs":"" , "tags":"", "password":"", "jumphost":""} node = {} if edit == None: - edit = { "host":True, "protocol":True, "port":True, "user":True, "password": True,"options":True, "logs":True, "tags":True } + edit = { "host":True, "protocol":True, "port":True, "user":True, "password": True,"options":True, "logs":True, "tags":True, "jumphost":True } questions = [] if edit["host"]: questions.append(inquirer.Text("host", message="Add Hostname or IP", validate=self._host_validation, default=defaults["host"])) @@ -1080,6 +1101,10 @@ class connapp: questions.append(inquirer.Text("tags", message="Add tags dictionary", validate=self._tags_validation, default=str(defaults["tags"]).replace("{","{{").replace("}","}}"))) else: node["tags"] = defaults["tags"] + if edit["jumphost"]: + questions.append(inquirer.Text("jumphost", message="Add Jumphost node", validate=self._jumphost_validation, default=str(defaults["jumphost"]).replace("{","{{").replace("}","}}"))) + else: + node["jumphost"] = defaults["jumphost"] if edit["user"]: questions.append(inquirer.Text("user", message="Pick username", validate=self._default_validation, default=defaults["user"])) else: @@ -1118,11 +1143,13 @@ class connapp: defaults = self.config.profiles[unique] if "tags" not in defaults: defaults["tags"] = "" + if "jumphost" not in defaults: + defaults["jumphost"] = "" except: - defaults = { "host":"", "protocol":"", "port":"", "user":"", "options":"", "logs":"", "tags": "" } + defaults = { "host":"", "protocol":"", "port":"", "user":"", "options":"", "logs":"", "tags": "", "jumphost": ""} profile = {} if edit == None: - edit = { "host":True, "protocol":True, "port":True, "user":True, "password": True,"options":True, "logs":True, "tags":True } + edit = { "host":True, "protocol":True, "port":True, "user":True, "password": True,"options":True, "logs":True, "tags":True, "jumphost":True } questions = [] if edit["host"]: questions.append(inquirer.Text("host", message="Add Hostname or IP", default=defaults["host"])) @@ -1148,6 +1175,10 @@ class connapp: questions.append(inquirer.Text("tags", message="Add tags dictionary", validate=self._profile_tags_validation, default=str(defaults["tags"]).replace("{","{{").replace("}","}}"))) else: profile["tags"] = defaults["tags"] + if edit["jumphost"]: + questions.append(inquirer.Text("jumphost", message="Add Jumphost node", validate=self._profile_jumphost_validation, default=str(defaults["jumphost"]).replace("{","{{").replace("}","}}"))) + else: + profile["jumphost"] = defaults["jumphost"] if edit["user"]: questions.append(inquirer.Text("user", message="Pick username", default=defaults["user"])) else: @@ -1179,6 +1210,7 @@ class connapp: questions.append(inquirer.Text("options", message="Pass extra options to protocol", validate=self._default_validation)) questions.append(inquirer.Text("logs", message="Pick logging path/file ", validate=self._default_validation)) questions.append(inquirer.Text("tags", message="Add tags dictionary", validate=self._tags_validation)) + questions.append(inquirer.Text("jumphost", message="Add Jumphost node", validate=self._jumphost_validation)) questions.append(inquirer.Text("user", message="Pick username", validate=self._default_validation)) questions.append(inquirer.List("password", message="Password: Use a local password, no password or a list of profiles to reference?", choices=["Local Password", "Profiles", "No Password"])) answer = inquirer.prompt(questions) @@ -1201,6 +1233,8 @@ class connapp: return answer def _type_node(self, arg_value, pat=re.compile(r"^[0-9a-zA-Z_.$@#-]+$")): + if arg_value == None: + raise ValueError("Missing argument node") if not pat.match(arg_value): raise ValueError(f"Argument error: {arg_value}") return arg_value diff --git a/connpy/core.py b/connpy/core.py index e54a716..9f67b2a 100755 --- a/connpy/core.py +++ b/connpy/core.py @@ -33,7 +33,7 @@ class node: ''' - def __init__(self, unique, host, options='', logs='', password='', port='', protocol='', user='', config='', tags=''): + def __init__(self, unique, host, options='', logs='', password='', port='', protocol='', user='', config='', tags='', jumphost=''): ''' ### Parameters: @@ -66,6 +66,8 @@ class node: - tags (dict) : Tags useful for automation and personal porpuse like "os", "prompt" and "screenleght_command" + + - jumphost (str): Reference another node to be used as a jumphost ''' if config == '': self.idletime = 0 @@ -74,7 +76,7 @@ class node: self.idletime = config.config["idletime"] self.key = config.key self.unique = unique - attr = {"host": host, "logs": logs, "options":options, "port": port, "protocol": protocol, "user": user, "tags": tags} + attr = {"host": host, "logs": logs, "options":options, "port": port, "protocol": protocol, "user": user, "tags": tags, "jumphost": jumphost} for key in attr: profile = re.search("^@(.*)", str(attr[key])) if profile and config != '': @@ -97,6 +99,45 @@ class node: self.password.append(config.profiles[profile.group(1)]["password"]) else: self.password = [password] + if self.jumphost != "" and config != '': + self.jumphost = config.getitem(self.jumphost) + for key in self.jumphost: + profile = re.search("^@(.*)", str(self.jumphost[key])) + if profile: + try: + self.jumphost[key] = config.profiles[profile.group(1)][key] + except: + self.jumphost[key] = "" + elif self.jumphost[key] == '' and key == "protocol": + try: + self.jumphost[key] = config.profiles["default"][key] + except: + self.jumphost[key] = "ssh" + if isinstance(self.jumphost["password"],list): + jumphost_password = [] + for i, s in enumerate(self.jumphost["password"]): + profile = re.search("^@(.*)", self.jumphost["password"][i]) + if profile: + jumphost_password.append(config.profiles[profile.group(1)]["password"]) + self.jumphost["password"] = jumphost_password + else: + self.jumphost["password"] = [self.jumphost["password"]] + if self.jumphost["password"] != [""]: + self.password = self.jumphost["password"] + self.password + + if self.jumphost["protocol"] == "ssh": + jumphost_cmd = self.jumphost["protocol"] + " -W %h:%p" + if self.port != '': + jumphost_cmd = jumphost_cmd + " -p " + self.jumphost["port"] + if self.options != '': + jumphost_cmd = jumphost_cmd + " " + self.jumphost["options"] + if self.user == '': + jumphost_cmd = jumphost_cmd + " {}".format(self.jumphost["host"]) + else: + jumphost_cmd = jumphost_cmd + " {}".format("@".join([self.jumphost["user"],self.jumphost["host"]])) + self.jumphost = f"-o ProxyCommand=\"{jumphost_cmd}\"" + else: + self.jumphost = "" def _passtx(self, passwords, *, keyfile=None): # decrypts passwords, used by other methdos. @@ -203,8 +244,8 @@ class node: ''' connect = self._connect(debug = debug) if connect == True: - # size = re.search('columns=([0-9]+).*lines=([0-9]+)',str(os.get_terminal_size())) - # self.child.setwinsize(int(size.group(2)),int(size.group(1))) + size = re.search('columns=([0-9]+).*lines=([0-9]+)',str(os.get_terminal_size())) + self.child.setwinsize(int(size.group(2)),int(size.group(1))) print("Connected to " + self.unique + " at " + self.host + (":" if self.port != '' else '') + self.port + " via: " + self.protocol) if 'logfile' in dir(self): # Initialize self.mylog @@ -431,6 +472,8 @@ class node: cmd = cmd + " " + self.options if self.logs != '': self.logfile = self._logfile() + if self.jumphost != '': + cmd = cmd + " " + self.jumphost if self.password[0] != '': passwords = self._passtx(self.password) else: