Compare commits

...

19 Commits

Author SHA1 Message Date
e2e4c9bfe7 minor bug fixes, add new tags, add bulk using file 2025-05-09 17:44:29 -03:00
582459d6d3 bug fix docker 2025-01-30 17:39:56 -03:00
b1188587fc change to openai 4o mini and improvements to ai code for new model 2024-07-21 18:39:46 -03:00
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
3365acb473 fix bug list nodes 2024-06-10 15:45:04 -03:00
e09481e6f1 update for google 2024-05-28 15:15:01 -03:00
7a46f640e2 add private policy 2024-05-24 18:22:30 -03:00
c2584c86ba Update README.md 2024-05-24 18:18:46 -03:00
a6aff6df76 update version 2024-05-24 17:57:03 -03:00
e5ebf8eea7 update license 2024-05-24 17:50:18 -03:00
b80ed64957 Update README.md 2024-05-24 17:44:22 -03:00
4823238538 Update README.md 2024-05-24 17:27:08 -03:00
9149d5b157 Update README.md 2024-05-24 17:26:37 -03:00
a1244855d4 add logo 2024-05-24 17:17:33 -03:00
0805f6f72f add logo 2024-05-24 17:15:15 -03:00
25de08a17c Update readme 2024-05-24 15:53:20 -03:00
13 changed files with 1178 additions and 1042 deletions

3
.gitignore vendored
View File

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

47
PRIVATE_POLICY.md Normal file
View File

@ -0,0 +1,47 @@
# Privacy Policy
## Introduction
Welcome to Connpy ("we", "our", "us"). Connpy is committed to protecting your privacy. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you use our app, which utilizes Google Login to manage its own files in your Google Drive. Please read this privacy policy carefully.
## Information We Collect
### Personal Information
When you use Connpy, we may collect the following information:
- **Google Account Information**: Your email address and basic profile information provided by Google during the login process.
### App-Specific Google Drive Files
Connpy requests access only to the files it creates and manages within your Google Drive. We do not access, read, or manipulate any other files in your Google Drive.
## How We Use Your Information
We use the information we collect in the following ways:
- **Authentication**: To log you into the app using your Google account.
- **File Management**: To upload, manage, and organize the files that Connpy creates in your Google Drive.
## Sharing Your Information
We do not share your personal information or any data related to your Google Drive files with third parties, except in the following cases:
- **Legal Obligations**: If required by law, we may disclose your information to comply with legal processes.
## Data Security
We implement appropriate technical and organizational measures to protect your personal information and the files managed by Connpy from unauthorized access, disclosure, alteration, or destruction.
## Your Rights
You have the following rights regarding your information:
- **Access and Update**: You can access and update your profile information through your Google account settings.
- **Revoke Access**: You can revoke Connpy's access to your Google Drive at any time via your Google account permissions settings.
- **Delete Data**: You can delete the files created by Connpy in your Google Drive at any time.
## Changes to This Privacy Policy
We may update this Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on our GitHub repository. You are advised to review this Privacy Policy periodically for any changes.
## Contact Us
If you have any questions about this Privacy Policy, please contact us at:
- **GitHub**: [https://github.com/fluzzi/connpy](https://github.com/fluzzi/connpy)

View File

@ -1,10 +1,16 @@
# Conn
<p align="center">
<img src="https://nginx.gederico.dynu.net/images/CONNPY-resized.png" alt="App Logo">
</p>
# Connpy
[![](https://img.shields.io/pypi/v/connpy.svg?style=flat-square)](https://pypi.org/pypi/connpy/)
[![](https://img.shields.io/pypi/pyversions/connpy.svg?style=flat-square)](https://pypi.org/pypi/connpy/)
[![](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/)
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
@ -18,33 +24,54 @@ docker compose -f path/to/folder/docker-compose.yml run -it connpy-app
```
## Connection manager
### Privacy Policy
Connpy is committed to protecting your privacy. Our privacy policy explains how we handle user data:
- **Data Access**: Connpy accesses data necessary for managing remote host connections, including server addresses, usernames, and passwords. This data is stored locally on your machine and is not transmitted or shared with any third parties.
- **Data Usage**: User data is used solely for the purpose of managing and automating SSH and Telnet connections.
- **Data Storage**: All connection details are stored locally and securely on your device. We do not store or process this data on our servers.
- **Data Sharing**: We do not share any user data with third parties.
### Google Integration
Connpy integrates with Google services for backup purposes:
- **Configuration Backup**: The app allows users to store their device information in the app configuration. This configuration can be synced with Google services to create backups.
- **Data Access**: Connpy only accesses its own files and does not access any other files on your Google account.
- **Data Usage**: The data is used solely for backup and restore purposes, ensuring that your device information and configurations are safe and recoverable.
- **Data Sharing**: Connpy does not share any user data with third parties, including Google. The backup data is only accessible by the user.
For more detailed information, please read our [Privacy Policy](https://connpy.gederico.dynu.net/fluzzi32/connpy/src/branch/main/PRIVATE_POLICY.md).
### Features
- You can generate profiles and reference them from nodes using @profilename so you dont
need to edit multiple nodes when changing password or other information.
- Nodes can be stored on @folder or @subfolder@folder to organize your devices. Then can
be referenced using node@subfolder@folder or node@folder
- If you have too many nodes. Get completion script using: conn config --completion.
Or use fzf installing pyfzf and running conn config --fzf true
- Create in bulk, copy, move, export and import nodes for easy management.
- Run automation scripts in network devices.
- use GPT AI to help you manage your devices.
- Manage connections using SSH, SFTP, Telnet, kubectl, and Docker exec.
- Set contexts to manage specific nodes from specific contexts (work/home/clients/etc).
- You can generate profiles and reference them from nodes using @profilename so you don't
need to edit multiple nodes when changing passwords or other information.
- Nodes can be stored on @folder or @subfolder@folder to organize your devices. They can
be referenced using node@subfolder@folder or node@folder.
- If you have too many nodes, get a completion script using: conn config --completion.
Or use fzf by installing pyfzf and running conn config --fzf true.
- 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.
- Much more!
### Usage:
```
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:
node|folder node[@subfolder][@folder]
Connect to specific node or show all matching nodes
[@subfolder][@folder]
Show all available connections globaly or in specified path
```
node|folder node[@subfolder][@folder]
Connect to specific node or show all matching nodes
[@subfolder][@folder]
Show all available connections globally or in specified path
### Options:
```
options:
-h, --help show this help message and exit
-v, --version Show version
-a, --add Add new node[@subfolder][@folder] or [@subfolder]@folder
@ -53,10 +80,8 @@ positional arguments:
-s, --show Show node[@subfolder][@folder]
-d, --debug Display all conections steps
-t, --sftp Connects using sftp instead of ssh
```
### Commands:
```
Commands:
profile Manage profiles
move(mv) Move node
copy(cp) Copy node
@ -70,6 +95,7 @@ positional arguments:
plugin Manage plugins
config Manage app config
sync Sync config with Google
context Manage contexts with regex matching
```
### Manage profiles:
@ -90,14 +116,26 @@ options:
### Examples:
```
#Add new profile
conn profile --add office-user
#Add new folder
conn --add @office
#Add new subfolder
conn --add @datacenter@office
#Add node to subfolder
conn --add server@datacenter@office
#Add node to folder
conn --add pc@office
#Show node information
conn --show server@datacenter@office
#Connect to nodes
conn pc@office
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

View File

@ -2,32 +2,35 @@
'''
## 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
- You can generate profiles and reference them from nodes using @profilename so you dont
need to edit multiple nodes when changing password or other information.
- Nodes can be stored on @folder or @subfolder@folder to organize your devices. Then can
be referenced using node@subfolder@folder or node@folder
- If you have too many nodes. Get completion script using: conn config --completion.
Or use fzf installing pyfzf and running conn config --fzf true
- Create in bulk, copy, move, export and import nodes for easy management.
- Run automation scripts in network devices.
- use GPT AI to help you manage your devices.
- Manage connections using SSH, SFTP, Telnet, kubectl, and Docker exec.
- Set contexts to manage specific nodes from specific contexts (work/home/clients/etc).
- You can generate profiles and reference them from nodes using @profilename so you don't
need to edit multiple nodes when changing passwords or other information.
- Nodes can be stored on @folder or @subfolder@folder to organize your devices. They can
be referenced using node@subfolder@folder or node@folder.
- If you have too many nodes, get a completion script using: conn config --completion.
Or use fzf by installing pyfzf and running conn config --fzf true.
- 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.
- Much more!
### Usage
```
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:
node|folder node[@subfolder][@folder]
Connect to specific node or show all matching nodes
[@subfolder][@folder]
Show all available connections globaly or in specified path
Options:
node|folder node[@subfolder][@folder]
Connect to specific node or show all matching nodes
[@subfolder][@folder]
Show all available connections globally or in specified path
options:
-h, --help show this help message and exit
-v, --version Show version
-a, --add Add new node[@subfolder][@folder] or [@subfolder]@folder
@ -51,6 +54,7 @@ Commands:
plugin Manage plugins
config Manage app config
sync Sync config with Google
context Manage contexts with regex matching
```
### Manage profiles
@ -71,14 +75,26 @@ options:
### Examples
```
#Add new profile
conn profile --add office-user
#Add new folder
conn --add @office
#Add new subfolder
conn --add @datacenter@office
#Add node to subfolder
conn --add server@datacenter@office
#Add node to folder
conn --add pc@office
#Show node information
conn --show server@datacenter@office
#Connect to nodes
conn pc@office
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
### General Structure

View File

@ -1,2 +1,2 @@
__version__ = "4.0.0"
__version__ = "4.1.3"

View File

@ -14,7 +14,7 @@ class ai:
### Attributes:
- model (str): Model of GPT api to use. Default is gpt-3.5-turbo.
- model (str): Model of GPT api to use. Default is gpt-4o-mini.
- temp (float): Value between 0 and 1 that control the randomness
of generated text, with higher values increasing
@ -39,7 +39,7 @@ class ai:
- api_key (str): A unique authentication token required to access
and interact with the API.
- model (str): Model of GPT api to use. Default is gpt-3.5-turbo.
- model (str): Model of GPT api to use. Default is gpt-4o-mini.
- temp (float): Value between 0 and 1 that control the randomness
of generated text, with higher values increasing
@ -68,7 +68,7 @@ class ai:
try:
self.model = self.config.config["openai"]["model"]
except:
self.model = "gpt-3.5-turbo"
self.model = "gpt-4o-mini"
self.temp = temp
self.__prompt = {}
self.__prompt["original_system"] = """
@ -128,7 +128,7 @@ Categorize the user's request based on the operation they want to perform on the
self.__prompt["original_function"]["parameters"]["required"] = ["type", "filter"]
self.__prompt["command_system"] = """
For each OS listed below, provide the command(s) needed to perform the specified action, depending on the device OS (e.g., Cisco IOSXR router, Linux server).
The application knows how to connect to devices via SSH, so you only need to provide the command(s) to run after connecting.
The application knows how to connect to devices via SSH, so you only need to provide the command(s) to run after connecting. This includes access configuration mode and commiting if required.
If the commands needed are not for the specific OS type, just send an empty list (e.g., []).
Note: Preserving the integrity of user-provided commands is of utmost importance. If a user has provided a specific command to run, include that command exactly as it was given, even if it's not recognized or understood. Under no circumstances should you modify or alter user-provided commands.
"""
@ -143,7 +143,7 @@ Categorize the user's request based on the operation they want to perform on the
self.__prompt["command_function"]["name"] = "get_commands"
self.__prompt["command_function"]["descriptions"] = """
For each OS listed below, provide the command(s) needed to perform the specified action, depending on the device OS (e.g., Cisco IOSXR router, Linux server).
The application knows how to connect to devices via SSH, so you only need to provide the command(s) to run after connecting.
The application knows how to connect to devices via SSH, so you only need to provide the command(s) to run after connecting. This includes access configuration mode and commiting if required.
If the commands needed are not for the specific OS type, just send an empty list (e.g., []).
"""
self.__prompt["command_function"]["parameters"] = {}
@ -196,7 +196,7 @@ Categorize the user's request based on the operation they want to perform on the
@MethodHook
def _clean_command_response(self, raw_response, node_list):
#Parse response for command request to openAI GPT.
# Parse response for command request to openAI GPT.
info_dict = {}
info_dict["commands"] = []
info_dict["variables"] = {}
@ -204,14 +204,24 @@ Categorize the user's request based on the operation they want to perform on the
for key, value in node_list.items():
newvalue = {}
commands = raw_response[value]
for i,e in enumerate(commands, start=1):
newvalue[f"command{i}"] = e
# Ensure commands is a list
if isinstance(commands, str):
commands = [commands]
# Determine the number of digits required for zero-padding
num_commands = len(commands)
num_digits = len(str(num_commands))
for i, e in enumerate(commands, start=1):
# Zero-pad the command number
command_num = f"command{str(i).zfill(num_digits)}"
newvalue[command_num] = e
if f"{{command{i}}}" not in info_dict["commands"]:
info_dict["commands"].append(f"{{command{i}}}")
info_dict["variables"]["__global__"][f"command{i}"] = ""
info_dict["commands"].append(f"{{{command_num}}}")
info_dict["variables"]["__global__"][command_num] = ""
info_dict["variables"][key] = newvalue
return info_dict
@MethodHook
def _get_commands(self, user_input, nodes):
#Send the request for commands for each device to openAI GPT.

View File

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

View File

@ -8,7 +8,7 @@ import sys
import inquirer
from .core import node,nodes
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 .plugins import Plugins
import yaml
@ -42,6 +42,7 @@ class connapp:
the config file.
'''
self.app = app
self.node = node
self.nodes = nodes
self.start_api = start_api
@ -109,6 +110,7 @@ class connapp:
#BULKPARSER
bulkparser = subparsers.add_parser("bulk", description="Add nodes in bulk")
bulkparser.add_argument("bulk", const="bulk", nargs=0, action=self._store_type, help="Add nodes in bulk")
bulkparser.add_argument("-f", "--file", nargs=1, help="Import nodes from a file. First line nodes, second line hosts")
bulkparser.set_defaults(func=self._func_others)
# EXPORTPARSER
exportparser = subparsers.add_parser("export", description="Export connection folder to Yaml file")
@ -312,11 +314,7 @@ class connapp:
if uniques == False:
print("Invalid node {}".format(args.data))
exit(5)
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'.")
self._print_instructions()
newnode = self._questions_nodes(args.data, uniques)
if newnode == False:
exit(7)
@ -343,7 +341,7 @@ class connapp:
elif isinstance(v, dict):
print(k + ":")
for i,d in v.items():
print(" - " + i + ": " + d)
print(" - " + i + ": " + str(d))
def _mod(self, args):
if args.data == None:
@ -442,7 +440,7 @@ class connapp:
elif isinstance(v, dict):
print(k + ":")
for i,d in v.items():
print(" - " + i + ": " + d)
print(" - " + i + ": " + str(d))
def _profile_add(self, args):
matches = list(filter(lambda k: k == args.data[0], self.profiles))
@ -484,7 +482,11 @@ class connapp:
return actions.get(args.command)(args)
def _ls(self, args):
items = getattr(self, args.data)
if args.data == "nodes":
attribute = "nodes_list"
else:
attribute = args.data
items = getattr(self, attribute)
if args.filter:
items = [ item for item in items if re.search(args.filter[0], item)]
if args.format and args.data == "nodes":
@ -541,7 +543,19 @@ class connapp:
print("{} {} succesfully to {}".format(args.data[0],action, args.data[1]))
def _bulk(self, args):
newnodes = self._questions_bulk()
if args.file and os.path.isfile(args.file[0]):
with open(args.file[0], 'r') as f:
lines = f.readlines()
# Expecting exactly 2 lines
if len(lines) < 2:
raise ValueError("The file must contain at least two lines: one for nodes, one for hosts.")
nodes = lines[0].strip()
hosts = lines[1].strip()
newnodes = self._questions_bulk(nodes, hosts)
else:
newnodes = self._questions_bulk()
if newnodes == False:
exit(7)
if not self.case:
@ -1077,16 +1091,16 @@ class connapp:
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
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
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
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
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[1:] not in self.profiles:
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
@ -1107,7 +1121,7 @@ class connapp:
def _port_validation(self, answers, current, regex = "(^[0-9]*$|^@.+$)"):
#Validate port in inquirer when managing nodes
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:
port = int(current)
except:
@ -1213,7 +1227,7 @@ class connapp:
#Inquirer questions when editing nodes or profiles
questions = []
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("options", message="Edit Options?"))
questions.append(inquirer.Confirm("logs", message="Edit logging path/file?"))
@ -1243,7 +1257,7 @@ class connapp:
else:
node["host"] = defaults["host"]
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:
node["protocol"] = defaults["protocol"]
if edit["port"]:
@ -1251,7 +1265,7 @@ class connapp:
else:
node["port"] = defaults["port"]
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:
node["options"] = defaults["options"]
if edit["logs"]:
@ -1317,7 +1331,7 @@ class connapp:
else:
profile["host"] = defaults["host"]
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:
profile["protocol"] = defaults["protocol"]
if edit["port"]:
@ -1325,7 +1339,7 @@ class connapp:
else:
profile["port"] = defaults["port"]
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:
profile["options"] = defaults["options"]
if edit["logs"]:
@ -1360,15 +1374,15 @@ class connapp:
result["id"] = unique
return result
def _questions_bulk(self):
def _questions_bulk(self, nodes="", hosts=""):
#Questions when using bulk command
questions = []
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", default=nodes, 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("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("host", message="Add comma separated list of Hostnames or IPs", default=hosts, validate=self._bulk_host_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("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("tags", message="Add tags dictionary", validate=self._tags_validation))
questions.append(inquirer.Text("jumphost", message="Add Jumphost node", validate=self._jumphost_validation))
@ -1548,3 +1562,45 @@ tasks:
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
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.
@ -326,6 +326,14 @@ class node:
connect = self._connect(timeout = timeout)
now = datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')
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:
prompt = self.tags["prompt"]
expects = [prompt, pexpect.EOF, pexpect.TIMEOUT]
@ -413,6 +421,14 @@ class node:
'''
connect = self._connect(timeout = timeout)
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:
prompt = self.tags["prompt"]
expects = [prompt, pexpect.EOF, pexpect.TIMEOUT]
@ -468,102 +484,165 @@ class node:
return connect
@MethodHook
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.
def _generate_ssh_sftp_cmd(self):
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"]:
cmd = self.protocol
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)"]
return self._generate_ssh_sftp_cmd()
elif self.protocol == "telnet":
cmd = "telnet " + self.host
if self.port != '':
cmd = cmd + " " + self.port
if self.options != '':
cmd = cmd + " " + self.options
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)"]
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("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
while attempts <= max_attempts:
child = pexpect.spawn(cmd)
if isinstance(self.tags, dict) and self.tags.get("console"):
child.sendline()
if debug:
print(cmd)
self.mylog = io.BytesIO()
child.logfile_read = self.mylog
if len(passwords) > 0:
loops = len(passwords)
else:
loops = 1
endloop = False
for i in range(0, loops):
for i in range(len(passwords) if passwords else 1):
while True:
results = child.expect(expects, timeout=timeout)
if results == 0:
results = child.expect(expects[self.protocol], timeout=timeout)
results_value = expects[self.protocol][results]
if results in initial_indices[self.protocol]:
if self.protocol in ["ssh", "sftp"]:
child.sendline('yes')
elif self.protocol == "telnet":
if self.user != '':
elif self.protocol in ["telnet", "kubectl", "docker"]:
if self.user:
child.sendline(self.user)
else:
self.missingtext = True
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()
if results == 12 and attempts != max_attempts:
if results_value == pexpect.TIMEOUT and attempts != max_attempts:
attempts += 1
endloop = True
break
else:
if results == 12:
after = "Connection timeout"
after = "Connection timeout" if results_value == pexpect.TIMEOUT else child.after.decode()
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:
after = child.after.decode()
return ("Connection failed code:" + str(results) + "\n" + child.before.decode().lstrip() + after + child.readline().decode()).rstrip()
if results == 8:
if len(passwords) > 0:
child.sendline(passwords[i])
self.missingtext = True
break
elif results_value == "suspend":
child.sendline("\r")
sleep(2)
else:
self.missingtext = True
break
if results in [9, 11]:
endloop = True
child.sendline()
break
if results == 10:
child.sendline("\r")
sleep(2)
endloop = True
child.sendline()
break
if endloop:
break
if results == 12:
if results_value == pexpect.TIMEOUT:
continue
else:
break
if isinstance(self.tags, dict) and self.tags.get("post_connect_commands"):
cmds = self.tags.get("post_connect_commands")
commands = [cmds] if isinstance(cmds, str) else cmds
for command in commands:
child.sendline(command)
sleep(1)
child.readline(0)
self.child = child
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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -4,11 +4,11 @@ version = attr: connpy._version.__version__
description = Connpy is a SSH/Telnet connection manager and automation module
long_description = file: README.md
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_email = fluzzi@gmail.com
url = https://github.com/fluzzi/connpy
license = MIT License
license = Custom Software License
license_files = LICENSE
project_urls =
Bug Tracker = https://github.com/fluzzi/connpy/issues
@ -29,6 +29,7 @@ install_requires =
pexpect
pycryptodome
Flask
Flask_Cors
pyfzf
waitress
PyYAML