Compare commits

..

4 Commits

Author SHA1 Message Date
4d8244a10f Add features:
- New protocols: Docker and Kubectl
 - Add contexts to filter the number of nodes
 - Add option to modify the api using plugins
 - Minor bug fixes
2024-07-15 15:38:01 -03:00
a71d8adcb3 bug fix 2024-07-05 17:49:53 -03:00
3c01d76391 add contexts and api plugins 2024-07-02 16:53:07 -03:00
89e828451c add kubectl and docker support 2024-06-17 15:58:28 -03:00
11 changed files with 672 additions and 227 deletions

3
.gitignore vendored
View File

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

View File

@ -9,7 +9,8 @@
[![](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 and telnet connection manager and automation module for Linux, Mac and Docker Connpy is a SSH, SFTP, Telnet, kubectl, and Docker pod connection manager and automation module for Linux, Mac, and Docker.
## Installation ## Installation
@ -43,33 +44,34 @@ 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
- You can generate profiles and reference them from nodes using @profilename so you dont - Manage connections using SSH, SFTP, Telnet, kubectl, and Docker exec.
need to edit multiple nodes when changing password or other information. - Set contexts to manage specific nodes from specific contexts (work/home/clients/etc).
- Nodes can be stored on @folder or @subfolder@folder to organize your devices. Then can - You can generate profiles and reference them from nodes using @profilename so you don't
be referenced using node@subfolder@folder or node@folder need to edit multiple nodes when changing passwords or other information.
- If you have too many nodes. Get completion script using: conn config --completion. - Nodes can be stored on @folder or @subfolder@folder to organize your devices. They can
Or use fzf installing pyfzf and running conn config --fzf true be referenced using node@subfolder@folder or node@folder.
- Create in bulk, copy, move, export and import nodes for easy management. - If you have too many nodes, get a completion script using: conn config --completion.
- Run automation scripts in network devices. Or use fzf by installing pyfzf and running conn config --fzf true.
- use GPT AI to help you manage your devices. - Create in bulk, copy, move, export, and import nodes for easy management.
- 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} ... conn {profile,move,mv,copy,cp,list,ls,bulk,export,import,ai,run,api,plugin,config,sync,context} ...
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 globaly or in specified path Show all available connections globally 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
@ -78,10 +80,8 @@ positional arguments:
-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,6 +95,7 @@ positional arguments:
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:
@ -115,14 +116,26 @@ 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,32 +2,35 @@
''' '''
## Connection manager ## Connection manager
Connpy is a connection manager that allows you to store nodes to connect them fast and password free. Connpy is a SSH, SFTP, Telnet, kubectl, and Docker pod connection manager and automation module for Linux, Mac, and Docker.
### Features ### Features
- You can generate profiles and reference them from nodes using @profilename so you dont - Manage connections using SSH, SFTP, Telnet, kubectl, and Docker exec.
need to edit multiple nodes when changing password or other information. - Set contexts to manage specific nodes from specific contexts (work/home/clients/etc).
- Nodes can be stored on @folder or @subfolder@folder to organize your devices. Then can - You can generate profiles and reference them from nodes using @profilename so you don't
be referenced using node@subfolder@folder or node@folder need to edit multiple nodes when changing passwords or other information.
- If you have too many nodes. Get completion script using: conn config --completion. - Nodes can be stored on @folder or @subfolder@folder to organize your devices. They can
Or use fzf installing pyfzf and running conn config --fzf true be referenced using node@subfolder@folder or node@folder.
- Create in bulk, copy, move, export and import nodes for easy management. - If you have too many nodes, get a completion script using: conn config --completion.
- Run automation scripts in network devices. Or use fzf by installing pyfzf and running conn config --fzf true.
- use GPT AI to help you manage your devices. - Create in bulk, copy, move, export, and import nodes for easy management.
- 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} ... conn {profile,move,mv,copy,cp,list,ls,bulk,export,import,ai,run,api,plugin,config,sync,context} ...
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 globaly or in specified path Show all available connections globally 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
@ -51,6 +54,7 @@ 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
@ -71,14 +75,26 @@ 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.0.3" __version__ = "4.1.0"

View File

@ -1,4 +1,5 @@
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
@ -6,6 +7,7 @@ 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 from .api import start_api,stop_api,debug_api,app
from .ai import ai from .ai import ai
from .plugins import Plugins from .plugins import Plugins
import yaml import yaml
@ -42,6 +42,7 @@ 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
@ -312,11 +313,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 +1078,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 +1108,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 +1214,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 +1244,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 +1252,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 +1318,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 +1326,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 +1367,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 +1549,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. cmd = self.protocol
if self.idletime > 0:
cmd += " -o ServerAliveInterval=" + str(self.idletime)
if self.port:
if self.protocol == "ssh":
cmd += " -p " + self.port
elif self.protocol == "sftp":
cmd += " -P " + self.port
if self.options:
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"]: if self.protocol in ["ssh", "sftp"]:
cmd = self.protocol return self._generate_ssh_sftp_cmd()
if self.idletime > 0:
cmd = cmd + " -o ServerAliveInterval=" + str(self.idletime)
if self.port != '':
if self.protocol == "ssh":
cmd = cmd + " -p " + self.port
elif self.protocol == "sftp":
cmd = cmd + " -P " + self.port
if self.options != '':
cmd = cmd + " " + self.options
if self.logs != '':
self.logfile = self._logfile()
if self.jumphost != '':
cmd = cmd + " " + self.jumphost
if self.password[0] != '':
passwords = self._passtx(self.password)
else:
passwords = []
if self.user == '':
cmd = cmd + " {}".format(self.host)
else:
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": 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()
if self.logs != '':
self.logfile = self._logfile()
if self.password[0] != '':
passwords = self._passtx(self.password)
else:
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)"]
else: else:
raise ValueError("Invalid protocol: " + self.protocol) 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 != '':
self.logfile = self._logfile()
default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$'
prompt = self.tags.get("prompt", default_prompt) if isinstance(self.tags, dict) else default_prompt
password_prompt = '[p|P]assword:|[u|U]sername:' if self.protocol != 'telnet' else '[p|P]assword:'
expects = {
"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)"],
"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_value == pexpect.TIMEOUT else child.after.decode()
after = "Connection timeout" return f"Connection failed code: {results}\n{child.before.decode().lstrip()}{after}{child.readline().decode()}".rstrip()
elif results in eof_indices[self.protocol]:
if results_value == password_prompt:
if passwords:
child.sendline(passwords[i])
else: else:
after = child.after.decode() self.missingtext = True
return ("Connection failed code:" + str(results) + "\n" + child.before.decode().lstrip() + after + child.readline().decode()).rstrip() break
if results == 8: elif results_value == "suspend":
if len(passwords) > 0: child.sendline("\r")
child.sendline(passwords[i]) sleep(2)
else: else:
self.missingtext = True endloop = True
break child.sendline()
if results in [9, 11]: break
endloop = True
child.sendline()
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

@ -0,0 +1,180 @@
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,30 +23,33 @@
</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 connection manager that allows you to store nodes to connect them fast and password free.</p> <p>Connpy is a SSH, SFTP, Telnet, kubectl, and Docker pod connection manager and automation module for Linux, Mac, and Docker.</p>
<h3 id="features">Features</h3> <h3 id="features">Features</h3>
<pre><code>- You can generate profiles and reference them from nodes using @profilename so you dont <pre><code>- Manage connections using SSH, SFTP, Telnet, kubectl, and Docker exec.
need to edit multiple nodes when changing password or other information. - Set contexts to manage specific nodes from specific contexts (work/home/clients/etc).
- Nodes can be stored on @folder or @subfolder@folder to organize your devices. Then can - You can generate profiles and reference them from nodes using @profilename so you don't
be referenced using node@subfolder@folder or node@folder need to edit multiple nodes when changing passwords or other information.
- If you have too many nodes. Get completion script using: conn config --completion. - Nodes can be stored on @folder or @subfolder@folder to organize your devices. They can
Or use fzf installing pyfzf and running conn config --fzf true be referenced using node@subfolder@folder or node@folder.
- Create in bulk, copy, move, export and import nodes for easy management. - If you have too many nodes, get a completion script using: conn config --completion.
- Run automation scripts in network devices. Or use fzf by installing pyfzf and running conn config --fzf true.
- use GPT AI to help you manage your devices. - Create in bulk, copy, move, export, and import nodes for easy management.
- 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} ... conn {profile,move,mv,copy,cp,list,ls,bulk,export,import,ai,run,api,plugin,config,sync,context} ...
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 globaly or in specified path Show all available connections globally 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
@ -70,6 +73,7 @@ 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
@ -86,14 +90,26 @@ options:
</code></pre> </code></pre>
<h3 id="examples">Examples</h3> <h3 id="examples">Examples</h3>
<pre><code> conn profile --add office-user <pre><code> #Add new profile
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>
@ -416,32 +432,35 @@ print(result)
&#39;&#39;&#39; &#39;&#39;&#39;
## Connection manager ## Connection manager
Connpy is a connection manager that allows you to store nodes to connect them fast and password free. Connpy is a SSH, SFTP, Telnet, kubectl, and Docker pod connection manager and automation module for Linux, Mac, and Docker.
### Features ### Features
- You can generate profiles and reference them from nodes using @profilename so you dont - Manage connections using SSH, SFTP, Telnet, kubectl, and Docker exec.
need to edit multiple nodes when changing password or other information. - Set contexts to manage specific nodes from specific contexts (work/home/clients/etc).
- Nodes can be stored on @folder or @subfolder@folder to organize your devices. Then can - You can generate profiles and reference them from nodes using @profilename so you don&#39;t
be referenced using node@subfolder@folder or node@folder need to edit multiple nodes when changing passwords or other information.
- If you have too many nodes. Get completion script using: conn config --completion. - Nodes can be stored on @folder or @subfolder@folder to organize your devices. They can
Or use fzf installing pyfzf and running conn config --fzf true be referenced using node@subfolder@folder or node@folder.
- Create in bulk, copy, move, export and import nodes for easy management. - If you have too many nodes, get a completion script using: conn config --completion.
- Run automation scripts in network devices. Or use fzf by installing pyfzf and running conn config --fzf true.
- use GPT AI to help you manage your devices. - Create in bulk, copy, move, export, and import nodes for easy management.
- 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} ... conn {profile,move,mv,copy,cp,list,ls,bulk,export,import,ai,run,api,plugin,config,sync,context} ...
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 globaly or in specified path Show all available connections globally 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
@ -465,6 +484,7 @@ 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
@ -485,14 +505,26 @@ 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
@ -2497,7 +2529,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 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.
@ -2555,7 +2587,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.
@ -2824,6 +2856,14 @@ 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]
@ -2911,6 +2951,14 @@ 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]
@ -2966,47 +3014,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. cmd = self.protocol
if self.idletime &gt; 0:
cmd += &#34; -o ServerAliveInterval=&#34; + str(self.idletime)
if self.port:
if self.protocol == &#34;ssh&#34;:
cmd += &#34; -p &#34; + self.port
elif self.protocol == &#34;sftp&#34;:
cmd += &#34; -P &#34; + self.port
if self.options:
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;]: if self.protocol in [&#34;ssh&#34;, &#34;sftp&#34;]:
cmd = self.protocol return self._generate_ssh_sftp_cmd()
if self.idletime &gt; 0:
cmd = cmd + &#34; -o ServerAliveInterval=&#34; + str(self.idletime)
if self.port != &#39;&#39;:
if self.protocol == &#34;ssh&#34;:
cmd = cmd + &#34; -p &#34; + self.port
elif self.protocol == &#34;sftp&#34;:
cmd = cmd + &#34; -P &#34; + self.port
if self.options != &#39;&#39;:
cmd = cmd + &#34; &#34; + self.options
if self.logs != &#39;&#39;:
self.logfile = self._logfile()
if self.jumphost != &#39;&#39;:
cmd = cmd + &#34; &#34; + self.jumphost
if self.password[0] != &#39;&#39;:
passwords = self._passtx(self.password)
else:
passwords = []
if self.user == &#39;&#39;:
cmd = cmd + &#34; {}&#34;.format(self.host)
else:
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;: elif self.protocol == &#34;telnet&#34;:
cmd = &#34;telnet &#34; + self.host return self._generate_telnet_cmd()
if self.port != &#39;&#39;: elif self.protocol == &#34;kubectl&#34;:
cmd = cmd + &#34; &#34; + self.port return self._generate_kube_cmd()
if self.options != &#39;&#39;: elif self.protocol == &#34;docker&#34;:
cmd = cmd + &#34; &#34; + self.options return self._generate_docker_cmd()
if self.logs != &#39;&#39;:
self.logfile = self._logfile()
if self.password[0] != &#39;&#39;:
passwords = self._passtx(self.password)
else:
passwords = []
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;]
else: else:
raise ValueError(&#34;Invalid protocol: &#34; + self.protocol) 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;:
self.logfile = self._logfile()
default_prompt = r&#39;&gt;$|#$|\$$|&gt;.$|#.$|\$.$&#39;
prompt = self.tags.get(&#34;prompt&#34;, default_prompt) if isinstance(self.tags, dict) else default_prompt
password_prompt = &#39;[p|P]assword:|[u|U]sername:&#39; if self.protocol != &#39;telnet&#39; else &#39;[p|P]assword:&#39;
expects = {
&#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;],
&#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;],
&#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;],
&#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;],
&#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]
}
error_indices = {
&#34;ssh&#34;: [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16],
&#34;sftp&#34;: [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16],
&#34;telnet&#34;: [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16],
&#34;kubectl&#34;: [1, 2, 3, 4, 8], # Define error indices for kube
&#34;docker&#34;: [1, 2, 3, 4, 5, 6, 7] # Define error indices for docker
}
eof_indices = {
&#34;ssh&#34;: [8, 9, 10, 11],
&#34;sftp&#34;: [8, 9, 10, 11],
&#34;telnet&#34;: [8, 9, 10, 11],
&#34;kubectl&#34;: [5, 6, 7], # Define eof indices for kube
&#34;docker&#34;: [8, 9, 10] # Define eof indices for docker
}
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)
@ -3014,54 +3116,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) &gt; 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 [&#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 == &#34;telnet&#34;: elif self.protocol in [&#34;telnet&#34;, &#34;kubectl&#34;]:
if self.user != &#39;&#39;: 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 = &#34;Connection timeout&#34; if results_value == pexpect.TIMEOUT else child.after.decode()
after = &#34;Connection timeout&#34; return f&#34;Connection failed code: {results}\n{child.before.decode().lstrip()}{after}{child.readline().decode()}&#34;.rstrip()
elif results in eof_indices[self.protocol]:
if results_value == password_prompt:
if passwords:
child.sendline(passwords[i])
else: else:
after = child.after.decode() self.missingtext = True
return (&#34;Connection failed code:&#34; + str(results) + &#34;\n&#34; + child.before.decode().lstrip() + after + child.readline().decode()).rstrip() break
if results == 8: elif results_value == &#34;suspend&#34;:
if len(passwords) &gt; 0: child.sendline(&#34;\r&#34;)
child.sendline(passwords[i]) sleep(2)
else: else:
self.missingtext = True endloop = True
break child.sendline()
if results in [9, 11]: break
endloop = True
child.sendline()
break
if results == 10:
child.sendline(&#34;\r&#34;)
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</code></pre> return True</code></pre>
@ -3208,6 +3311,14 @@ 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]
@ -3336,6 +3447,14 @@ 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,10 +1,11 @@
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.2.4 inquirer>=3.3.0
openai>=0.27.8 openai>=0.27.8
pexpect>=4.8.0 pexpect>=4.8.0
protobuf>=5.26.1 protobuf>=5.27.2
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, 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
@ -29,6 +29,7 @@ install_requires =
pexpect pexpect
pycryptodome pycryptodome
Flask Flask
Flask_Cors
pyfzf pyfzf
waitress waitress
PyYAML PyYAML