add kubectl and docker support

This commit is contained in:
2024-06-17 15:58:28 -03:00
parent 3365acb473
commit 89e828451c
4 changed files with 195 additions and 86 deletions

View File

@ -1,2 +1,2 @@
__version__ = "4.0.3" __version__ = "4.1.0b1"

View File

@ -312,11 +312,7 @@ class connapp:
if uniques == False: if uniques == False:
print("Invalid node {}".format(args.data)) print("Invalid node {}".format(args.data))
exit(5) exit(5)
print("You can use the configured setting in a profile using @profilename.") self._print_instructions()
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}")
print("Some useful tags to set for automation are 'os', 'screen_length_command', and 'prompt'.")
newnode = self._questions_nodes(args.data, uniques) newnode = self._questions_nodes(args.data, uniques)
if newnode == False: if newnode == False:
exit(7) exit(7)
@ -1081,16 +1077,16 @@ class connapp:
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current)) raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
return True return True
def _profile_protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^$)"): def _profile_protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^$)"):
#Validate protocol in inquirer when managing profiles #Validate protocol in inquirer when managing profiles
if not re.match(regex, current): if not re.match(regex, current):
raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet or leave empty") raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, kubectl, docker or leave empty")
return True return True
def _protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^$|^@.+$)"): def _protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^$|^@.+$)"):
#Validate protocol in inquirer when managing nodes #Validate protocol in inquirer when managing nodes
if not re.match(regex, current): if not re.match(regex, current):
raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, leave empty or @profile") raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, kubectl, docker leave empty or @profile")
if current.startswith("@"): if current.startswith("@"):
if current[1:] not in self.profiles: if current[1:] not in self.profiles:
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current)) raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
@ -1111,7 +1107,7 @@ class connapp:
def _port_validation(self, answers, current, regex = "(^[0-9]*$|^@.+$)"): def _port_validation(self, answers, current, regex = "(^[0-9]*$|^@.+$)"):
#Validate port in inquirer when managing nodes #Validate port in inquirer when managing nodes
if not re.match(regex, current): if not re.match(regex, current):
raise inquirer.errors.ValidationError("", reason="Pick a port between 1-65535, @profile or leave empty") raise inquirer.errors.ValidationError("", reason="Pick a port between 1-6553/app5, @profile or leave empty")
try: try:
port = int(current) port = int(current)
except: except:
@ -1217,7 +1213,7 @@ class connapp:
#Inquirer questions when editing nodes or profiles #Inquirer questions when editing nodes or profiles
questions = [] questions = []
questions.append(inquirer.Confirm("host", message="Edit Hostname/IP?")) questions.append(inquirer.Confirm("host", message="Edit Hostname/IP?"))
questions.append(inquirer.Confirm("protocol", message="Edit Protocol?")) questions.append(inquirer.Confirm("protocol", message="Edit Protocol/app?"))
questions.append(inquirer.Confirm("port", message="Edit Port?")) questions.append(inquirer.Confirm("port", message="Edit Port?"))
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?"))
@ -1247,7 +1243,7 @@ class connapp:
else: else:
node["host"] = defaults["host"] node["host"] = defaults["host"]
if edit["protocol"]: if edit["protocol"]:
questions.append(inquirer.Text("protocol", message="Select Protocol", validate=self._protocol_validation, default=defaults["protocol"])) questions.append(inquirer.Text("protocol", message="Select Protocol/app", validate=self._protocol_validation, default=defaults["protocol"]))
else: else:
node["protocol"] = defaults["protocol"] node["protocol"] = defaults["protocol"]
if edit["port"]: if edit["port"]:
@ -1255,7 +1251,7 @@ class connapp:
else: else:
node["port"] = defaults["port"] node["port"] = defaults["port"]
if edit["options"]: if edit["options"]:
questions.append(inquirer.Text("options", message="Pass extra options to protocol", validate=self._default_validation, default=defaults["options"])) questions.append(inquirer.Text("options", message="Pass extra options to protocol/app", validate=self._default_validation, default=defaults["options"]))
else: else:
node["options"] = defaults["options"] node["options"] = defaults["options"]
if edit["logs"]: if edit["logs"]:
@ -1321,7 +1317,7 @@ class connapp:
else: else:
profile["host"] = defaults["host"] profile["host"] = defaults["host"]
if edit["protocol"]: if edit["protocol"]:
questions.append(inquirer.Text("protocol", message="Select Protocol", validate=self._profile_protocol_validation, default=defaults["protocol"])) questions.append(inquirer.Text("protocol", message="Select Protocol/app", validate=self._profile_protocol_validation, default=defaults["protocol"]))
else: else:
profile["protocol"] = defaults["protocol"] profile["protocol"] = defaults["protocol"]
if edit["port"]: if edit["port"]:
@ -1329,7 +1325,7 @@ class connapp:
else: else:
profile["port"] = defaults["port"] profile["port"] = defaults["port"]
if edit["options"]: if edit["options"]:
questions.append(inquirer.Text("options", message="Pass extra options to protocol", default=defaults["options"])) questions.append(inquirer.Text("options", message="Pass extra options to protocol/app", default=defaults["options"]))
else: else:
profile["options"] = defaults["options"] profile["options"] = defaults["options"]
if edit["logs"]: if edit["logs"]:
@ -1370,9 +1366,9 @@ class connapp:
questions.append(inquirer.Text("ids", message="add a comma separated list of nodes to add", validate=self._bulk_node_validation)) questions.append(inquirer.Text("ids", message="add a comma separated list of nodes to add", validate=self._bulk_node_validation))
questions.append(inquirer.Text("location", message="Add a @folder, @subfolder@folder or leave empty", validate=self._bulk_folder_validation)) questions.append(inquirer.Text("location", message="Add a @folder, @subfolder@folder or leave empty", validate=self._bulk_folder_validation))
questions.append(inquirer.Text("host", message="Add comma separated list of Hostnames or IPs", validate=self._bulk_host_validation)) questions.append(inquirer.Text("host", message="Add comma separated list of Hostnames or IPs", validate=self._bulk_host_validation))
questions.append(inquirer.Text("protocol", message="Select Protocol", validate=self._protocol_validation)) questions.append(inquirer.Text("protocol", message="Select Protocol/app", validate=self._protocol_validation))
questions.append(inquirer.Text("port", message="Select Port Number", validate=self._port_validation)) questions.append(inquirer.Text("port", message="Select Port Number", validate=self._port_validation))
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/app", 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("jumphost", message="Add Jumphost node", validate=self._jumphost_validation))
@ -1552,3 +1548,45 @@ tasks:
output: null output: null
...''' ...'''
def _print_instructions(self):
instructions = """
Welcome to Connpy node Addition Wizard!
Here are some important instructions and tips for configuring your new node:
1. **Profiles**:
- You can use the configured settings in a profile using `@profilename`.
2. **Available Protocols and Apps**:
- ssh
- telnet
- kubectl (`kubectl exec`)
- docker (`docker exec`)
3. **Optional Values**:
- You can leave any value empty except for the hostname/IP.
4. **Passwords**:
- You can pass one or more passwords using comma-separated `@profiles`.
5. **Logging**:
- You can use the following variables in the logging file name:
- `${id}`
- `${unique}`
- `${host}`
- `${port}`
- `${user}`
- `${protocol}`
6. **Well-Known Tags**:
- `os`: Identified by AI to generate commands based on the operating system.
- `screen_length_command`: Used by automation to avoid pagination on different devices (e.g., `terminal length 0` for Cisco devices).
- `prompt`: Replaces default app prompt to identify the end of output or where the user can start inputting commands.
- `kube_command`: Replaces the default command (`/bin/bash`) for `kubectl exec`.
- `docker_command`: Replaces the default command for `docker exec`.
Please follow these instructions carefully to ensure proper configuration of your new node.
"""
# print(instructions)
mdprint(Markdown(instructions))

View File

@ -57,7 +57,7 @@ class node:
- port (str): Port to connect to node, default 22 for ssh and 23 - port (str): Port to connect to node, default 22 for ssh and 23
for telnet. for telnet.
- protocol (str): Select ssh or telnet. Default is ssh. - protocol (str): Select ssh, telnet, kubectl or docker. Default is ssh.
- user (str): Username to of the node. - user (str): Username to of the node.
@ -326,6 +326,14 @@ class node:
connect = self._connect(timeout = timeout) connect = self._connect(timeout = timeout)
now = datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S') now = datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')
if connect == True: if connect == True:
# Attempt to set the terminal size
try:
self.child.setwinsize(65535, 65535)
except Exception:
try:
self.child.setwinsize(10000, 10000)
except Exception:
pass
if "prompt" in self.tags: if "prompt" in self.tags:
prompt = self.tags["prompt"] prompt = self.tags["prompt"]
expects = [prompt, pexpect.EOF, pexpect.TIMEOUT] expects = [prompt, pexpect.EOF, pexpect.TIMEOUT]
@ -413,6 +421,14 @@ class node:
''' '''
connect = self._connect(timeout = timeout) connect = self._connect(timeout = timeout)
if connect == True: if connect == True:
# Attempt to set the terminal size
try:
self.child.setwinsize(65535, 65535)
except Exception:
try:
self.child.setwinsize(10000, 10000)
except Exception:
pass
if "prompt" in self.tags: if "prompt" in self.tags:
prompt = self.tags["prompt"] prompt = self.tags["prompt"]
expects = [prompt, pexpect.EOF, pexpect.TIMEOUT] expects = [prompt, pexpect.EOF, pexpect.TIMEOUT]
@ -468,47 +484,101 @@ class node:
return connect return connect
@MethodHook @MethodHook
def _connect(self, debug = False, timeout = 10, max_attempts = 3): def _generate_ssh_sftp_cmd(self):
# Method to connect to the node, it parse all the information, create the ssh/telnet command and login to the node.
if self.protocol in ["ssh", "sftp"]:
cmd = self.protocol cmd = self.protocol
if self.idletime > 0: if self.idletime > 0:
cmd = cmd + " -o ServerAliveInterval=" + str(self.idletime) cmd += " -o ServerAliveInterval=" + str(self.idletime)
if self.port != '': if self.port:
if self.protocol == "ssh": if self.protocol == "ssh":
cmd = cmd + " -p " + self.port cmd += " -p " + self.port
elif self.protocol == "sftp": elif self.protocol == "sftp":
cmd = cmd + " -P " + self.port cmd += " -P " + self.port
if self.options != '': if self.options:
cmd = cmd + " " + self.options cmd += " " + self.options
if self.logs != '': if self.jumphost:
self.logfile = self._logfile() cmd += " " + self.jumphost
if self.jumphost != '': user_host = f"{self.user}@{self.host}" if self.user else self.host
cmd = cmd + " " + self.jumphost cmd += f" {user_host}"
if self.password[0] != '': return cmd
passwords = self._passtx(self.password)
else: @MethodHook
passwords = [] def _generate_telnet_cmd(self):
if self.user == '': cmd = f"telnet {self.host}"
cmd = cmd + " {}".format(self.host) if self.port:
else: cmd += f" {self.port}"
cmd = cmd + " {}".format("@".join([self.user,self.host])) if self.options:
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", "[b|B]ad (owner|permissions)"] cmd += f" {self.options}"
return cmd
@MethodHook
def _generate_kube_cmd(self):
cmd = f"kubectl exec {self.options} {self.host} -it --"
kube_command = self.tags.get("kube_command", "/bin/bash") if isinstance(self.tags, dict) else "/bin/bash"
cmd += f" {kube_command}"
return cmd
@MethodHook
def _generate_docker_cmd(self):
cmd = f"docker {self.options} exec -it {self.host}"
docker_command = self.tags.get("docker_command", "/bin/bash") if isinstance(self.tags, dict) else "/bin/bash"
cmd += f" {docker_command}"
return cmd
@MethodHook
def _get_cmd(self):
if self.protocol in ["ssh", "sftp"]:
return self._generate_ssh_sftp_cmd()
elif self.protocol == "telnet": elif self.protocol == "telnet":
cmd = "telnet " + self.host return self._generate_telnet_cmd()
if self.port != '': elif self.protocol == "kubectl":
cmd = cmd + " " + self.port return self._generate_kube_cmd()
if self.options != '': elif self.protocol == "docker":
cmd = cmd + " " + self.options return self._generate_docker_cmd()
else:
raise ValueError(f"Invalid protocol: {self.protocol}")
@MethodHook
def _connect(self, debug=False, timeout=10, max_attempts=3):
cmd = self._get_cmd()
passwords = self._passtx(self.password) if self.password[0] else []
if self.logs != '': if self.logs != '':
self.logfile = self._logfile() self.logfile = self._logfile()
if self.password[0] != '': default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$'
passwords = self._passtx(self.password) prompt = self.tags.get("prompt", default_prompt) if isinstance(self.tags, dict) else default_prompt
else: password_prompt = '[p|P]assword:|[u|U]sername:' if self.protocol != 'telnet' else '[p|P]assword:'
passwords = []
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", "[b|B]ad (owner|permissions)"] expects = {
else: "ssh": ['yes/no', 'refused', 'supported', 'Invalid|[u|U]sage: ssh', 'ssh-keygen.*\"', 'timeout|timed.out', 'unavailable', 'closed', password_prompt, prompt, 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching", "[b|B]ad (owner|permissions)"],
raise ValueError("Invalid protocol: " + self.protocol) "sftp": ['yes/no', 'refused', 'supported', 'Invalid|[u|U]sage: sftp', 'ssh-keygen.*\"', 'timeout|timed.out', 'unavailable', 'closed', password_prompt, prompt, 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching", "[b|B]ad (owner|permissions)"],
"telnet": ['[u|U]sername:', 'refused', 'supported', 'invalid|unrecognized option', 'ssh-keygen.*\"', 'timeout|timed.out', 'unavailable', 'closed', password_prompt, prompt, 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching", "[b|B]ad (owner|permissions)"],
"kubectl": ['[u|U]sername:', '[r|R]efused', '[E|e]rror', 'DEPRECATED', pexpect.TIMEOUT, password_prompt, prompt, pexpect.EOF, "expired|invalid"],
"docker": ['[u|U]sername:', 'Cannot', '[E|e]rror', 'failed', 'not a docker command', 'unknown', 'unable to resolve', pexpect.TIMEOUT, password_prompt, prompt, pexpect.EOF]
}
error_indices = {
"ssh": [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16],
"sftp": [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16],
"telnet": [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16],
"kubectl": [1, 2, 3, 4, 8], # Define error indices for kube
"docker": [1, 2, 3, 4, 5, 6, 7] # Define error indices for docker
}
eof_indices = {
"ssh": [8, 9, 10, 11],
"sftp": [8, 9, 10, 11],
"telnet": [8, 9, 10, 11],
"kubectl": [5, 6, 7], # Define eof indices for kube
"docker": [8, 9, 10] # Define eof indices for docker
}
initial_indices = {
"ssh": [0],
"sftp": [0],
"telnet": [0],
"kubectl": [0], # Define special indices for kube
"docker": [0] # Define special indices for docker
}
attempts = 1 attempts = 1
while attempts <= max_attempts: while attempts <= max_attempts:
child = pexpect.spawn(cmd) child = pexpect.spawn(cmd)
@ -516,54 +586,55 @@ class node:
print(cmd) print(cmd)
self.mylog = io.BytesIO() self.mylog = io.BytesIO()
child.logfile_read = self.mylog child.logfile_read = self.mylog
if len(passwords) > 0:
loops = len(passwords)
else:
loops = 1
endloop = False endloop = False
for i in range(0, loops): for i in range(len(passwords) if passwords else 1):
while True: while True:
results = child.expect(expects, timeout=timeout) results = child.expect(expects[self.protocol], timeout=timeout)
if results == 0: results_value = expects[self.protocol][results]
if results in initial_indices[self.protocol]:
if self.protocol in ["ssh", "sftp"]: if self.protocol in ["ssh", "sftp"]:
child.sendline('yes') child.sendline('yes')
elif self.protocol == "telnet": elif self.protocol in ["telnet", "kubectl"]:
if self.user != '': if self.user:
child.sendline(self.user) child.sendline(self.user)
else: else:
self.missingtext = True self.missingtext = True
break break
if results in [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16]:
elif results in error_indices[self.protocol]:
child.terminate() child.terminate()
if results == 12 and attempts != max_attempts: if results_value == pexpect.TIMEOUT and attempts != max_attempts:
attempts += 1 attempts += 1
endloop = True endloop = True
break break
else: else:
if results == 12: after = "Connection timeout" if results == 12 else child.after.decode()
after = "Connection timeout" return f"Connection failed code: {results}\n{child.before.decode().lstrip()}{after}{child.readline().decode()}".rstrip()
else:
after = child.after.decode() elif results in eof_indices[self.protocol]:
return ("Connection failed code:" + str(results) + "\n" + child.before.decode().lstrip() + after + child.readline().decode()).rstrip() if results_value == password_prompt:
if results == 8: if passwords:
if len(passwords) > 0:
child.sendline(passwords[i]) child.sendline(passwords[i])
else: else:
self.missingtext = True self.missingtext = True
break break
if results in [9, 11]: elif results_value == "suspend":
child.sendline("\r")
sleep(2)
else:
endloop = True endloop = True
child.sendline() child.sendline()
break break
if results == 10:
child.sendline("\r")
sleep(2)
if endloop: if endloop:
break break
if results == 12: if results_value == pexpect.TIMEOUT:
continue continue
else: else:
break break
child.readline(0) child.readline(0)
self.child = child self.child = child
return True return True

View File

@ -4,7 +4,7 @@ version = attr: connpy._version.__version__
description = Connpy is a SSH/Telnet connection manager and automation module description = Connpy is a SSH/Telnet connection manager and automation module
long_description = file: README.md long_description = file: README.md
long_description_content_type = text/markdown long_description_content_type = text/markdown
keywords = networking, automation, ssh, telnet, connection manager keywords = networking, automation, docker, kubernetes, ssh, telnet, connection manager
author = Federico Luzzi author = Federico Luzzi
author_email = fluzzi@gmail.com author_email = fluzzi@gmail.com
url = https://github.com/fluzzi/connpy url = https://github.com/fluzzi/connpy