Compare commits

..

No commits in common. "4d8244a10f4285e4874cd09772ea822e87c4a9c0" and "3365acb4735fca1fd0d3d7050fa0b13cdb307f6f" have entirely different histories.

11 changed files with 227 additions and 672 deletions

3
.gitignore vendored
View File

@ -130,6 +130,3 @@ dmypy.json
#clients #clients
*sync_client* *sync_client*
#App
connpy-completion-helper

View File

@ -9,8 +9,7 @@
[![](https://img.shields.io/pypi/l/connpy.svg?style=flat-square)](https://github.com/fluzzi/connpy/blob/main/LICENSE) [![](https://img.shields.io/pypi/l/connpy.svg?style=flat-square)](https://github.com/fluzzi/connpy/blob/main/LICENSE)
[![](https://img.shields.io/pypi/dm/connpy.svg?style=flat-square)](https://pypi.org/pypi/connpy/) [![](https://img.shields.io/pypi/dm/connpy.svg?style=flat-square)](https://pypi.org/pypi/connpy/)
Connpy is a SSH, SFTP, Telnet, kubectl, and Docker pod connection manager and automation module for Linux, Mac, and Docker. Connpy is a ssh and telnet connection manager and automation module for Linux, Mac and Docker
## Installation ## Installation
@ -44,34 +43,33 @@ Connpy integrates with Google services for backup purposes:
For more detailed information, please read our [Privacy Policy](https://connpy.gederico.dynu.net/fluzzi32/connpy/src/branch/main/PRIVATE_POLICY.md). For more detailed information, please read our [Privacy Policy](https://connpy.gederico.dynu.net/fluzzi32/connpy/src/branch/main/PRIVATE_POLICY.md).
### Features ### Features
- Manage connections using SSH, SFTP, Telnet, kubectl, and Docker exec. - You can generate profiles and reference them from nodes using @profilename so you dont
- Set contexts to manage specific nodes from specific contexts (work/home/clients/etc). need to edit multiple nodes when changing password or other information.
- You can generate profiles and reference them from nodes using @profilename so you don't - Nodes can be stored on @folder or @subfolder@folder to organize your devices. Then can
need to edit multiple nodes when changing passwords or other information. be referenced using node@subfolder@folder or node@folder
- Nodes can be stored on @folder or @subfolder@folder to organize your devices. They can - If you have too many nodes. Get completion script using: conn config --completion.
be referenced using node@subfolder@folder or node@folder. Or use fzf installing pyfzf and running conn config --fzf true
- If you have too many nodes, get a completion script using: conn config --completion. - Create in bulk, copy, move, export and import nodes for easy management.
Or use fzf by installing pyfzf and running conn config --fzf true. - Run automation scripts in network devices.
- Create in bulk, copy, move, export, and import nodes for easy management. - use GPT AI to help you manage your devices.
- Run automation scripts on network devices.
- Use GPT AI to help you manage your devices.
- Add plugins with your own scripts. - Add plugins with your own scripts.
- Much more! - Much more!
### Usage: ### Usage:
``` ```
usage: conn [-h] [--add | --del | --mod | --show | --debug] [node|folder] [--sftp] usage: conn [-h] [--add | --del | --mod | --show | --debug] [node|folder] [--sftp]
conn {profile,move,mv,copy,cp,list,ls,bulk,export,import,ai,run,api,plugin,config,sync,context} ... conn {profile,move,mv,copy,cp,list,ls,bulk,export,import,ai,run,api,plugin,config} ...
positional arguments: positional arguments:
node|folder node[@subfolder][@folder] node|folder node[@subfolder][@folder]
Connect to specific node or show all matching nodes Connect to specific node or show all matching nodes
[@subfolder][@folder] [@subfolder][@folder]
Show all available connections globally or in specified path Show all available connections globaly or in specified path
```
options: ### Options:
```
-h, --help show this help message and exit -h, --help show this help message and exit
-v, --version Show version -v, --version Show version
-a, --add Add new node[@subfolder][@folder] or [@subfolder]@folder -a, --add Add new node[@subfolder][@folder] or [@subfolder]@folder
@ -80,8 +78,10 @@ options:
-s, --show Show node[@subfolder][@folder] -s, --show Show node[@subfolder][@folder]
-d, --debug Display all conections steps -d, --debug Display all conections steps
-t, --sftp Connects using sftp instead of ssh -t, --sftp Connects using sftp instead of ssh
```
Commands: ### Commands:
```
profile Manage profiles profile Manage profiles
move(mv) Move node move(mv) Move node
copy(cp) Copy node copy(cp) Copy node
@ -95,7 +95,6 @@ Commands:
plugin Manage plugins plugin Manage plugins
config Manage app config config Manage app config
sync Sync config with Google sync Sync config with Google
context Manage contexts with regex matching
``` ```
### Manage profiles: ### Manage profiles:
@ -116,26 +115,14 @@ options:
### Examples: ### Examples:
``` ```
#Add new profile
conn profile --add office-user conn profile --add office-user
#Add new folder
conn --add @office conn --add @office
#Add new subfolder
conn --add @datacenter@office conn --add @datacenter@office
#Add node to subfolder
conn --add server@datacenter@office conn --add server@datacenter@office
#Add node to folder
conn --add pc@office conn --add pc@office
#Show node information
conn --show server@datacenter@office conn --show server@datacenter@office
#Connect to nodes
conn pc@office conn pc@office
conn server conn server
#Create and set new context
conn context -a office .*@office
conn context --set office
#Run a command in a node
conn run server ls -la
``` ```
## Plugin Requirements for Connpy ## Plugin Requirements for Connpy

View File

@ -2,35 +2,32 @@
''' '''
## Connection manager ## Connection manager
Connpy is a SSH, SFTP, Telnet, kubectl, and Docker pod connection manager and automation module for Linux, Mac, and Docker. Connpy is a connection manager that allows you to store nodes to connect them fast and password free.
### Features ### Features
- Manage connections using SSH, SFTP, Telnet, kubectl, and Docker exec. - You can generate profiles and reference them from nodes using @profilename so you dont
- Set contexts to manage specific nodes from specific contexts (work/home/clients/etc). need to edit multiple nodes when changing password or other information.
- You can generate profiles and reference them from nodes using @profilename so you don't - Nodes can be stored on @folder or @subfolder@folder to organize your devices. Then can
need to edit multiple nodes when changing passwords or other information. be referenced using node@subfolder@folder or node@folder
- Nodes can be stored on @folder or @subfolder@folder to organize your devices. They can - If you have too many nodes. Get completion script using: conn config --completion.
be referenced using node@subfolder@folder or node@folder. Or use fzf installing pyfzf and running conn config --fzf true
- If you have too many nodes, get a completion script using: conn config --completion. - Create in bulk, copy, move, export and import nodes for easy management.
Or use fzf by installing pyfzf and running conn config --fzf true. - Run automation scripts in network devices.
- Create in bulk, copy, move, export, and import nodes for easy management. - use GPT AI to help you manage your devices.
- Run automation scripts on network devices.
- Use GPT AI to help you manage your devices.
- Add plugins with your own scripts. - Add plugins with your own scripts.
- Much more! - Much more!
### Usage ### Usage
``` ```
usage: conn [-h] [--add | --del | --mod | --show | --debug] [node|folder] [--sftp] usage: conn [-h] [--add | --del | --mod | --show | --debug] [node|folder] [--sftp]
conn {profile,move,mv,copy,cp,list,ls,bulk,export,import,ai,run,api,plugin,config,sync,context} ... conn {profile,move,mv,copy,cp,list,ls,bulk,export,import,ai,run,api,plugin,config} ...
positional arguments: positional arguments:
node|folder node[@subfolder][@folder] node|folder node[@subfolder][@folder]
Connect to specific node or show all matching nodes Connect to specific node or show all matching nodes
[@subfolder][@folder] [@subfolder][@folder]
Show all available connections globally or in specified path Show all available connections globaly or in specified path
Options:
options:
-h, --help show this help message and exit -h, --help show this help message and exit
-v, --version Show version -v, --version Show version
-a, --add Add new node[@subfolder][@folder] or [@subfolder]@folder -a, --add Add new node[@subfolder][@folder] or [@subfolder]@folder
@ -54,7 +51,6 @@ Commands:
plugin Manage plugins plugin Manage plugins
config Manage app config config Manage app config
sync Sync config with Google sync Sync config with Google
context Manage contexts with regex matching
``` ```
### Manage profiles ### Manage profiles
@ -75,26 +71,14 @@ options:
### Examples ### Examples
``` ```
#Add new profile
conn profile --add office-user conn profile --add office-user
#Add new folder
conn --add @office conn --add @office
#Add new subfolder
conn --add @datacenter@office conn --add @datacenter@office
#Add node to subfolder
conn --add server@datacenter@office conn --add server@datacenter@office
#Add node to folder
conn --add pc@office conn --add pc@office
#Show node information
conn --show server@datacenter@office conn --show server@datacenter@office
#Connect to nodes
conn pc@office conn pc@office
conn server conn server
#Create and set new context
conn context -a office .*@office
conn context --set office
#Run a command in a node
conn run server ls -la
``` ```
## Plugin Requirements for Connpy ## Plugin Requirements for Connpy
### General Structure ### General Structure

View File

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

View File

@ -1,5 +1,4 @@
from flask import Flask, request, jsonify from flask import Flask, request, jsonify
from flask_cors import CORS
from connpy import configfile, node, nodes, hooks from connpy import configfile, node, nodes, hooks
from connpy.ai import ai as myai from connpy.ai import ai as myai
from waitress import serve from waitress import serve
@ -7,7 +6,6 @@ import os
import signal import signal
app = Flask(__name__) app = Flask(__name__)
CORS(app)
conf = configfile() conf = configfile()
PID_FILE1 = "/run/connpy.pid" PID_FILE1 = "/run/connpy.pid"

View File

@ -8,7 +8,7 @@ import sys
import inquirer import inquirer
from .core import node,nodes from .core import node,nodes
from ._version import __version__ from ._version import __version__
from .api import start_api,stop_api,debug_api,app from .api import start_api,stop_api,debug_api
from .ai import ai from .ai import ai
from .plugins import Plugins from .plugins import Plugins
import yaml import yaml
@ -42,7 +42,6 @@ class connapp:
the config file. the config file.
''' '''
self.app = app
self.node = node self.node = node
self.nodes = nodes self.nodes = nodes
self.start_api = start_api self.start_api = start_api
@ -313,7 +312,11 @@ class connapp:
if uniques == False: if uniques == False:
print("Invalid node {}".format(args.data)) print("Invalid node {}".format(args.data))
exit(5) exit(5)
self._print_instructions() 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}")
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)
@ -1078,16 +1081,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$|^kubectl$|^docker$|^$)"): def _profile_protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^$)"):
#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, kubectl, docker or leave empty") raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet or leave empty")
return True return True
def _protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^$|^@.+$)"): def _protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^$|^@.+$)"):
#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, kubectl, docker leave empty or @profile") raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, 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))
@ -1108,7 +1111,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-6553/app5, @profile or leave empty") raise inquirer.errors.ValidationError("", reason="Pick a port between 1-65535, @profile or leave empty")
try: try:
port = int(current) port = int(current)
except: except:
@ -1214,7 +1217,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/app?")) questions.append(inquirer.Confirm("protocol", message="Edit Protocol?"))
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?"))
@ -1244,7 +1247,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/app", validate=self._protocol_validation, default=defaults["protocol"])) questions.append(inquirer.Text("protocol", message="Select Protocol", validate=self._protocol_validation, default=defaults["protocol"]))
else: else:
node["protocol"] = defaults["protocol"] node["protocol"] = defaults["protocol"]
if edit["port"]: if edit["port"]:
@ -1252,7 +1255,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/app", validate=self._default_validation, default=defaults["options"])) questions.append(inquirer.Text("options", message="Pass extra options to protocol", validate=self._default_validation, default=defaults["options"]))
else: else:
node["options"] = defaults["options"] node["options"] = defaults["options"]
if edit["logs"]: if edit["logs"]:
@ -1318,7 +1321,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/app", validate=self._profile_protocol_validation, default=defaults["protocol"])) questions.append(inquirer.Text("protocol", message="Select Protocol", validate=self._profile_protocol_validation, default=defaults["protocol"]))
else: else:
profile["protocol"] = defaults["protocol"] profile["protocol"] = defaults["protocol"]
if edit["port"]: if edit["port"]:
@ -1326,7 +1329,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/app", default=defaults["options"])) questions.append(inquirer.Text("options", message="Pass extra options to protocol", default=defaults["options"]))
else: else:
profile["options"] = defaults["options"] profile["options"] = defaults["options"]
if edit["logs"]: if edit["logs"]:
@ -1367,9 +1370,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/app", validate=self._protocol_validation)) questions.append(inquirer.Text("protocol", message="Select Protocol", 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/app", 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("jumphost", message="Add Jumphost node", validate=self._jumphost_validation))
@ -1549,45 +1552,3 @@ 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, telnet, kubectl or docker. Default is ssh. - protocol (str): Select ssh or telnet. Default is ssh.
- user (str): Username to of the node. - user (str): Username to of the node.
@ -326,14 +326,6 @@ 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]
@ -421,14 +413,6 @@ 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]
@ -484,101 +468,47 @@ class node:
return connect return connect
@MethodHook @MethodHook
def _generate_ssh_sftp_cmd(self): def _connect(self, debug = False, timeout = 10, max_attempts = 3):
# 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 += " -o ServerAliveInterval=" + str(self.idletime) cmd = cmd + " -o ServerAliveInterval=" + str(self.idletime)
if self.port: if self.port != '':
if self.protocol == "ssh": if self.protocol == "ssh":
cmd += " -p " + self.port cmd = cmd + " -p " + self.port
elif self.protocol == "sftp": elif self.protocol == "sftp":
cmd += " -P " + self.port cmd = cmd + " -P " + self.port
if self.options: if self.options != '':
cmd += " " + self.options cmd = cmd + " " + self.options
if self.jumphost:
cmd += " " + self.jumphost
user_host = f"{self.user}@{self.host}" if self.user else self.host
cmd += f" {user_host}"
return cmd
@MethodHook
def _generate_telnet_cmd(self):
cmd = f"telnet {self.host}"
if self.port:
cmd += f" {self.port}"
if self.options:
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":
return self._generate_telnet_cmd()
elif self.protocol == "kubectl":
return self._generate_kube_cmd()
elif self.protocol == "docker":
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()
default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$' if self.jumphost != '':
prompt = self.tags.get("prompt", default_prompt) if isinstance(self.tags, dict) else default_prompt cmd = cmd + " " + self.jumphost
password_prompt = '[p|P]assword:|[u|U]sername:' if self.protocol != 'telnet' else '[p|P]assword:' if self.password[0] != '':
passwords = self._passtx(self.password)
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)"], passwords = []
"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)"], if self.user == '':
"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)"], cmd = cmd + " {}".format(self.host)
"kubectl": ['[u|U]sername:', '[r|R]efused', '[E|e]rror', 'DEPRECATED', pexpect.TIMEOUT, password_prompt, prompt, pexpect.EOF, "expired|invalid"], else:
"docker": ['[u|U]sername:', 'Cannot', '[E|e]rror', 'failed', 'not a docker command', 'unknown', 'unable to resolve', pexpect.TIMEOUT, password_prompt, prompt, pexpect.EOF] cmd = cmd + " {}".format("@".join([self.user,self.host]))
} 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)"]
elif self.protocol == "telnet":
error_indices = { cmd = "telnet " + self.host
"ssh": [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16], if self.port != '':
"sftp": [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16], cmd = cmd + " " + self.port
"telnet": [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16], if self.options != '':
"kubectl": [1, 2, 3, 4, 8], # Define error indices for kube cmd = cmd + " " + self.options
"docker": [1, 2, 3, 4, 5, 6, 7] # Define error indices for docker if self.logs != '':
} self.logfile = self._logfile()
if self.password[0] != '':
eof_indices = { passwords = self._passtx(self.password)
"ssh": [8, 9, 10, 11], else:
"sftp": [8, 9, 10, 11], passwords = []
"telnet": [8, 9, 10, 11], 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)"]
"kubectl": [5, 6, 7], # Define eof indices for kube else:
"docker": [8, 9, 10] # Define eof indices for docker raise ValueError("Invalid protocol: " + self.protocol)
}
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)
@ -586,55 +516,54 @@ 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(len(passwords) if passwords else 1): for i in range(0, loops):
while True: while True:
results = child.expect(expects[self.protocol], timeout=timeout) results = child.expect(expects, timeout=timeout)
results_value = expects[self.protocol][results] if results == 0:
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 in ["telnet", "kubectl"]: elif self.protocol == "telnet":
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_value == pexpect.TIMEOUT and attempts != max_attempts: if results == 12 and attempts != max_attempts:
attempts += 1 attempts += 1
endloop = True endloop = True
break break
else: else:
after = "Connection timeout" if results_value == pexpect.TIMEOUT else child.after.decode() if results == 12:
return f"Connection failed code: {results}\n{child.before.decode().lstrip()}{after}{child.readline().decode()}".rstrip() after = "Connection timeout"
else:
elif results in eof_indices[self.protocol]: after = child.after.decode()
if results_value == password_prompt: return ("Connection failed code:" + str(results) + "\n" + child.before.decode().lstrip() + after + child.readline().decode()).rstrip()
if passwords: if results == 8:
if len(passwords) > 0:
child.sendline(passwords[i]) child.sendline(passwords[i])
else: else:
self.missingtext = True self.missingtext = True
break break
elif results_value == "suspend": if results in [9, 11]:
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_value == pexpect.TIMEOUT: if results == 12:
continue continue
else: else:
break break
child.readline(0) child.readline(0)
self.child = child self.child = child
return True return True

View File

@ -1,180 +0,0 @@
import argparse
import yaml
import re
class context_manager:
def __init__(self, connapp):
self.connapp = connapp
self.config = connapp.config
self.contexts = self.config.config["contexts"]
self.current_context = self.config.config["current_context"]
self.regex = [re.compile(regex) for regex in self.contexts[self.current_context]]
def add_context(self, context, regex):
if not context.isalnum():
print("Context name has to be alphanumeric.")
exit(1)
elif context in self.contexts:
print(f"Context {context} already exists.")
exit(2)
else:
self.contexts[context] = regex
self.connapp._change_settings("contexts", self.contexts)
def modify_context(self, context, regex):
if context == "all":
print("Can't modify default context: all")
exit(3)
elif context not in self.contexts:
print(f"Context {context} doesn't exist.")
exit(4)
else:
self.contexts[context] = regex
self.connapp._change_settings("contexts", self.contexts)
def delete_context(self, context):
if context == "all":
print("Can't delete default context: all")
exit(3)
elif context not in self.contexts:
print(f"Context {context} doesn't exist.")
exit(4)
if context == self.current_context:
print(f"Can't delete current context: {self.current_context}")
exit(5)
else:
self.contexts.pop(context)
self.connapp._change_settings("contexts", self.contexts)
def list_contexts(self):
for key in self.contexts.keys():
if key == self.current_context:
print(f"{key} * (active)")
else:
print(key)
def set_context(self, context):
if context not in self.contexts:
print(f"Context {context} doesn't exist.")
exit(4)
elif context == self.current_context:
print(f"Context {context} already set")
exit(0)
else:
self.connapp._change_settings("current_context", context)
def show_context(self, context):
if context not in self.contexts:
print(f"Context {context} doesn't exist.")
exit(4)
else:
yaml_output = yaml.dump(self.contexts[context], sort_keys=False, default_flow_style=False)
print(yaml_output)
@staticmethod
def add_default_context(config):
config_modified = False
if "contexts" not in config.config:
config.config["contexts"] = {}
config.config["contexts"]["all"] = [".*"]
config_modified = True
if "current_context" not in config.config:
config.config["current_context"] = "all"
config_modified = True
if config_modified:
config._saveconfig(config.file)
def match_any_regex(self, node, regex_list):
return any(regex.match(node) for regex in regex_list)
def modify_node_list(self, *args, **kwargs):
filtered_nodes = [node for node in kwargs["result"] if self.match_any_regex(node, self.regex)]
return filtered_nodes
def modify_node_dict(self, *args, **kwargs):
filtered_nodes = {key: value for key, value in kwargs["result"].items() if self.match_any_regex(key, self.regex)}
return filtered_nodes
class Preload:
def __init__(self, connapp):
#define contexts if doesn't exist
connapp.config.modify(context_manager.add_default_context)
#filter nodes using context
cm = context_manager(connapp)
connapp.nodes_list = [node for node in connapp.nodes_list if cm.match_any_regex(node, cm.regex)]
connapp.folders = [node for node in connapp.folders if cm.match_any_regex(node, cm.regex)]
connapp.config._getallnodes.register_post_hook(cm.modify_node_list)
connapp.config._getallfolders.register_post_hook(cm.modify_node_list)
connapp.config._getallnodesfull.register_post_hook(cm.modify_node_dict)
class Parser:
def __init__(self):
self.parser = argparse.ArgumentParser(description="Manage contexts with regex matching", formatter_class=argparse.RawTextHelpFormatter)
self.description = "Manage contexts with regex matching"
# Define the context name as a positional argument
self.parser.add_argument("context_name", help="Name of the context", nargs='?')
group = self.parser.add_mutually_exclusive_group(required=True)
group.add_argument("-a", "--add", nargs='+', help='Add a new context with regex values. Usage: context -a name "regex1" "regex2"')
group.add_argument("-r", "--rm", "--del", action='store_true', help="Delete a context. Usage: context -d name")
group.add_argument("--ls", action='store_true', help="List all contexts. Usage: context --list")
group.add_argument("--set", action='store_true', help="Set the used context. Usage: context --set name")
group.add_argument("-s", "--show", action='store_true', help="Show the defined regex of a context. Usage: context --show name")
group.add_argument("-e", "--edit", "--mod", nargs='+', help='Modify an existing context. Usage: context --mod name "regex1" "regex2"')
class Entrypoint:
def __init__(self, args, parser, connapp):
if args.add and len(args.add) < 2:
parser.error("--add requires at least 2 arguments: name and at least one regex")
if args.edit and len(args.edit) < 2:
parser.error("--edit requires at least 2 arguments: name and at least one regex")
if args.ls and args.context_name is not None:
parser.error("--ls does not require a context name")
if args.rm and not args.context_name:
parser.error("--rm require a context name")
if args.set and not args.context_name:
parser.error("--set require a context name")
if args.show and not args.context_name:
parser.error("--show require a context name")
cm = context_manager(connapp)
if args.add:
cm.add_context(args.add[0], args.add[1:])
elif args.rm:
cm.delete_context(args.context_name)
elif args.ls:
cm.list_contexts()
elif args.edit:
cm.modify_context(args.edit[0], args.edit[1:])
elif args.set:
cm.set_context(args.context_name)
elif args.show:
cm.show_context(args.context_name)
def _connpy_completion(wordsnumber, words, info=None):
if wordsnumber == 3:
result = ["--help", "--add", "--del", "--rm", "--ls", "--set", "--show", "--edit", "--mod"]
elif wordsnumber == 4 and words[1] in ["--del", "-r", "--rm", "--set", "--edit", "--mod", "-e", "--show", "-s"]:
contexts = info["config"]["config"]["contexts"].keys()
current_context = info["config"]["config"]["current_context"]
default_context = "all"
if words[1] in ["--del", "-r", "--rm"]:
# Filter out default context and current context
result = [context for context in contexts if context not in [default_context, current_context]]
elif words[1] == "--set":
# Filter out current context
result = [context for context in contexts if context != current_context]
elif words[1] in ["--edit", "--mod", "-e"]:
# Filter out default context
result = [context for context in contexts if context != default_context]
elif words[1] in ["--show", "-s"]:
# No filter for show
result = list(contexts)
return result

View File

@ -23,33 +23,30 @@
</header> </header>
<section id="section-intro"> <section id="section-intro">
<h2 id="connection-manager">Connection manager</h2> <h2 id="connection-manager">Connection manager</h2>
<p>Connpy is a SSH, SFTP, Telnet, kubectl, and Docker pod connection manager and automation module for Linux, Mac, and Docker.</p> <p>Connpy is a connection manager that allows you to store nodes to connect them fast and password free.</p>
<h3 id="features">Features</h3> <h3 id="features">Features</h3>
<pre><code>- Manage connections using SSH, SFTP, Telnet, kubectl, and Docker exec. <pre><code>- You can generate profiles and reference them from nodes using @profilename so you dont
- Set contexts to manage specific nodes from specific contexts (work/home/clients/etc). need to edit multiple nodes when changing password or other information.
- You can generate profiles and reference them from nodes using @profilename so you don't - Nodes can be stored on @folder or @subfolder@folder to organize your devices. Then can
need to edit multiple nodes when changing passwords or other information. be referenced using node@subfolder@folder or node@folder
- Nodes can be stored on @folder or @subfolder@folder to organize your devices. They can - If you have too many nodes. Get completion script using: conn config --completion.
be referenced using node@subfolder@folder or node@folder. Or use fzf installing pyfzf and running conn config --fzf true
- If you have too many nodes, get a completion script using: conn config --completion. - Create in bulk, copy, move, export and import nodes for easy management.
Or use fzf by installing pyfzf and running conn config --fzf true. - Run automation scripts in network devices.
- Create in bulk, copy, move, export, and import nodes for easy management. - use GPT AI to help you manage your devices.
- Run automation scripts on network devices.
- Use GPT AI to help you manage your devices.
- Add plugins with your own scripts. - Add plugins with your own scripts.
- Much more! - Much more!
</code></pre> </code></pre>
<h3 id="usage">Usage</h3> <h3 id="usage">Usage</h3>
<pre><code>usage: conn [-h] [--add | --del | --mod | --show | --debug] [node|folder] [--sftp] <pre><code>usage: conn [-h] [--add | --del | --mod | --show | --debug] [node|folder] [--sftp]
conn {profile,move,mv,copy,cp,list,ls,bulk,export,import,ai,run,api,plugin,config,sync,context} ... conn {profile,move,mv,copy,cp,list,ls,bulk,export,import,ai,run,api,plugin,config} ...
positional arguments: positional arguments:
node|folder node[@subfolder][@folder] node|folder node[@subfolder][@folder]
Connect to specific node or show all matching nodes Connect to specific node or show all matching nodes
[@subfolder][@folder] [@subfolder][@folder]
Show all available connections globally or in specified path Show all available connections globaly or in specified path
Options:
options:
-h, --help show this help message and exit -h, --help show this help message and exit
-v, --version Show version -v, --version Show version
-a, --add Add new node[@subfolder][@folder] or [@subfolder]@folder -a, --add Add new node[@subfolder][@folder] or [@subfolder]@folder
@ -73,7 +70,6 @@ Commands:
plugin Manage plugins plugin Manage plugins
config Manage app config config Manage app config
sync Sync config with Google sync Sync config with Google
context Manage contexts with regex matching
</code></pre> </code></pre>
<h3 id="manage-profiles">Manage profiles</h3> <h3 id="manage-profiles">Manage profiles</h3>
<pre><code>usage: conn profile [-h] (--add | --del | --mod | --show) profile <pre><code>usage: conn profile [-h] (--add | --del | --mod | --show) profile
@ -90,26 +86,14 @@ options:
</code></pre> </code></pre>
<h3 id="examples">Examples</h3> <h3 id="examples">Examples</h3>
<pre><code> #Add new profile <pre><code> conn profile --add office-user
conn profile --add office-user
#Add new folder
conn --add @office conn --add @office
#Add new subfolder
conn --add @datacenter@office conn --add @datacenter@office
#Add node to subfolder
conn --add server@datacenter@office conn --add server@datacenter@office
#Add node to folder
conn --add pc@office conn --add pc@office
#Show node information
conn --show server@datacenter@office conn --show server@datacenter@office
#Connect to nodes
conn pc@office conn pc@office
conn server conn server
#Create and set new context
conn context -a office .*@office
conn context --set office
#Run a command in a node
conn run server ls -la
</code></pre> </code></pre>
<h2 id="plugin-requirements-for-connpy">Plugin Requirements for Connpy</h2> <h2 id="plugin-requirements-for-connpy">Plugin Requirements for Connpy</h2>
<h3 id="general-structure">General Structure</h3> <h3 id="general-structure">General Structure</h3>
@ -432,35 +416,32 @@ print(result)
&#39;&#39;&#39; &#39;&#39;&#39;
## Connection manager ## Connection manager
Connpy is a SSH, SFTP, Telnet, kubectl, and Docker pod connection manager and automation module for Linux, Mac, and Docker. Connpy is a connection manager that allows you to store nodes to connect them fast and password free.
### Features ### Features
- Manage connections using SSH, SFTP, Telnet, kubectl, and Docker exec. - You can generate profiles and reference them from nodes using @profilename so you dont
- Set contexts to manage specific nodes from specific contexts (work/home/clients/etc). need to edit multiple nodes when changing password or other information.
- You can generate profiles and reference them from nodes using @profilename so you don&#39;t - Nodes can be stored on @folder or @subfolder@folder to organize your devices. Then can
need to edit multiple nodes when changing passwords or other information. be referenced using node@subfolder@folder or node@folder
- Nodes can be stored on @folder or @subfolder@folder to organize your devices. They can - If you have too many nodes. Get completion script using: conn config --completion.
be referenced using node@subfolder@folder or node@folder. Or use fzf installing pyfzf and running conn config --fzf true
- If you have too many nodes, get a completion script using: conn config --completion. - Create in bulk, copy, move, export and import nodes for easy management.
Or use fzf by installing pyfzf and running conn config --fzf true. - Run automation scripts in network devices.
- Create in bulk, copy, move, export, and import nodes for easy management. - use GPT AI to help you manage your devices.
- Run automation scripts on network devices.
- Use GPT AI to help you manage your devices.
- Add plugins with your own scripts. - Add plugins with your own scripts.
- Much more! - Much more!
### Usage ### Usage
``` ```
usage: conn [-h] [--add | --del | --mod | --show | --debug] [node|folder] [--sftp] usage: conn [-h] [--add | --del | --mod | --show | --debug] [node|folder] [--sftp]
conn {profile,move,mv,copy,cp,list,ls,bulk,export,import,ai,run,api,plugin,config,sync,context} ... conn {profile,move,mv,copy,cp,list,ls,bulk,export,import,ai,run,api,plugin,config} ...
positional arguments: positional arguments:
node|folder node[@subfolder][@folder] node|folder node[@subfolder][@folder]
Connect to specific node or show all matching nodes Connect to specific node or show all matching nodes
[@subfolder][@folder] [@subfolder][@folder]
Show all available connections globally or in specified path Show all available connections globaly or in specified path
Options:
options:
-h, --help show this help message and exit -h, --help show this help message and exit
-v, --version Show version -v, --version Show version
-a, --add Add new node[@subfolder][@folder] or [@subfolder]@folder -a, --add Add new node[@subfolder][@folder] or [@subfolder]@folder
@ -484,7 +465,6 @@ Commands:
plugin Manage plugins plugin Manage plugins
config Manage app config config Manage app config
sync Sync config with Google sync Sync config with Google
context Manage contexts with regex matching
``` ```
### Manage profiles ### Manage profiles
@ -505,26 +485,14 @@ options:
### Examples ### Examples
``` ```
#Add new profile
conn profile --add office-user conn profile --add office-user
#Add new folder
conn --add @office conn --add @office
#Add new subfolder
conn --add @datacenter@office conn --add @datacenter@office
#Add node to subfolder
conn --add server@datacenter@office conn --add server@datacenter@office
#Add node to folder
conn --add pc@office conn --add pc@office
#Show node information
conn --show server@datacenter@office conn --show server@datacenter@office
#Connect to nodes
conn pc@office conn pc@office
conn server conn server
#Create and set new context
conn context -a office .*@office
conn context --set office
#Run a command in a node
conn run server ls -la
``` ```
## Plugin Requirements for Connpy ## Plugin Requirements for Connpy
### General Structure ### General Structure
@ -2529,7 +2497,7 @@ def getitems(self, uniques):
- 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, telnet, kubectl or docker. Default is ssh. - protocol (str): Select ssh or telnet. Default is ssh.
- user (str): Username to of the node. - user (str): Username to of the node.
@ -2587,7 +2555,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, telnet, kubectl or docker. Default is ssh. - protocol (str): Select ssh or telnet. Default is ssh.
- user (str): Username to of the node. - user (str): Username to of the node.
@ -2856,14 +2824,6 @@ class node:
connect = self._connect(timeout = timeout) connect = self._connect(timeout = timeout)
now = datetime.datetime.now().strftime(&#39;%Y-%m-%d_%H%M%S&#39;) now = datetime.datetime.now().strftime(&#39;%Y-%m-%d_%H%M%S&#39;)
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 &#34;prompt&#34; in self.tags: if &#34;prompt&#34; in self.tags:
prompt = self.tags[&#34;prompt&#34;] prompt = self.tags[&#34;prompt&#34;]
expects = [prompt, pexpect.EOF, pexpect.TIMEOUT] expects = [prompt, pexpect.EOF, pexpect.TIMEOUT]
@ -2951,14 +2911,6 @@ class node:
&#39;&#39;&#39; &#39;&#39;&#39;
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 &#34;prompt&#34; in self.tags: if &#34;prompt&#34; in self.tags:
prompt = self.tags[&#34;prompt&#34;] prompt = self.tags[&#34;prompt&#34;]
expects = [prompt, pexpect.EOF, pexpect.TIMEOUT] expects = [prompt, pexpect.EOF, pexpect.TIMEOUT]
@ -3014,101 +2966,47 @@ class node:
return connect return connect
@MethodHook @MethodHook
def _generate_ssh_sftp_cmd(self): def _connect(self, debug = False, timeout = 10, max_attempts = 3):
# 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 [&#34;ssh&#34;, &#34;sftp&#34;]:
cmd = self.protocol cmd = self.protocol
if self.idletime &gt; 0: if self.idletime &gt; 0:
cmd += &#34; -o ServerAliveInterval=&#34; + str(self.idletime) cmd = cmd + &#34; -o ServerAliveInterval=&#34; + str(self.idletime)
if self.port: if self.port != &#39;&#39;:
if self.protocol == &#34;ssh&#34;: if self.protocol == &#34;ssh&#34;:
cmd += &#34; -p &#34; + self.port cmd = cmd + &#34; -p &#34; + self.port
elif self.protocol == &#34;sftp&#34;: elif self.protocol == &#34;sftp&#34;:
cmd += &#34; -P &#34; + self.port cmd = cmd + &#34; -P &#34; + self.port
if self.options: if self.options != &#39;&#39;:
cmd += &#34; &#34; + self.options cmd = cmd + &#34; &#34; + self.options
if self.jumphost:
cmd += &#34; &#34; + self.jumphost
user_host = f&#34;{self.user}@{self.host}&#34; if self.user else self.host
cmd += f&#34; {user_host}&#34;
return cmd
@MethodHook
def _generate_telnet_cmd(self):
cmd = f&#34;telnet {self.host}&#34;
if self.port:
cmd += f&#34; {self.port}&#34;
if self.options:
cmd += f&#34; {self.options}&#34;
return cmd
@MethodHook
def _generate_kube_cmd(self):
cmd = f&#34;kubectl exec {self.options} {self.host} -it --&#34;
kube_command = self.tags.get(&#34;kube_command&#34;, &#34;/bin/bash&#34;) if isinstance(self.tags, dict) else &#34;/bin/bash&#34;
cmd += f&#34; {kube_command}&#34;
return cmd
@MethodHook
def _generate_docker_cmd(self):
cmd = f&#34;docker {self.options} exec -it {self.host}&#34;
docker_command = self.tags.get(&#34;docker_command&#34;, &#34;/bin/bash&#34;) if isinstance(self.tags, dict) else &#34;/bin/bash&#34;
cmd += f&#34; {docker_command}&#34;
return cmd
@MethodHook
def _get_cmd(self):
if self.protocol in [&#34;ssh&#34;, &#34;sftp&#34;]:
return self._generate_ssh_sftp_cmd()
elif self.protocol == &#34;telnet&#34;:
return self._generate_telnet_cmd()
elif self.protocol == &#34;kubectl&#34;:
return self._generate_kube_cmd()
elif self.protocol == &#34;docker&#34;:
return self._generate_docker_cmd()
else:
raise ValueError(f&#34;Invalid protocol: {self.protocol}&#34;)
@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 != &#39;&#39;: if self.logs != &#39;&#39;:
self.logfile = self._logfile() self.logfile = self._logfile()
default_prompt = r&#39;&gt;$|#$|\$$|&gt;.$|#.$|\$.$&#39; if self.jumphost != &#39;&#39;:
prompt = self.tags.get(&#34;prompt&#34;, default_prompt) if isinstance(self.tags, dict) else default_prompt cmd = cmd + &#34; &#34; + self.jumphost
password_prompt = &#39;[p|P]assword:|[u|U]sername:&#39; if self.protocol != &#39;telnet&#39; else &#39;[p|P]assword:&#39; if self.password[0] != &#39;&#39;:
passwords = self._passtx(self.password)
expects = { else:
&#34;ssh&#34;: [&#39;yes/no&#39;, &#39;refused&#39;, &#39;supported&#39;, &#39;Invalid|[u|U]sage: ssh&#39;, &#39;ssh-keygen.*\&#34;&#39;, &#39;timeout|timed.out&#39;, &#39;unavailable&#39;, &#39;closed&#39;, password_prompt, prompt, &#39;suspend&#39;, pexpect.EOF, pexpect.TIMEOUT, &#34;No route to host&#34;, &#34;resolve hostname&#34;, &#34;no matching&#34;, &#34;[b|B]ad (owner|permissions)&#34;], passwords = []
&#34;sftp&#34;: [&#39;yes/no&#39;, &#39;refused&#39;, &#39;supported&#39;, &#39;Invalid|[u|U]sage: sftp&#39;, &#39;ssh-keygen.*\&#34;&#39;, &#39;timeout|timed.out&#39;, &#39;unavailable&#39;, &#39;closed&#39;, password_prompt, prompt, &#39;suspend&#39;, pexpect.EOF, pexpect.TIMEOUT, &#34;No route to host&#34;, &#34;resolve hostname&#34;, &#34;no matching&#34;, &#34;[b|B]ad (owner|permissions)&#34;], if self.user == &#39;&#39;:
&#34;telnet&#34;: [&#39;[u|U]sername:&#39;, &#39;refused&#39;, &#39;supported&#39;, &#39;invalid|unrecognized option&#39;, &#39;ssh-keygen.*\&#34;&#39;, &#39;timeout|timed.out&#39;, &#39;unavailable&#39;, &#39;closed&#39;, password_prompt, prompt, &#39;suspend&#39;, pexpect.EOF, pexpect.TIMEOUT, &#34;No route to host&#34;, &#34;resolve hostname&#34;, &#34;no matching&#34;, &#34;[b|B]ad (owner|permissions)&#34;], cmd = cmd + &#34; {}&#34;.format(self.host)
&#34;kubectl&#34;: [&#39;[u|U]sername:&#39;, &#39;[r|R]efused&#39;, &#39;[E|e]rror&#39;, &#39;DEPRECATED&#39;, pexpect.TIMEOUT, password_prompt, prompt, pexpect.EOF, &#34;expired|invalid&#34;], else:
&#34;docker&#34;: [&#39;[u|U]sername:&#39;, &#39;Cannot&#39;, &#39;[E|e]rror&#39;, &#39;failed&#39;, &#39;not a docker command&#39;, &#39;unknown&#39;, &#39;unable to resolve&#39;, pexpect.TIMEOUT, password_prompt, prompt, pexpect.EOF] 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: (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;[b|B]ad (owner|permissions)&#34;]
elif self.protocol == &#34;telnet&#34;:
error_indices = { cmd = &#34;telnet &#34; + self.host
&#34;ssh&#34;: [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16], if self.port != &#39;&#39;:
&#34;sftp&#34;: [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16], cmd = cmd + &#34; &#34; + self.port
&#34;telnet&#34;: [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16], if self.options != &#39;&#39;:
&#34;kubectl&#34;: [1, 2, 3, 4, 8], # Define error indices for kube cmd = cmd + &#34; &#34; + self.options
&#34;docker&#34;: [1, 2, 3, 4, 5, 6, 7] # Define error indices for docker if self.logs != &#39;&#39;:
} self.logfile = self._logfile()
if self.password[0] != &#39;&#39;:
eof_indices = { passwords = self._passtx(self.password)
&#34;ssh&#34;: [8, 9, 10, 11], else:
&#34;sftp&#34;: [8, 9, 10, 11], passwords = []
&#34;telnet&#34;: [8, 9, 10, 11], 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;[b|B]ad (owner|permissions)&#34;]
&#34;kubectl&#34;: [5, 6, 7], # Define eof indices for kube else:
&#34;docker&#34;: [8, 9, 10] # Define eof indices for docker raise ValueError(&#34;Invalid protocol: &#34; + self.protocol)
}
initial_indices = {
&#34;ssh&#34;: [0],
&#34;sftp&#34;: [0],
&#34;telnet&#34;: [0],
&#34;kubectl&#34;: [0], # Define special indices for kube
&#34;docker&#34;: [0] # Define special indices for docker
}
attempts = 1 attempts = 1
while attempts &lt;= max_attempts: while attempts &lt;= max_attempts:
child = pexpect.spawn(cmd) child = pexpect.spawn(cmd)
@ -3116,55 +3014,54 @@ 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) &gt; 0:
loops = len(passwords)
else:
loops = 1
endloop = False endloop = False
for i in range(len(passwords) if passwords else 1): for i in range(0, loops):
while True: while True:
results = child.expect(expects[self.protocol], timeout=timeout) results = child.expect(expects, timeout=timeout)
results_value = expects[self.protocol][results] if results == 0:
if results in initial_indices[self.protocol]:
if self.protocol in [&#34;ssh&#34;, &#34;sftp&#34;]: if self.protocol in [&#34;ssh&#34;, &#34;sftp&#34;]:
child.sendline(&#39;yes&#39;) child.sendline(&#39;yes&#39;)
elif self.protocol in [&#34;telnet&#34;, &#34;kubectl&#34;]: elif self.protocol == &#34;telnet&#34;:
if self.user: if self.user != &#39;&#39;:
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_value == pexpect.TIMEOUT and attempts != max_attempts: if results == 12 and attempts != max_attempts:
attempts += 1 attempts += 1
endloop = True endloop = True
break break
else: else:
after = &#34;Connection timeout&#34; if results_value == pexpect.TIMEOUT else child.after.decode() if results == 12:
return f&#34;Connection failed code: {results}\n{child.before.decode().lstrip()}{after}{child.readline().decode()}&#34;.rstrip() after = &#34;Connection timeout&#34;
else:
elif results in eof_indices[self.protocol]: after = child.after.decode()
if results_value == password_prompt: return (&#34;Connection failed code:&#34; + str(results) + &#34;\n&#34; + child.before.decode().lstrip() + after + child.readline().decode()).rstrip()
if passwords: if results == 8:
if len(passwords) &gt; 0:
child.sendline(passwords[i]) child.sendline(passwords[i])
else: else:
self.missingtext = True self.missingtext = True
break break
elif results_value == &#34;suspend&#34;: if results in [9, 11]:
child.sendline(&#34;\r&#34;)
sleep(2)
else:
endloop = True endloop = True
child.sendline() child.sendline()
break break
if results == 10:
child.sendline(&#34;\r&#34;)
sleep(2)
if endloop: if endloop:
break break
if results_value == pexpect.TIMEOUT: if results == 12:
continue continue
else: else:
break break
child.readline(0) child.readline(0)
self.child = child self.child = child
return True</code></pre> return True</code></pre>
@ -3311,14 +3208,6 @@ def run(self, commands, vars = None,*, folder = &#39;&#39;, prompt = r&#39;&gt;$
connect = self._connect(timeout = timeout) connect = self._connect(timeout = timeout)
now = datetime.datetime.now().strftime(&#39;%Y-%m-%d_%H%M%S&#39;) now = datetime.datetime.now().strftime(&#39;%Y-%m-%d_%H%M%S&#39;)
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 &#34;prompt&#34; in self.tags: if &#34;prompt&#34; in self.tags:
prompt = self.tags[&#34;prompt&#34;] prompt = self.tags[&#34;prompt&#34;]
expects = [prompt, pexpect.EOF, pexpect.TIMEOUT] expects = [prompt, pexpect.EOF, pexpect.TIMEOUT]
@ -3447,14 +3336,6 @@ def test(self, commands, expected, vars = None,*, prompt = r&#39;&gt;$|#$|\$$|&g
&#39;&#39;&#39; &#39;&#39;&#39;
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 &#34;prompt&#34; in self.tags: if &#34;prompt&#34; in self.tags:
prompt = self.tags[&#34;prompt&#34;] prompt = self.tags[&#34;prompt&#34;]
expects = [prompt, pexpect.EOF, pexpect.TIMEOUT] expects = [prompt, pexpect.EOF, pexpect.TIMEOUT]

View File

@ -1,11 +1,10 @@
Flask>=2.3.2 Flask>=2.3.2
Flask_Cors>=4.0.1
google_api_python_client>=2.125.0 google_api_python_client>=2.125.0
google_auth_oauthlib>=1.2.0 google_auth_oauthlib>=1.2.0
inquirer>=3.3.0 inquirer>=3.2.4
openai>=0.27.8 openai>=0.27.8
pexpect>=4.8.0 pexpect>=4.8.0
protobuf>=5.27.2 protobuf>=5.26.1
pycryptodome>=3.18.0 pycryptodome>=3.18.0
pyfzf>=0.3.1 pyfzf>=0.3.1
PyYAML>=6.0.1 PyYAML>=6.0.1

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, docker, kubernetes, ssh, telnet, connection manager keywords = networking, automation, 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
@ -29,7 +29,6 @@ install_requires =
pexpect pexpect
pycryptodome pycryptodome
Flask Flask
Flask_Cors
pyfzf pyfzf
waitress waitress
PyYAML PyYAML