Compare commits
19 Commits
1bd9bd62c5
...
main
Author | SHA1 | Date | |
---|---|---|---|
e2e4c9bfe7 | |||
582459d6d3 | |||
b1188587fc | |||
4d8244a10f | |||
a71d8adcb3 | |||
3c01d76391 | |||
89e828451c | |||
3365acb473 | |||
e09481e6f1 | |||
7a46f640e2 | |||
c2584c86ba | |||
a6aff6df76 | |||
e5ebf8eea7 | |||
b80ed64957 | |||
4823238538 | |||
9149d5b157 | |||
a1244855d4 | |||
0805f6f72f | |||
25de08a17c |
3
.gitignore
vendored
3
.gitignore
vendored
@ -130,3 +130,6 @@ dmypy.json
|
||||
|
||||
#clients
|
||||
*sync_client*
|
||||
|
||||
#App
|
||||
connpy-completion-helper
|
||||
|
47
PRIVATE_POLICY.md
Normal file
47
PRIVATE_POLICY.md
Normal 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)
|
82
README.md
82
README.md
@ -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://pypi.org/pypi/connpy/)
|
||||
[](https://pypi.org/pypi/connpy/)
|
||||
[](https://github.com/fluzzi/connpy/blob/main/LICENSE)
|
||||
[](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
|
||||
|
||||
|
@ -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
|
||||
|
@ -1,2 +1,2 @@
|
||||
__version__ = "4.0.0"
|
||||
__version__ = "4.1.3"
|
||||
|
||||
|
30
connpy/ai.py
30
connpy/ai.py
@ -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.
|
||||
|
@ -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"
|
||||
|
@ -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))
|
||||
|
213
connpy/core.py
213
connpy/core.py
@ -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
|
||||
|
180
connpy/core_plugins/context.py
Normal file
180
connpy/core_plugins/context.py
Normal 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
@ -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
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user