merge Jumphost, modify license

This commit is contained in:
Federico Luzzi 2023-12-06 10:53:42 -03:00
commit 3e32aa958c
6 changed files with 200 additions and 49 deletions

29
LICENSE
View File

@ -1,21 +1,16 @@
MIT License Custom Software License
Copyright (c) 2022 Fede Luzzi Copyright (c) 2022 Federico Luzzi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, and modify the Software, subject to the following conditions:
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all Commercial Use: The use of the Software for commercial purposes, including but not limited to selling, sublicensing, or generating revenue in any form, is expressly prohibited for individuals and entities other than the copyright holder.
copies or substantial portions of the Software.
Personal and Non-commercial Use: Individuals and entities are permitted to use, copy, and modify the Software for personal and non-commercial purposes.
Distribution: Redistribution of the original or modified Software is allowed, provided the Software is not sold or sublicensed and this license notice is included in all copies or substantial portions of the Software.
Support and Sale: The copyright holder reserves the exclusive right to sell or offer support services for the Software to any company or commercial entity.
Disclaimer: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,2 +1,2 @@
__version__ = "3.6.5" __version__ = "3.7.0"

View File

@ -92,7 +92,7 @@ class configfile:
def _createconfig(self, conf): def _createconfig(self, conf):
#Create config file #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): if not os.path.exists(conf):
with open(conf, "w") as f: with open(conf, "w") as f:
json.dump(defaultconfig, f, indent = 4) json.dump(defaultconfig, f, indent = 4)
@ -238,14 +238,14 @@ class configfile:
return nodes 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 #Add connection from config
if folder == '': 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 == '': 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 != '': 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=''): def _connections_del(self,*, id, folder='', subfolder=''):
@ -274,9 +274,9 @@ class configfile:
del self.connections[folder][subfolder] 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 #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 ): def _profiles_del(self,*, id ):

View File

@ -539,6 +539,7 @@ class connapp:
newnode["options"] = newnodes["options"] newnode["options"] = newnodes["options"]
newnode["logs"] = newnodes["logs"] newnode["logs"] = newnodes["logs"]
newnode["tags"] = newnodes["tags"] newnode["tags"] = newnodes["tags"]
newnode["jumphost"] = newnodes["jumphost"]
newnode["user"] = newnodes["user"] newnode["user"] = newnodes["user"]
newnode["password"] = newnodes["password"] newnode["password"] = newnodes["password"]
count +=1 count +=1
@ -992,6 +993,23 @@ class connapp:
raise inquirer.errors.ValidationError("", reason="Tags should be a python dictionary.".format(current)) raise inquirer.errors.ValidationError("", reason="Tags should be a python dictionary.".format(current))
return True 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): def _default_validation(self, answers, current):
#Default validation type used in multiples questions in inquirer #Default validation type used in multiples questions in inquirer
if current.startswith("@"): if current.startswith("@"):
@ -1039,6 +1057,7 @@ class connapp:
questions.append(inquirer.Confirm("options", message="Edit Options?")) questions.append(inquirer.Confirm("options", message="Edit Options?"))
questions.append(inquirer.Confirm("logs", message="Edit logging path/file?")) questions.append(inquirer.Confirm("logs", message="Edit logging path/file?"))
questions.append(inquirer.Confirm("tags", message="Edit tags?")) 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("user", message="Edit User?"))
questions.append(inquirer.Confirm("password", message="Edit password?")) questions.append(inquirer.Confirm("password", message="Edit password?"))
answers = inquirer.prompt(questions) answers = inquirer.prompt(questions)
@ -1050,11 +1069,13 @@ class connapp:
defaults = self.config.getitem(unique) defaults = self.config.getitem(unique)
if "tags" not in defaults: if "tags" not in defaults:
defaults["tags"] = "" defaults["tags"] = ""
if "jumphost" not in defaults:
defaults["jumphost"] = ""
except: except:
defaults = { "host":"", "protocol":"", "port":"", "user":"", "options":"", "logs":"" , "tags":"", "password":""} defaults = { "host":"", "protocol":"", "port":"", "user":"", "options":"", "logs":"" , "tags":"", "password":"", "jumphost":""}
node = {} node = {}
if edit == None: 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 = [] questions = []
if edit["host"]: if edit["host"]:
questions.append(inquirer.Text("host", message="Add Hostname or IP", validate=self._host_validation, default=defaults["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("}","}}"))) questions.append(inquirer.Text("tags", message="Add tags dictionary", validate=self._tags_validation, default=str(defaults["tags"]).replace("{","{{").replace("}","}}")))
else: else:
node["tags"] = defaults["tags"] 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"]: if edit["user"]:
questions.append(inquirer.Text("user", message="Pick username", validate=self._default_validation, default=defaults["user"])) questions.append(inquirer.Text("user", message="Pick username", validate=self._default_validation, default=defaults["user"]))
else: else:
@ -1118,11 +1143,13 @@ class connapp:
defaults = self.config.profiles[unique] defaults = self.config.profiles[unique]
if "tags" not in defaults: if "tags" not in defaults:
defaults["tags"] = "" defaults["tags"] = ""
if "jumphost" not in defaults:
defaults["jumphost"] = ""
except: except:
defaults = { "host":"", "protocol":"", "port":"", "user":"", "options":"", "logs":"", "tags": "" } defaults = { "host":"", "protocol":"", "port":"", "user":"", "options":"", "logs":"", "tags": "", "jumphost": ""}
profile = {} profile = {}
if edit == None: 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 = [] questions = []
if edit["host"]: if edit["host"]:
questions.append(inquirer.Text("host", message="Add Hostname or IP", default=defaults["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("}","}}"))) questions.append(inquirer.Text("tags", message="Add tags dictionary", validate=self._profile_tags_validation, default=str(defaults["tags"]).replace("{","{{").replace("}","}}")))
else: else:
profile["tags"] = defaults["tags"] 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"]: if edit["user"]:
questions.append(inquirer.Text("user", message="Pick username", default=defaults["user"])) questions.append(inquirer.Text("user", message="Pick username", default=defaults["user"]))
else: 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("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("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("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.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"])) 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) answer = inquirer.prompt(questions)
@ -1201,6 +1233,8 @@ class connapp:
return answer return answer
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 arg_value == None:
raise ValueError("Missing argument node")
if not pat.match(arg_value): if not pat.match(arg_value):
raise ValueError(f"Argument error: {arg_value}") raise ValueError(f"Argument error: {arg_value}")
return arg_value return arg_value

View File

@ -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: ### Parameters:
@ -66,6 +66,8 @@ class node:
- tags (dict) : Tags useful for automation and personal porpuse - tags (dict) : Tags useful for automation and personal porpuse
like "os", "prompt" and "screenleght_command" like "os", "prompt" and "screenleght_command"
- jumphost (str): Reference another node to be used as a jumphost
''' '''
if config == '': if config == '':
self.idletime = 0 self.idletime = 0
@ -74,7 +76,7 @@ class node:
self.idletime = config.config["idletime"] self.idletime = config.config["idletime"]
self.key = config.key self.key = config.key
self.unique = unique 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: for key in attr:
profile = re.search("^@(.*)", str(attr[key])) profile = re.search("^@(.*)", str(attr[key]))
if profile and config != '': if profile and config != '':
@ -97,6 +99,45 @@ class node:
self.password.append(config.profiles[profile.group(1)]["password"]) self.password.append(config.profiles[profile.group(1)]["password"])
else: else:
self.password = [password] 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.jumphost["port"] != '':
jumphost_cmd = jumphost_cmd + " -p " + self.jumphost["port"]
if self.jumphost["options"] != '':
jumphost_cmd = jumphost_cmd + " " + self.jumphost["options"]
if self.jumphost["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): def _passtx(self, passwords, *, keyfile=None):
# decrypts passwords, used by other methdos. # decrypts passwords, used by other methdos.
@ -431,6 +472,8 @@ class node:
cmd = cmd + " " + self.options cmd = cmd + " " + self.options
if self.logs != '': if self.logs != '':
self.logfile = self._logfile() self.logfile = self._logfile()
if self.jumphost != '':
cmd = cmd + " " + self.jumphost
if self.password[0] != '': if self.password[0] != '':
passwords = self._passtx(self.password) passwords = self._passtx(self.password)
else: else:
@ -439,7 +482,7 @@ class node:
cmd = cmd + " {}".format(self.host) cmd = cmd + " {}".format(self.host)
else: else:
cmd = cmd + " {}".format("@".join([self.user,self.host])) cmd = cmd + " {}".format("@".join([self.user,self.host]))
expects = ['yes/no', 'refused', 'supported', 'Invalid|[u|U]sage:', '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", "bad permissions"] expects = ['yes/no', 'refused', 'supported', 'Invalid|[u|U]sage: (ssh|sftp)', '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", "bad permissions"]
elif self.protocol == "telnet": elif self.protocol == "telnet":
cmd = "telnet " + self.host cmd = "telnet " + self.host
if self.port != '': if self.port != '':
@ -452,7 +495,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|timed.out', 'unavailable', 'closed', '[p|P]assword:', r'>$|#$|\$$|>.$|#.$|\$.$', 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching", "bad permissions"] expects = ['[u|U]sername:', 'refused', 'supported', 'invalid option', 'ssh-keygen.*\"', 'timeout|timed.out', 'unavailable', 'closed', '[p|P]assword:', r'>$|#$|\$$|>.$|#.$|\$.$', 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching", "bad permissions"]
else: else:
raise ValueError("Invalid protocol: " + self.protocol) raise ValueError("Invalid protocol: " + self.protocol)
attempts = 1 attempts = 1

View File

@ -1390,7 +1390,7 @@ Categorize the user's request based on the operation they want to perform on
def _createconfig(self, conf): def _createconfig(self, conf):
#Create config file #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): if not os.path.exists(conf):
with open(conf, "w") as f: with open(conf, "w") as f:
json.dump(defaultconfig, f, indent = 4) json.dump(defaultconfig, f, indent = 4)
@ -1536,14 +1536,14 @@ Categorize the user's request based on the operation they want to perform on
return nodes 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 #Add connection from config
if folder == '': 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 == '': 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 != '': 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=''): def _connections_del(self,*, id, folder='', subfolder=''):
@ -1572,9 +1572,9 @@ Categorize the user's request based on the operation they want to perform on
del self.connections[folder][subfolder] 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 #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 ): def _profiles_del(self,*, id ):
@ -2332,6 +2332,7 @@ Categorize the user's request based on the operation they want to perform on
newnode["options"] = newnodes["options"] newnode["options"] = newnodes["options"]
newnode["logs"] = newnodes["logs"] newnode["logs"] = newnodes["logs"]
newnode["tags"] = newnodes["tags"] newnode["tags"] = newnodes["tags"]
newnode["jumphost"] = newnodes["jumphost"]
newnode["user"] = newnodes["user"] newnode["user"] = newnodes["user"]
newnode["password"] = newnodes["password"] newnode["password"] = newnodes["password"]
count +=1 count +=1
@ -2785,6 +2786,23 @@ Categorize the user's request based on the operation they want to perform on
raise inquirer.errors.ValidationError("", reason="Tags should be a python dictionary.".format(current)) raise inquirer.errors.ValidationError("", reason="Tags should be a python dictionary.".format(current))
return True 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): def _default_validation(self, answers, current):
#Default validation type used in multiples questions in inquirer #Default validation type used in multiples questions in inquirer
if current.startswith("@"): if current.startswith("@"):
@ -2832,6 +2850,7 @@ Categorize the user's request based on the operation they want to perform on
questions.append(inquirer.Confirm("options", message="Edit Options?")) questions.append(inquirer.Confirm("options", message="Edit Options?"))
questions.append(inquirer.Confirm("logs", message="Edit logging path/file?")) questions.append(inquirer.Confirm("logs", message="Edit logging path/file?"))
questions.append(inquirer.Confirm("tags", message="Edit tags?")) 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("user", message="Edit User?"))
questions.append(inquirer.Confirm("password", message="Edit password?")) questions.append(inquirer.Confirm("password", message="Edit password?"))
answers = inquirer.prompt(questions) answers = inquirer.prompt(questions)
@ -2843,11 +2862,13 @@ Categorize the user's request based on the operation they want to perform on
defaults = self.config.getitem(unique) defaults = self.config.getitem(unique)
if "tags" not in defaults: if "tags" not in defaults:
defaults["tags"] = "" defaults["tags"] = ""
if "jumphost" not in defaults:
defaults["jumphost"] = ""
except: except:
defaults = { "host":"", "protocol":"", "port":"", "user":"", "options":"", "logs":"" , "tags":"", "password":""} defaults = { "host":"", "protocol":"", "port":"", "user":"", "options":"", "logs":"" , "tags":"", "password":"", "jumphost":""}
node = {} node = {}
if edit == None: 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 = [] questions = []
if edit["host"]: if edit["host"]:
questions.append(inquirer.Text("host", message="Add Hostname or IP", validate=self._host_validation, default=defaults["host"])) questions.append(inquirer.Text("host", message="Add Hostname or IP", validate=self._host_validation, default=defaults["host"]))
@ -2873,6 +2894,10 @@ Categorize the user's request based on the operation they want to perform on
questions.append(inquirer.Text("tags", message="Add tags dictionary", validate=self._tags_validation, default=str(defaults["tags"]).replace("{","{{").replace("}","}}"))) questions.append(inquirer.Text("tags", message="Add tags dictionary", validate=self._tags_validation, default=str(defaults["tags"]).replace("{","{{").replace("}","}}")))
else: else:
node["tags"] = defaults["tags"] 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"]: if edit["user"]:
questions.append(inquirer.Text("user", message="Pick username", validate=self._default_validation, default=defaults["user"])) questions.append(inquirer.Text("user", message="Pick username", validate=self._default_validation, default=defaults["user"]))
else: else:
@ -2911,11 +2936,13 @@ Categorize the user's request based on the operation they want to perform on
defaults = self.config.profiles[unique] defaults = self.config.profiles[unique]
if "tags" not in defaults: if "tags" not in defaults:
defaults["tags"] = "" defaults["tags"] = ""
if "jumphost" not in defaults:
defaults["jumphost"] = ""
except: except:
defaults = { "host":"", "protocol":"", "port":"", "user":"", "options":"", "logs":"", "tags": "" } defaults = { "host":"", "protocol":"", "port":"", "user":"", "options":"", "logs":"", "tags": "", "jumphost": ""}
profile = {} profile = {}
if edit == None: 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 = [] questions = []
if edit["host"]: if edit["host"]:
questions.append(inquirer.Text("host", message="Add Hostname or IP", default=defaults["host"])) questions.append(inquirer.Text("host", message="Add Hostname or IP", default=defaults["host"]))
@ -2941,6 +2968,10 @@ Categorize the user's request based on the operation they want to perform on
questions.append(inquirer.Text("tags", message="Add tags dictionary", validate=self._profile_tags_validation, default=str(defaults["tags"]).replace("{","{{").replace("}","}}"))) questions.append(inquirer.Text("tags", message="Add tags dictionary", validate=self._profile_tags_validation, default=str(defaults["tags"]).replace("{","{{").replace("}","}}")))
else: else:
profile["tags"] = defaults["tags"] 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"]: if edit["user"]:
questions.append(inquirer.Text("user", message="Pick username", default=defaults["user"])) questions.append(inquirer.Text("user", message="Pick username", default=defaults["user"]))
else: else:
@ -2972,6 +3003,7 @@ Categorize the user's request based on the operation they want to perform on
questions.append(inquirer.Text("options", message="Pass extra options to protocol", validate=self._default_validation)) 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("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("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.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"])) 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) answer = inquirer.prompt(questions)
@ -2994,6 +3026,8 @@ Categorize the user's request based on the operation they want to perform on
return answer return answer
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 arg_value == None:
raise ValueError("Missing argument node")
if not pat.match(arg_value): if not pat.match(arg_value):
raise ValueError(f"Argument error: {arg_value}") raise ValueError(f"Argument error: {arg_value}")
return arg_value return arg_value
@ -3326,7 +3360,7 @@ tasks:
</dd> </dd>
<dt id="connpy.node"><code class="flex name class"> <dt id="connpy.node"><code class="flex name class">
<span>class <span class="ident">node</span></span> <span>class <span class="ident">node</span></span>
<span>(</span><span>unique, host, options='', logs='', password='', port='', protocol='', user='', config='', tags='')</span> <span>(</span><span>unique, host, options='', logs='', password='', port='', protocol='', user='', config='', tags='', jumphost='')</span>
</code></dt> </code></dt>
<dd> <dd>
<div class="desc"><p>This class generates a node object. Containts all the information and methods to connect and interact with a device using ssh or telnet.</p> <div class="desc"><p>This class generates a node object. Containts all the information and methods to connect and interact with a device using ssh or telnet.</p>
@ -3369,6 +3403,8 @@ tasks:
- tags (dict) : Tags useful for automation and personal porpuse - tags (dict) : Tags useful for automation and personal porpuse
like "os", "prompt" and "screenleght_command" like "os", "prompt" and "screenleght_command"
- jumphost (str): Reference another node to be used as a jumphost
</code></pre></div> </code></pre></div>
<details class="source"> <details class="source">
<summary> <summary>
@ -3391,7 +3427,7 @@ tasks:
&#39;&#39;&#39; &#39;&#39;&#39;
def __init__(self, unique, host, options=&#39;&#39;, logs=&#39;&#39;, password=&#39;&#39;, port=&#39;&#39;, protocol=&#39;&#39;, user=&#39;&#39;, config=&#39;&#39;, tags=&#39;&#39;): def __init__(self, unique, host, options=&#39;&#39;, logs=&#39;&#39;, password=&#39;&#39;, port=&#39;&#39;, protocol=&#39;&#39;, user=&#39;&#39;, config=&#39;&#39;, tags=&#39;&#39;, jumphost=&#39;&#39;):
&#39;&#39;&#39; &#39;&#39;&#39;
### Parameters: ### Parameters:
@ -3424,6 +3460,8 @@ tasks:
- tags (dict) : Tags useful for automation and personal porpuse - tags (dict) : Tags useful for automation and personal porpuse
like &#34;os&#34;, &#34;prompt&#34; and &#34;screenleght_command&#34; like &#34;os&#34;, &#34;prompt&#34; and &#34;screenleght_command&#34;
- jumphost (str): Reference another node to be used as a jumphost
&#39;&#39;&#39; &#39;&#39;&#39;
if config == &#39;&#39;: if config == &#39;&#39;:
self.idletime = 0 self.idletime = 0
@ -3432,7 +3470,7 @@ tasks:
self.idletime = config.config[&#34;idletime&#34;] self.idletime = config.config[&#34;idletime&#34;]
self.key = config.key self.key = config.key
self.unique = unique self.unique = unique
attr = {&#34;host&#34;: host, &#34;logs&#34;: logs, &#34;options&#34;:options, &#34;port&#34;: port, &#34;protocol&#34;: protocol, &#34;user&#34;: user, &#34;tags&#34;: tags} attr = {&#34;host&#34;: host, &#34;logs&#34;: logs, &#34;options&#34;:options, &#34;port&#34;: port, &#34;protocol&#34;: protocol, &#34;user&#34;: user, &#34;tags&#34;: tags, &#34;jumphost&#34;: jumphost}
for key in attr: for key in attr:
profile = re.search(&#34;^@(.*)&#34;, str(attr[key])) profile = re.search(&#34;^@(.*)&#34;, str(attr[key]))
if profile and config != &#39;&#39;: if profile and config != &#39;&#39;:
@ -3455,6 +3493,45 @@ tasks:
self.password.append(config.profiles[profile.group(1)][&#34;password&#34;]) self.password.append(config.profiles[profile.group(1)][&#34;password&#34;])
else: else:
self.password = [password] self.password = [password]
if self.jumphost != &#34;&#34; and config != &#39;&#39;:
self.jumphost = config.getitem(self.jumphost)
for key in self.jumphost:
profile = re.search(&#34;^@(.*)&#34;, str(self.jumphost[key]))
if profile:
try:
self.jumphost[key] = config.profiles[profile.group(1)][key]
except:
self.jumphost[key] = &#34;&#34;
elif self.jumphost[key] == &#39;&#39; and key == &#34;protocol&#34;:
try:
self.jumphost[key] = config.profiles[&#34;default&#34;][key]
except:
self.jumphost[key] = &#34;ssh&#34;
if isinstance(self.jumphost[&#34;password&#34;],list):
jumphost_password = []
for i, s in enumerate(self.jumphost[&#34;password&#34;]):
profile = re.search(&#34;^@(.*)&#34;, self.jumphost[&#34;password&#34;][i])
if profile:
jumphost_password.append(config.profiles[profile.group(1)][&#34;password&#34;])
self.jumphost[&#34;password&#34;] = jumphost_password
else:
self.jumphost[&#34;password&#34;] = [self.jumphost[&#34;password&#34;]]
if self.jumphost[&#34;password&#34;] != [&#34;&#34;]:
self.password = self.jumphost[&#34;password&#34;] + self.password
if self.jumphost[&#34;protocol&#34;] == &#34;ssh&#34;:
jumphost_cmd = self.jumphost[&#34;protocol&#34;] + &#34; -W %h:%p&#34;
if self.jumphost[&#34;port&#34;] != &#39;&#39;:
jumphost_cmd = jumphost_cmd + &#34; -p &#34; + self.jumphost[&#34;port&#34;]
if self.jumphost[&#34;options&#34;] != &#39;&#39;:
jumphost_cmd = jumphost_cmd + &#34; &#34; + self.jumphost[&#34;options&#34;]
if self.jumphost[&#34;user&#34;] == &#39;&#39;:
jumphost_cmd = jumphost_cmd + &#34; {}&#34;.format(self.jumphost[&#34;host&#34;])
else:
jumphost_cmd = jumphost_cmd + &#34; {}&#34;.format(&#34;@&#34;.join([self.jumphost[&#34;user&#34;],self.jumphost[&#34;host&#34;]]))
self.jumphost = f&#34;-o ProxyCommand=\&#34;{jumphost_cmd}\&#34;&#34;
else:
self.jumphost = &#34;&#34;
def _passtx(self, passwords, *, keyfile=None): def _passtx(self, passwords, *, keyfile=None):
# decrypts passwords, used by other methdos. # decrypts passwords, used by other methdos.
@ -3789,6 +3866,8 @@ tasks:
cmd = cmd + &#34; &#34; + self.options cmd = cmd + &#34; &#34; + self.options
if self.logs != &#39;&#39;: if self.logs != &#39;&#39;:
self.logfile = self._logfile() self.logfile = self._logfile()
if self.jumphost != &#39;&#39;:
cmd = cmd + &#34; &#34; + self.jumphost
if self.password[0] != &#39;&#39;: if self.password[0] != &#39;&#39;:
passwords = self._passtx(self.password) passwords = self._passtx(self.password)
else: else:
@ -3797,7 +3876,7 @@ tasks:
cmd = cmd + &#34; {}&#34;.format(self.host) cmd = cmd + &#34; {}&#34;.format(self.host)
else: else:
cmd = cmd + &#34; {}&#34;.format(&#34;@&#34;.join([self.user,self.host])) cmd = cmd + &#34; {}&#34;.format(&#34;@&#34;.join([self.user,self.host]))
expects = [&#39;yes/no&#39;, &#39;refused&#39;, &#39;supported&#39;, &#39;Invalid|[u|U]sage:&#39;, &#39;ssh-keygen.*\&#34;&#39;, &#39;timeout|timed.out&#39;, &#39;unavailable&#39;, &#39;closed&#39;, &#39;[p|P]assword:|[u|U]sername:&#39;, r&#39;&gt;$|#$|\$$|&gt;.$|#.$|\$.$&#39;, &#39;suspend&#39;, pexpect.EOF, pexpect.TIMEOUT, &#34;No route to host&#34;, &#34;resolve hostname&#34;, &#34;no matching&#34;, &#34;bad permissions&#34;] expects = [&#39;yes/no&#39;, &#39;refused&#39;, &#39;supported&#39;, &#39;Invalid|[u|U]sage: (ssh|sftp)&#39;, &#39;ssh-keygen.*\&#34;&#39;, &#39;timeout|timed.out&#39;, &#39;unavailable&#39;, &#39;closed&#39;, &#39;[p|P]assword:|[u|U]sername:&#39;, r&#39;&gt;$|#$|\$$|&gt;.$|#.$|\$.$&#39;, &#39;suspend&#39;, pexpect.EOF, pexpect.TIMEOUT, &#34;No route to host&#34;, &#34;resolve hostname&#34;, &#34;no matching&#34;, &#34;bad permissions&#34;]
elif self.protocol == &#34;telnet&#34;: elif self.protocol == &#34;telnet&#34;:
cmd = &#34;telnet &#34; + self.host cmd = &#34;telnet &#34; + self.host
if self.port != &#39;&#39;: if self.port != &#39;&#39;:
@ -3810,7 +3889,7 @@ tasks:
passwords = self._passtx(self.password) passwords = self._passtx(self.password)
else: else:
passwords = [] passwords = []
expects = [&#39;[u|U]sername:&#39;, &#39;refused&#39;, &#39;supported&#39;, &#39;cipher&#39;, &#39;ssh-keygen.*\&#34;&#39;, &#39;timeout|timed.out&#39;, &#39;unavailable&#39;, &#39;closed&#39;, &#39;[p|P]assword:&#39;, r&#39;&gt;$|#$|\$$|&gt;.$|#.$|\$.$&#39;, &#39;suspend&#39;, pexpect.EOF, pexpect.TIMEOUT, &#34;No route to host&#34;, &#34;resolve hostname&#34;, &#34;no matching&#34;, &#34;bad permissions&#34;] expects = [&#39;[u|U]sername:&#39;, &#39;refused&#39;, &#39;supported&#39;, &#39;invalid option&#39;, &#39;ssh-keygen.*\&#34;&#39;, &#39;timeout|timed.out&#39;, &#39;unavailable&#39;, &#39;closed&#39;, &#39;[p|P]assword:&#39;, r&#39;&gt;$|#$|\$$|&gt;.$|#.$|\$.$&#39;, &#39;suspend&#39;, pexpect.EOF, pexpect.TIMEOUT, &#34;No route to host&#34;, &#34;resolve hostname&#34;, &#34;no matching&#34;, &#34;bad permissions&#34;]
else: else:
raise ValueError(&#34;Invalid protocol: &#34; + self.protocol) raise ValueError(&#34;Invalid protocol: &#34; + self.protocol)
attempts = 1 attempts = 1