add capture feature, change printing mechanics for all app, bug fixes
This commit is contained in:
94
README.md
94
README.md
@@ -154,9 +154,8 @@ options:
|
|||||||
- **Purpose**: Handles parsing of command-line arguments.
|
- **Purpose**: Handles parsing of command-line arguments.
|
||||||
- **Requirements**:
|
- **Requirements**:
|
||||||
- Must contain only one method: `__init__`.
|
- Must contain only one method: `__init__`.
|
||||||
- The `__init__` method must initialize at least two attributes:
|
- The `__init__` method must initialize at least one attribute:
|
||||||
- `self.parser`: An instance of `argparse.ArgumentParser`.
|
- `self.parser`: An instance of `argparse.ArgumentParser`.
|
||||||
- `self.description`: A string containing the description of the parser.
|
|
||||||
2. **Class `Entrypoint`**:
|
2. **Class `Entrypoint`**:
|
||||||
- **Purpose**: Acts as the entry point for plugin execution, utilizing parsed arguments and integrating with the main application.
|
- **Purpose**: Acts as the entry point for plugin execution, utilizing parsed arguments and integrating with the main application.
|
||||||
- **Requirements**:
|
- **Requirements**:
|
||||||
@@ -253,6 +252,97 @@ There are 2 methods that allows you to define custom logic to be executed before
|
|||||||
- `if __name__ == "__main__":`
|
- `if __name__ == "__main__":`
|
||||||
- This block allows the plugin to be run as a standalone script for testing or independent use.
|
- This block allows the plugin to be run as a standalone script for testing or independent use.
|
||||||
|
|
||||||
|
### Command Completion Support
|
||||||
|
|
||||||
|
Plugins can provide intelligent **tab completion** by defining a function called `_connpy_completion` in the plugin script. This function will be called by Connpy to assist with command-line completion when the user types partial input.
|
||||||
|
|
||||||
|
#### Function Signature
|
||||||
|
|
||||||
|
```
|
||||||
|
def _connpy_completion(wordsnumber, words, info=None):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|----------------|-------------|
|
||||||
|
| `wordsnumber` | Integer indicating the number of words (space-separated tokens) currently on the command line. For plugins, this typically starts at 3 (e.g., `connpy <plugin> ...`). |
|
||||||
|
| `words` | A list of tokens (words) already typed. `words[0]` is always the name of the plugin, followed by any subcommands or arguments. |
|
||||||
|
| `info` | A dictionary of structured context data provided by Connpy to help with suggestions. |
|
||||||
|
|
||||||
|
#### Contents of `info`
|
||||||
|
|
||||||
|
The `info` dictionary contains helpful context to generate completions:
|
||||||
|
|
||||||
|
```
|
||||||
|
info = {
|
||||||
|
"config": config_dict, # The full loaded configuration
|
||||||
|
"nodes": node_list, # List of all known node names
|
||||||
|
"folders": folder_list, # List of all defined folder names
|
||||||
|
"profiles": profile_list, # List of all profile names
|
||||||
|
"plugins": plugin_list # List of all plugin names
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use this data to generate suggestions based on the current input.
|
||||||
|
|
||||||
|
#### Return Value
|
||||||
|
|
||||||
|
The function must return a list of suggestion strings to be presented to the user.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```
|
||||||
|
def _connpy_completion(wordsnumber, words, info=None):
|
||||||
|
if wordsnumber == 3:
|
||||||
|
return ["--help", "--verbose", "start", "stop"]
|
||||||
|
|
||||||
|
elif wordsnumber == 4 and words[2] == "start":
|
||||||
|
return info["nodes"] # Suggest node names
|
||||||
|
|
||||||
|
return []
|
||||||
|
```
|
||||||
|
|
||||||
|
> In this example, if the user types `connpy myplugin start ` and presses Tab, it will suggest node names.
|
||||||
|
|
||||||
|
### Handling Unknown Arguments
|
||||||
|
|
||||||
|
Plugins can choose to accept and process unknown arguments that are **not explicitly defined** in the parser. To enable this behavior, the plugin must define the following hidden argument in its `Parser` class:
|
||||||
|
|
||||||
|
```
|
||||||
|
self.parser.add_argument(
|
||||||
|
"--unknown-args",
|
||||||
|
action="store_true",
|
||||||
|
default=True,
|
||||||
|
help=argparse.SUPPRESS
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Behavior:
|
||||||
|
|
||||||
|
- When this argument is present, Connpy will parse the known arguments and capture any extra (unknown) ones.
|
||||||
|
- These unknown arguments will be passed to the plugin as `args.unknown_args` inside the `Entrypoint`.
|
||||||
|
- If the user does not pass any unknown arguments, `args.unknown_args` will contain the default value (`True`, unless overridden).
|
||||||
|
|
||||||
|
#### Example:
|
||||||
|
|
||||||
|
If a plugin accepts unknown tcpdump flags like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
connpy myplugin -nn -s0
|
||||||
|
```
|
||||||
|
|
||||||
|
And defines the hidden `--unknown-args` flag as shown above, then:
|
||||||
|
|
||||||
|
- `args.unknown_args` inside `Entrypoint.__init__()` will be: `['-nn', '-s0']`
|
||||||
|
|
||||||
|
> This allows the plugin to receive and process arguments intended for external tools (e.g., `tcpdump`) without argparse raising an error.
|
||||||
|
|
||||||
|
#### Note:
|
||||||
|
|
||||||
|
If a plugin does **not** define `--unknown-args`, any extra arguments passed will cause argparse to fail with an unrecognized arguments error.
|
||||||
|
|
||||||
### Script Verification
|
### Script Verification
|
||||||
- The `verify_script` method in `plugins.py` is used to check the plugin script's compliance with these standards.
|
- The `verify_script` method in `plugins.py` is used to check the plugin script's compliance with these standards.
|
||||||
- Non-compliant scripts will be rejected to ensure consistency and proper functionality within the plugin system.
|
- Non-compliant scripts will be rejected to ensure consistency and proper functionality within the plugin system.
|
||||||
|
@@ -112,9 +112,8 @@ options:
|
|||||||
- **Purpose**: Handles parsing of command-line arguments.
|
- **Purpose**: Handles parsing of command-line arguments.
|
||||||
- **Requirements**:
|
- **Requirements**:
|
||||||
- Must contain only one method: `__init__`.
|
- Must contain only one method: `__init__`.
|
||||||
- The `__init__` method must initialize at least two attributes:
|
- The `__init__` method must initialize at least one attribute:
|
||||||
- `self.parser`: An instance of `argparse.ArgumentParser`.
|
- `self.parser`: An instance of `argparse.ArgumentParser`.
|
||||||
- `self.description`: A string containing the description of the parser.
|
|
||||||
2. **Class `Entrypoint`**:
|
2. **Class `Entrypoint`**:
|
||||||
- **Purpose**: Acts as the entry point for plugin execution, utilizing parsed arguments and integrating with the main application.
|
- **Purpose**: Acts as the entry point for plugin execution, utilizing parsed arguments and integrating with the main application.
|
||||||
- **Requirements**:
|
- **Requirements**:
|
||||||
@@ -210,6 +209,97 @@ There are 2 methods that allows you to define custom logic to be executed before
|
|||||||
- `if __name__ == "__main__":`
|
- `if __name__ == "__main__":`
|
||||||
- This block allows the plugin to be run as a standalone script for testing or independent use.
|
- This block allows the plugin to be run as a standalone script for testing or independent use.
|
||||||
|
|
||||||
|
### Command Completion Support
|
||||||
|
|
||||||
|
Plugins can provide intelligent **tab completion** by defining a function called `_connpy_completion` in the plugin script. This function will be called by Connpy to assist with command-line completion when the user types partial input.
|
||||||
|
|
||||||
|
#### Function Signature
|
||||||
|
|
||||||
|
```
|
||||||
|
def _connpy_completion(wordsnumber, words, info=None):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|----------------|-------------|
|
||||||
|
| `wordsnumber` | Integer indicating the number of words (space-separated tokens) currently on the command line. For plugins, this typically starts at 3 (e.g., `connpy <plugin> ...`). |
|
||||||
|
| `words` | A list of tokens (words) already typed. `words[0]` is always the name of the plugin, followed by any subcommands or arguments. |
|
||||||
|
| `info` | A dictionary of structured context data provided by Connpy to help with suggestions. |
|
||||||
|
|
||||||
|
#### Contents of `info`
|
||||||
|
|
||||||
|
The `info` dictionary contains helpful context to generate completions:
|
||||||
|
|
||||||
|
```
|
||||||
|
info = {
|
||||||
|
"config": config_dict, # The full loaded configuration
|
||||||
|
"nodes": node_list, # List of all known node names
|
||||||
|
"folders": folder_list, # List of all defined folder names
|
||||||
|
"profiles": profile_list, # List of all profile names
|
||||||
|
"plugins": plugin_list # List of all plugin names
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use this data to generate suggestions based on the current input.
|
||||||
|
|
||||||
|
#### Return Value
|
||||||
|
|
||||||
|
The function must return a list of suggestion strings to be presented to the user.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```
|
||||||
|
def _connpy_completion(wordsnumber, words, info=None):
|
||||||
|
if wordsnumber == 3:
|
||||||
|
return ["--help", "--verbose", "start", "stop"]
|
||||||
|
|
||||||
|
elif wordsnumber == 4 and words[2] == "start":
|
||||||
|
return info["nodes"] # Suggest node names
|
||||||
|
|
||||||
|
return []
|
||||||
|
```
|
||||||
|
|
||||||
|
> In this example, if the user types `connpy myplugin start ` and presses Tab, it will suggest node names.
|
||||||
|
|
||||||
|
### Handling Unknown Arguments
|
||||||
|
|
||||||
|
Plugins can choose to accept and process unknown arguments that are **not explicitly defined** in the parser. To enable this behavior, the plugin must define the following hidden argument in its `Parser` class:
|
||||||
|
|
||||||
|
```
|
||||||
|
self.parser.add_argument(
|
||||||
|
"--unknown-args",
|
||||||
|
action="store_true",
|
||||||
|
default=True,
|
||||||
|
help=argparse.SUPPRESS
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Behavior:
|
||||||
|
|
||||||
|
- When this argument is present, Connpy will parse the known arguments and capture any extra (unknown) ones.
|
||||||
|
- These unknown arguments will be passed to the plugin as `args.unknown_args` inside the `Entrypoint`.
|
||||||
|
- If the user does not pass any unknown arguments, `args.unknown_args` will contain the default value (`True`, unless overridden).
|
||||||
|
|
||||||
|
#### Example:
|
||||||
|
|
||||||
|
If a plugin accepts unknown tcpdump flags like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
connpy myplugin -nn -s0
|
||||||
|
```
|
||||||
|
|
||||||
|
And defines the hidden `--unknown-args` flag as shown above, then:
|
||||||
|
|
||||||
|
- `args.unknown_args` inside `Entrypoint.__init__()` will be: `['-nn', '-s0']`
|
||||||
|
|
||||||
|
> This allows the plugin to receive and process arguments intended for external tools (e.g., `tcpdump`) without argparse raising an error.
|
||||||
|
|
||||||
|
#### Note:
|
||||||
|
|
||||||
|
If a plugin does **not** define `--unknown-args`, any extra arguments passed will cause argparse to fail with an unrecognized arguments error.
|
||||||
|
|
||||||
### Script Verification
|
### Script Verification
|
||||||
- The `verify_script` method in `plugins.py` is used to check the plugin script's compliance with these standards.
|
- The `verify_script` method in `plugins.py` is used to check the plugin script's compliance with these standards.
|
||||||
- Non-compliant scripts will be rejected to ensure consistency and proper functionality within the plugin system.
|
- Non-compliant scripts will be rejected to ensure consistency and proper functionality within the plugin system.
|
||||||
@@ -422,8 +512,9 @@ from .ai import ai
|
|||||||
from .plugins import Plugins
|
from .plugins import Plugins
|
||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
from pkg_resources import get_distribution
|
from pkg_resources import get_distribution
|
||||||
|
from . import printer
|
||||||
|
|
||||||
__all__ = ["node", "nodes", "configfile", "connapp", "ai", "Plugins"]
|
__all__ = ["node", "nodes", "configfile", "connapp", "ai", "Plugins", "printer"]
|
||||||
__author__ = "Federico Luzzi"
|
__author__ = "Federico Luzzi"
|
||||||
__pdoc__ = {
|
__pdoc__ = {
|
||||||
'core': False,
|
'core': False,
|
||||||
@@ -438,5 +529,6 @@ __pdoc__ = {
|
|||||||
'node.deferred_class_hooks': False,
|
'node.deferred_class_hooks': False,
|
||||||
'nodes.deferred_class_hooks': False,
|
'nodes.deferred_class_hooks': False,
|
||||||
'connapp': False,
|
'connapp': False,
|
||||||
'connapp.encrypt': True
|
'connapp.encrypt': True,
|
||||||
|
'printer': False
|
||||||
}
|
}
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
__version__ = "4.1.4"
|
__version__ = "4.2b1"
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
from flask import Flask, request, jsonify
|
from flask import Flask, request, jsonify
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from connpy import configfile, node, nodes, hooks
|
from connpy import configfile, node, nodes, hooks, printer
|
||||||
from connpy.ai import ai as myai
|
from connpy.ai import ai as myai
|
||||||
from waitress import serve
|
from waitress import serve
|
||||||
import os
|
import os
|
||||||
@@ -143,7 +143,7 @@ def stop_api():
|
|||||||
port = int(f.readline().strip())
|
port = int(f.readline().strip())
|
||||||
PID_FILE=PID_FILE2
|
PID_FILE=PID_FILE2
|
||||||
except:
|
except:
|
||||||
print("Connpy api server is not running.")
|
printer.warning("Connpy API server is not running.")
|
||||||
return
|
return
|
||||||
# Send a SIGTERM signal to the process
|
# Send a SIGTERM signal to the process
|
||||||
try:
|
try:
|
||||||
@@ -152,7 +152,7 @@ def stop_api():
|
|||||||
pass
|
pass
|
||||||
# Delete the PID file
|
# Delete the PID file
|
||||||
os.remove(PID_FILE)
|
os.remove(PID_FILE)
|
||||||
print(f"Server with process ID {pid} stopped.")
|
printer.info(f"Server with process ID {pid} stopped.")
|
||||||
return port
|
return port
|
||||||
|
|
||||||
@hooks.MethodHook
|
@hooks.MethodHook
|
||||||
@@ -168,7 +168,7 @@ def start_server(port=8048):
|
|||||||
@hooks.MethodHook
|
@hooks.MethodHook
|
||||||
def start_api(port=8048):
|
def start_api(port=8048):
|
||||||
if os.path.exists(PID_FILE1) or os.path.exists(PID_FILE2):
|
if os.path.exists(PID_FILE1) or os.path.exists(PID_FILE2):
|
||||||
print("Connpy server is already running.")
|
printer.warning("Connpy server is already running.")
|
||||||
return
|
return
|
||||||
pid = os.fork()
|
pid = os.fork()
|
||||||
if pid == 0:
|
if pid == 0:
|
||||||
@@ -182,7 +182,7 @@ def start_api(port=8048):
|
|||||||
with open(PID_FILE2, "w") as f:
|
with open(PID_FILE2, "w") as f:
|
||||||
f.write(str(pid) + "\n" + str(port))
|
f.write(str(pid) + "\n" + str(port))
|
||||||
except:
|
except:
|
||||||
print("Cound't create PID file")
|
printer.error("Couldn't create PID file.")
|
||||||
return
|
exit(1)
|
||||||
print(f'Server is running with process ID {pid} in port {port}')
|
printer.start(f"Server is running with process ID {pid} on port {port}")
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@ import sys
|
|||||||
import inquirer
|
import inquirer
|
||||||
from .core import node,nodes
|
from .core import node,nodes
|
||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
|
from . import printer
|
||||||
from .api import start_api,stop_api,debug_api,app
|
from .api import start_api,stop_api,debug_api,app
|
||||||
from .ai import ai
|
from .ai import ai
|
||||||
from .plugins import Plugins
|
from .plugins import Plugins
|
||||||
@@ -17,8 +18,13 @@ class NoAliasDumper(yaml.SafeDumper):
|
|||||||
def ignore_aliases(self, data):
|
def ignore_aliases(self, data):
|
||||||
return True
|
return True
|
||||||
import ast
|
import ast
|
||||||
from rich import print as mdprint
|
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
|
from rich.console import Console, Group
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.text import Text
|
||||||
|
from rich.rule import Rule
|
||||||
|
from rich.style import Style
|
||||||
|
mdprint = Console().print
|
||||||
try:
|
try:
|
||||||
from pyfzf.pyfzf import FzfPrompt
|
from pyfzf.pyfzf import FzfPrompt
|
||||||
except:
|
except:
|
||||||
@@ -70,7 +76,7 @@ class connapp:
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
#DEFAULTPARSER
|
#DEFAULTPARSER
|
||||||
defaultparser = argparse.ArgumentParser(prog = "conn", description = "SSH and Telnet connection manager", formatter_class=argparse.RawTextHelpFormatter)
|
defaultparser = argparse.ArgumentParser(prog = "connpy", description = "SSH and Telnet connection manager", formatter_class=argparse.RawTextHelpFormatter)
|
||||||
subparsers = defaultparser.add_subparsers(title="Commands", dest="subcommand")
|
subparsers = defaultparser.add_subparsers(title="Commands", dest="subcommand")
|
||||||
#NODEPARSER
|
#NODEPARSER
|
||||||
nodeparser = subparsers.add_parser("node", formatter_class=argparse.RawTextHelpFormatter)
|
nodeparser = subparsers.add_parser("node", formatter_class=argparse.RawTextHelpFormatter)
|
||||||
@@ -190,6 +196,10 @@ class connapp:
|
|||||||
argv[0] = "profile"
|
argv[0] = "profile"
|
||||||
if len(argv) < 1 or argv[0] not in self.commands:
|
if len(argv) < 1 or argv[0] not in self.commands:
|
||||||
argv.insert(0,"node")
|
argv.insert(0,"node")
|
||||||
|
args, unknown_args = defaultparser.parse_known_args(argv)
|
||||||
|
if hasattr(args, "unknown_args"):
|
||||||
|
args.unknown_args = unknown_args
|
||||||
|
else:
|
||||||
args = defaultparser.parse_args(argv)
|
args = defaultparser.parse_args(argv)
|
||||||
if args.subcommand in self.plugins.plugins:
|
if args.subcommand in self.plugins.plugins:
|
||||||
self.plugins.plugins[args.subcommand].Entrypoint(args, self.plugins.plugin_parsers[args.subcommand].parser, self)
|
self.plugins.plugins[args.subcommand].Entrypoint(args, self.plugins.plugin_parsers[args.subcommand].parser, self)
|
||||||
@@ -211,14 +221,14 @@ class connapp:
|
|||||||
return actions.get(args.action)(args)
|
return actions.get(args.action)(args)
|
||||||
|
|
||||||
def _version(self, args):
|
def _version(self, args):
|
||||||
print(__version__)
|
printer.info(f"Connpy {__version__}")
|
||||||
|
|
||||||
def _connect(self, args):
|
def _connect(self, args):
|
||||||
if args.data == None:
|
if args.data == None:
|
||||||
matches = self.nodes_list
|
matches = self.nodes_list
|
||||||
if len(matches) == 0:
|
if len(matches) == 0:
|
||||||
print("There are no nodes created")
|
printer.warning("There are no nodes created")
|
||||||
print("try: conn --help")
|
printer.info("try: connpy --help")
|
||||||
exit(9)
|
exit(9)
|
||||||
else:
|
else:
|
||||||
if args.data.startswith("@"):
|
if args.data.startswith("@"):
|
||||||
@@ -226,7 +236,7 @@ class connapp:
|
|||||||
else:
|
else:
|
||||||
matches = list(filter(lambda k: k.startswith(args.data), self.nodes_list))
|
matches = list(filter(lambda k: k.startswith(args.data), self.nodes_list))
|
||||||
if len(matches) == 0:
|
if len(matches) == 0:
|
||||||
print("{} not found".format(args.data))
|
printer.error("{} not found".format(args.data))
|
||||||
exit(2)
|
exit(2)
|
||||||
elif len(matches) > 1:
|
elif len(matches) > 1:
|
||||||
matches[0] = self._choose(matches,"node", "connect")
|
matches[0] = self._choose(matches,"node", "connect")
|
||||||
@@ -243,16 +253,16 @@ class connapp:
|
|||||||
|
|
||||||
def _del(self, args):
|
def _del(self, args):
|
||||||
if args.data == None:
|
if args.data == None:
|
||||||
print("Missing argument node")
|
printer.error("Missing argument node")
|
||||||
exit(3)
|
exit(3)
|
||||||
elif args.data.startswith("@"):
|
elif args.data.startswith("@"):
|
||||||
matches = list(filter(lambda k: k == args.data, self.folders))
|
matches = list(filter(lambda k: k == args.data, self.folders))
|
||||||
else:
|
else:
|
||||||
matches = self.config._getallnodes(args.data)
|
matches = self.config._getallnodes(args.data)
|
||||||
if len(matches) == 0:
|
if len(matches) == 0:
|
||||||
print("{} not found".format(args.data))
|
printer.error("{} not found".format(args.data))
|
||||||
exit(2)
|
exit(2)
|
||||||
print("Removing: {}".format(matches))
|
printer.info("Removing: {}".format(matches))
|
||||||
question = [inquirer.Confirm("delete", message="Are you sure you want to continue?")]
|
question = [inquirer.Confirm("delete", message="Are you sure you want to continue?")]
|
||||||
confirm = inquirer.prompt(question)
|
confirm = inquirer.prompt(question)
|
||||||
if confirm == None:
|
if confirm == None:
|
||||||
@@ -267,14 +277,14 @@ class connapp:
|
|||||||
self.config._connections_del(**nodeuniques)
|
self.config._connections_del(**nodeuniques)
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
if len(matches) == 1:
|
if len(matches) == 1:
|
||||||
print("{} deleted succesfully".format(matches[0]))
|
printer.success("{} deleted successfully".format(matches[0]))
|
||||||
else:
|
else:
|
||||||
print(f"{len(matches)} nodes deleted succesfully")
|
printer.success(f"{len(matches)} nodes deleted successfully")
|
||||||
|
|
||||||
def _add(self, args):
|
def _add(self, args):
|
||||||
args.data = self._type_node(args.data)
|
args.data = self._type_node(args.data)
|
||||||
if args.data == None:
|
if args.data == None:
|
||||||
print("Missing argument node")
|
printer.error("Missing argument node")
|
||||||
exit(3)
|
exit(3)
|
||||||
elif args.data.startswith("@"):
|
elif args.data.startswith("@"):
|
||||||
type = "folder"
|
type = "folder"
|
||||||
@@ -285,34 +295,34 @@ class connapp:
|
|||||||
matches = list(filter(lambda k: k == args.data, self.nodes_list))
|
matches = list(filter(lambda k: k == args.data, self.nodes_list))
|
||||||
reversematches = list(filter(lambda k: k == "@" + args.data, self.folders))
|
reversematches = list(filter(lambda k: k == "@" + args.data, self.folders))
|
||||||
if len(matches) > 0:
|
if len(matches) > 0:
|
||||||
print("{} already exist".format(matches[0]))
|
printer.error("{} already exist".format(matches[0]))
|
||||||
exit(4)
|
exit(4)
|
||||||
if len(reversematches) > 0:
|
if len(reversematches) > 0:
|
||||||
print("{} already exist".format(reversematches[0]))
|
printer.error("{} already exist".format(reversematches[0]))
|
||||||
exit(4)
|
exit(4)
|
||||||
else:
|
else:
|
||||||
if type == "folder":
|
if type == "folder":
|
||||||
uniques = self.config._explode_unique(args.data)
|
uniques = self.config._explode_unique(args.data)
|
||||||
if uniques == False:
|
if uniques == False:
|
||||||
print("Invalid folder {}".format(args.data))
|
printer.error("Invalid folder {}".format(args.data))
|
||||||
exit(5)
|
exit(5)
|
||||||
if "subfolder" in uniques.keys():
|
if "subfolder" in uniques.keys():
|
||||||
parent = "@" + uniques["folder"]
|
parent = "@" + uniques["folder"]
|
||||||
if parent not in self.folders:
|
if parent not in self.folders:
|
||||||
print("Folder {} not found".format(uniques["folder"]))
|
printer.error("Folder {} not found".format(uniques["folder"]))
|
||||||
exit(2)
|
exit(2)
|
||||||
self.config._folder_add(**uniques)
|
self.config._folder_add(**uniques)
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
print("{} added succesfully".format(args.data))
|
printer.success("{} added successfully".format(args.data))
|
||||||
if type == "node":
|
if type == "node":
|
||||||
nodefolder = args.data.partition("@")
|
nodefolder = args.data.partition("@")
|
||||||
nodefolder = "@" + nodefolder[2]
|
nodefolder = "@" + nodefolder[2]
|
||||||
if nodefolder not in self.folders and nodefolder != "@":
|
if nodefolder not in self.folders and nodefolder != "@":
|
||||||
print(nodefolder + " not found")
|
printer.error(nodefolder + " not found")
|
||||||
exit(2)
|
exit(2)
|
||||||
uniques = self.config._explode_unique(args.data)
|
uniques = self.config._explode_unique(args.data)
|
||||||
if uniques == False:
|
if uniques == False:
|
||||||
print("Invalid node {}".format(args.data))
|
printer.error("Invalid node {}".format(args.data))
|
||||||
exit(5)
|
exit(5)
|
||||||
self._print_instructions()
|
self._print_instructions()
|
||||||
newnode = self._questions_nodes(args.data, uniques)
|
newnode = self._questions_nodes(args.data, uniques)
|
||||||
@@ -320,44 +330,43 @@ class connapp:
|
|||||||
exit(7)
|
exit(7)
|
||||||
self.config._connections_add(**newnode)
|
self.config._connections_add(**newnode)
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
print("{} added succesfully".format(args.data))
|
printer.success("{} added successfully".format(args.data))
|
||||||
|
|
||||||
def _show(self, args):
|
def _show(self, args):
|
||||||
if args.data == None:
|
if args.data == None:
|
||||||
print("Missing argument node")
|
printer.error("Missing argument node")
|
||||||
exit(3)
|
exit(3)
|
||||||
matches = list(filter(lambda k: k == args.data, self.nodes_list))
|
if args.data.startswith("@"):
|
||||||
|
matches = list(filter(lambda k: args.data in k, self.nodes_list))
|
||||||
|
else:
|
||||||
|
matches = list(filter(lambda k: k.startswith(args.data), self.nodes_list))
|
||||||
if len(matches) == 0:
|
if len(matches) == 0:
|
||||||
print("{} not found".format(args.data))
|
printer.error("{} not found".format(args.data))
|
||||||
exit(2)
|
exit(2)
|
||||||
|
elif len(matches) > 1:
|
||||||
|
matches[0] = self._choose(matches,"node", "connect")
|
||||||
|
if matches[0] == None:
|
||||||
|
exit(7)
|
||||||
node = self.config.getitem(matches[0])
|
node = self.config.getitem(matches[0])
|
||||||
for k, v in node.items():
|
yaml_output = yaml.dump(node, sort_keys=False, default_flow_style=False)
|
||||||
if isinstance(v, str):
|
printer.custom(matches[0],"")
|
||||||
print(k + ": " + v)
|
print(yaml_output)
|
||||||
elif isinstance(v, list):
|
|
||||||
print(k + ":")
|
|
||||||
for i in v:
|
|
||||||
print(" - " + i)
|
|
||||||
elif isinstance(v, dict):
|
|
||||||
print(k + ":")
|
|
||||||
for i,d in v.items():
|
|
||||||
print(" - " + i + ": " + str(d))
|
|
||||||
|
|
||||||
def _mod(self, args):
|
def _mod(self, args):
|
||||||
if args.data == None:
|
if args.data == None:
|
||||||
print("Missing argument node")
|
printer.error("Missing argument node")
|
||||||
exit(3)
|
exit(3)
|
||||||
matches = self.config._getallnodes(args.data)
|
matches = self.config._getallnodes(args.data)
|
||||||
if len(matches) == 0:
|
if len(matches) == 0:
|
||||||
print("No connection found with filter: {}".format(args.data))
|
printer.error("No connection found with filter: {}".format(args.data))
|
||||||
exit(2)
|
exit(2)
|
||||||
elif len(matches) == 1:
|
elif len(matches) == 1:
|
||||||
uniques = self.config._explode_unique(args.data)
|
uniques = self.config._explode_unique(matches[0])
|
||||||
unique = matches[0]
|
unique = matches[0]
|
||||||
else:
|
else:
|
||||||
uniques = {"id": None, "folder": None}
|
uniques = {"id": None, "folder": None}
|
||||||
unique = None
|
unique = None
|
||||||
print("Editing: {}".format(matches))
|
printer.info("Editing: {}".format(matches))
|
||||||
node = {}
|
node = {}
|
||||||
for i in matches:
|
for i in matches:
|
||||||
node[i] = self.config.getitem(i)
|
node[i] = self.config.getitem(i)
|
||||||
@@ -371,12 +380,12 @@ class connapp:
|
|||||||
uniques.update(node[matches[0]])
|
uniques.update(node[matches[0]])
|
||||||
uniques["type"] = "connection"
|
uniques["type"] = "connection"
|
||||||
if sorted(updatenode.items()) == sorted(uniques.items()):
|
if sorted(updatenode.items()) == sorted(uniques.items()):
|
||||||
print("Nothing to do here")
|
printer.info("Nothing to do here")
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
self.config._connections_add(**updatenode)
|
self.config._connections_add(**updatenode)
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
print("{} edited succesfully".format(args.data))
|
printer.success("{} edited successfully".format(args.data))
|
||||||
else:
|
else:
|
||||||
for k in node:
|
for k in node:
|
||||||
updatednode = self.config._explode_unique(k)
|
updatednode = self.config._explode_unique(k)
|
||||||
@@ -388,12 +397,12 @@ class connapp:
|
|||||||
editcount += 1
|
editcount += 1
|
||||||
updatednode[key] = updatenode[key]
|
updatednode[key] = updatenode[key]
|
||||||
if not editcount:
|
if not editcount:
|
||||||
print("Nothing to do here")
|
printer.info("Nothing to do here")
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
self.config._connections_add(**updatednode)
|
self.config._connections_add(**updatednode)
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
print("{} edited succesfully".format(matches))
|
printer.success("{} edited successfully".format(matches))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@@ -407,57 +416,48 @@ class connapp:
|
|||||||
def _profile_del(self, args):
|
def _profile_del(self, args):
|
||||||
matches = list(filter(lambda k: k == args.data[0], self.profiles))
|
matches = list(filter(lambda k: k == args.data[0], self.profiles))
|
||||||
if len(matches) == 0:
|
if len(matches) == 0:
|
||||||
print("{} not found".format(args.data[0]))
|
printer.error("{} not found".format(args.data[0]))
|
||||||
exit(2)
|
exit(2)
|
||||||
if matches[0] == "default":
|
if matches[0] == "default":
|
||||||
print("Can't delete default profile")
|
printer.error("Can't delete default profile")
|
||||||
exit(6)
|
exit(6)
|
||||||
usedprofile = self.config._profileused(matches[0])
|
usedprofile = self.config._profileused(matches[0])
|
||||||
if len(usedprofile) > 0:
|
if len(usedprofile) > 0:
|
||||||
print("Profile {} used in the following nodes:".format(matches[0]))
|
printer.error(f"Profile {matches[0]} used in the following nodes:\n{', '.join(usedprofile)}")
|
||||||
print(", ".join(usedprofile))
|
|
||||||
exit(8)
|
exit(8)
|
||||||
question = [inquirer.Confirm("delete", message="Are you sure you want to delete {}?".format(matches[0]))]
|
question = [inquirer.Confirm("delete", message="Are you sure you want to delete {}?".format(matches[0]))]
|
||||||
confirm = inquirer.prompt(question)
|
confirm = inquirer.prompt(question)
|
||||||
if confirm["delete"]:
|
if confirm["delete"]:
|
||||||
self.config._profiles_del(id = matches[0])
|
self.config._profiles_del(id = matches[0])
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
print("{} deleted succesfully".format(matches[0]))
|
printer.success("{} deleted successfully".format(matches[0]))
|
||||||
|
|
||||||
def _profile_show(self, args):
|
def _profile_show(self, args):
|
||||||
matches = list(filter(lambda k: k == args.data[0], self.profiles))
|
matches = list(filter(lambda k: k == args.data[0], self.profiles))
|
||||||
if len(matches) == 0:
|
if len(matches) == 0:
|
||||||
print("{} not found".format(args.data[0]))
|
printer.error("{} not found".format(args.data[0]))
|
||||||
exit(2)
|
exit(2)
|
||||||
profile = self.config.profiles[matches[0]]
|
profile = self.config.profiles[matches[0]]
|
||||||
for k, v in profile.items():
|
yaml_output = yaml.dump(profile, sort_keys=False, default_flow_style=False)
|
||||||
if isinstance(v, str):
|
printer.custom(matches[0],"")
|
||||||
print(k + ": " + v)
|
print(yaml_output)
|
||||||
elif isinstance(v, list):
|
|
||||||
print(k + ":")
|
|
||||||
for i in v:
|
|
||||||
print(" - " + i)
|
|
||||||
elif isinstance(v, dict):
|
|
||||||
print(k + ":")
|
|
||||||
for i,d in v.items():
|
|
||||||
print(" - " + i + ": " + str(d))
|
|
||||||
|
|
||||||
def _profile_add(self, args):
|
def _profile_add(self, args):
|
||||||
matches = list(filter(lambda k: k == args.data[0], self.profiles))
|
matches = list(filter(lambda k: k == args.data[0], self.profiles))
|
||||||
if len(matches) > 0:
|
if len(matches) > 0:
|
||||||
print("Profile {} Already exist".format(matches[0]))
|
printer.error("Profile {} Already exist".format(matches[0]))
|
||||||
exit(4)
|
exit(4)
|
||||||
newprofile = self._questions_profiles(args.data[0])
|
newprofile = self._questions_profiles(args.data[0])
|
||||||
if newprofile == False:
|
if newprofile == False:
|
||||||
exit(7)
|
exit(7)
|
||||||
self.config._profiles_add(**newprofile)
|
self.config._profiles_add(**newprofile)
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
print("{} added succesfully".format(args.data[0]))
|
printer.success("{} added successfully".format(args.data[0]))
|
||||||
|
|
||||||
def _profile_mod(self, args):
|
def _profile_mod(self, args):
|
||||||
matches = list(filter(lambda k: k == args.data[0], self.profiles))
|
matches = list(filter(lambda k: k == args.data[0], self.profiles))
|
||||||
if len(matches) == 0:
|
if len(matches) == 0:
|
||||||
print("{} not found".format(args.data[0]))
|
printer.error("{} not found".format(args.data[0]))
|
||||||
exit(2)
|
exit(2)
|
||||||
profile = self.config.profiles[matches[0]]
|
profile = self.config.profiles[matches[0]]
|
||||||
oldprofile = {"id": matches[0]}
|
oldprofile = {"id": matches[0]}
|
||||||
@@ -469,12 +469,12 @@ class connapp:
|
|||||||
if not updateprofile:
|
if not updateprofile:
|
||||||
exit(7)
|
exit(7)
|
||||||
if sorted(updateprofile.items()) == sorted(oldprofile.items()):
|
if sorted(updateprofile.items()) == sorted(oldprofile.items()):
|
||||||
print("Nothing to do here")
|
printer.info("Nothing to do here")
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
self.config._profiles_add(**updateprofile)
|
self.config._profiles_add(**updateprofile)
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
print("{} edited succesfully".format(args.data[0]))
|
printer.success("{} edited successfully".format(args.data[0]))
|
||||||
|
|
||||||
def _func_others(self, args):
|
def _func_others(self, args):
|
||||||
#Function called when using other commands
|
#Function called when using other commands
|
||||||
@@ -509,7 +509,9 @@ class connapp:
|
|||||||
formated[upper_key] = upper_value
|
formated[upper_key] = upper_value
|
||||||
newitems.append(args.format[0].format(**formated))
|
newitems.append(args.format[0].format(**formated))
|
||||||
items = newitems
|
items = newitems
|
||||||
print(*items, sep="\n")
|
yaml_output = yaml.dump(items, sort_keys=False, default_flow_style=False)
|
||||||
|
printer.custom(args.data,"")
|
||||||
|
print(yaml_output)
|
||||||
|
|
||||||
def _mvcp(self, args):
|
def _mvcp(self, args):
|
||||||
if not self.case:
|
if not self.case:
|
||||||
@@ -518,20 +520,20 @@ class connapp:
|
|||||||
source = list(filter(lambda k: k == args.data[0], self.nodes_list))
|
source = list(filter(lambda k: k == args.data[0], self.nodes_list))
|
||||||
dest = list(filter(lambda k: k == args.data[1], self.nodes_list))
|
dest = list(filter(lambda k: k == args.data[1], self.nodes_list))
|
||||||
if len(source) != 1:
|
if len(source) != 1:
|
||||||
print("{} not found".format(args.data[0]))
|
printer.error("{} not found".format(args.data[0]))
|
||||||
exit(2)
|
exit(2)
|
||||||
if len(dest) > 0:
|
if len(dest) > 0:
|
||||||
print("Node {} Already exist".format(args.data[1]))
|
printer.error("Node {} Already exist".format(args.data[1]))
|
||||||
exit(4)
|
exit(4)
|
||||||
nodefolder = args.data[1].partition("@")
|
nodefolder = args.data[1].partition("@")
|
||||||
nodefolder = "@" + nodefolder[2]
|
nodefolder = "@" + nodefolder[2]
|
||||||
if nodefolder not in self.folders and nodefolder != "@":
|
if nodefolder not in self.folders and nodefolder != "@":
|
||||||
print("{} not found".format(nodefolder))
|
printer.error("{} not found".format(nodefolder))
|
||||||
exit(2)
|
exit(2)
|
||||||
olduniques = self.config._explode_unique(args.data[0])
|
olduniques = self.config._explode_unique(args.data[0])
|
||||||
newuniques = self.config._explode_unique(args.data[1])
|
newuniques = self.config._explode_unique(args.data[1])
|
||||||
if newuniques == False:
|
if newuniques == False:
|
||||||
print("Invalid node {}".format(args.data[1]))
|
printer.error("Invalid node {}".format(args.data[1]))
|
||||||
exit(5)
|
exit(5)
|
||||||
node = self.config.getitem(source[0])
|
node = self.config.getitem(source[0])
|
||||||
newnode = {**newuniques, **node}
|
newnode = {**newuniques, **node}
|
||||||
@@ -540,7 +542,7 @@ class connapp:
|
|||||||
self.config._connections_del(**olduniques)
|
self.config._connections_del(**olduniques)
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
action = "moved" if args.command == "move" else "copied"
|
action = "moved" if args.command == "move" else "copied"
|
||||||
print("{} {} succesfully to {}".format(args.data[0],action, args.data[1]))
|
printer.success("{} {} successfully to {}".format(args.data[0],action, args.data[1]))
|
||||||
|
|
||||||
def _bulk(self, args):
|
def _bulk(self, args):
|
||||||
if args.file and os.path.isfile(args.file[0]):
|
if args.file and os.path.isfile(args.file[0]):
|
||||||
@@ -549,7 +551,9 @@ class connapp:
|
|||||||
|
|
||||||
# Expecting exactly 2 lines
|
# Expecting exactly 2 lines
|
||||||
if len(lines) < 2:
|
if len(lines) < 2:
|
||||||
raise ValueError("The file must contain at least two lines: one for nodes, one for hosts.")
|
printer.error("The file must contain at least two lines: one for nodes, one for hosts.")
|
||||||
|
exit(11)
|
||||||
|
|
||||||
|
|
||||||
nodes = lines[0].strip()
|
nodes = lines[0].strip()
|
||||||
hosts = lines[1].strip()
|
hosts = lines[1].strip()
|
||||||
@@ -569,10 +573,10 @@ class connapp:
|
|||||||
matches = list(filter(lambda k: k == unique, self.nodes_list))
|
matches = list(filter(lambda k: k == unique, self.nodes_list))
|
||||||
reversematches = list(filter(lambda k: k == "@" + unique, self.folders))
|
reversematches = list(filter(lambda k: k == "@" + unique, self.folders))
|
||||||
if len(matches) > 0:
|
if len(matches) > 0:
|
||||||
print("Node {} already exist, ignoring it".format(unique))
|
printer.info("Node {} already exist, ignoring it".format(unique))
|
||||||
continue
|
continue
|
||||||
if len(reversematches) > 0:
|
if len(reversematches) > 0:
|
||||||
print("Folder with name {} already exist, ignoring it".format(unique))
|
printer.info("Folder with name {} already exist, ignoring it".format(unique))
|
||||||
continue
|
continue
|
||||||
newnode = {"id": n}
|
newnode = {"id": n}
|
||||||
if newnodes["location"] != "":
|
if newnodes["location"] != "":
|
||||||
@@ -596,9 +600,9 @@ class connapp:
|
|||||||
self.nodes_list = self.config._getallnodes()
|
self.nodes_list = self.config._getallnodes()
|
||||||
if count > 0:
|
if count > 0:
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
print("Succesfully added {} nodes".format(count))
|
printer.success("Successfully added {} nodes".format(count))
|
||||||
else:
|
else:
|
||||||
print("0 nodes added")
|
printer.info("0 nodes added")
|
||||||
|
|
||||||
def _completion(self, args):
|
def _completion(self, args):
|
||||||
if args.data[0] == "bash":
|
if args.data[0] == "bash":
|
||||||
@@ -633,7 +637,7 @@ class connapp:
|
|||||||
folder = os.path.abspath(args.data[0]).rstrip('/')
|
folder = os.path.abspath(args.data[0]).rstrip('/')
|
||||||
with open(pathfile, "w") as f:
|
with open(pathfile, "w") as f:
|
||||||
f.write(str(folder))
|
f.write(str(folder))
|
||||||
print("Config saved")
|
printer.success("Config saved")
|
||||||
|
|
||||||
def _openai(self, args):
|
def _openai(self, args):
|
||||||
if "openai" in self.config.config:
|
if "openai" in self.config.config:
|
||||||
@@ -647,37 +651,37 @@ class connapp:
|
|||||||
def _change_settings(self, name, value):
|
def _change_settings(self, name, value):
|
||||||
self.config.config[name] = value
|
self.config.config[name] = value
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
print("Config saved")
|
printer.success("Config saved")
|
||||||
|
|
||||||
def _func_plugin(self, args):
|
def _func_plugin(self, args):
|
||||||
if args.add:
|
if args.add:
|
||||||
if not os.path.exists(args.add[1]):
|
if not os.path.exists(args.add[1]):
|
||||||
print("File {} dosn't exists.".format(args.add[1]))
|
printer.error("File {} dosn't exists.".format(args.add[1]))
|
||||||
exit(14)
|
exit(14)
|
||||||
if args.add[0].isalpha() and args.add[0].islower() and len(args.add[0]) <= 15:
|
if args.add[0].isalpha() and args.add[0].islower() and len(args.add[0]) <= 15:
|
||||||
disabled_dest_file = os.path.join(self.config.defaultdir + "/plugins", args.add[0] + ".py.bkp")
|
disabled_dest_file = os.path.join(self.config.defaultdir + "/plugins", args.add[0] + ".py.bkp")
|
||||||
if args.add[0] in self.commands or os.path.exists(disabled_dest_file):
|
if args.add[0] in self.commands or os.path.exists(disabled_dest_file):
|
||||||
print("Plugin name can't be the same as other commands.")
|
printer.error("Plugin name can't be the same as other commands.")
|
||||||
exit(15)
|
exit(15)
|
||||||
else:
|
else:
|
||||||
check_bad_script = self.plugins.verify_script(args.add[1])
|
check_bad_script = self.plugins.verify_script(args.add[1])
|
||||||
if check_bad_script:
|
if check_bad_script:
|
||||||
print(check_bad_script)
|
printer.error(check_bad_script)
|
||||||
exit(16)
|
exit(16)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
dest_file = os.path.join(self.config.defaultdir + "/plugins", args.add[0] + ".py")
|
dest_file = os.path.join(self.config.defaultdir + "/plugins", args.add[0] + ".py")
|
||||||
shutil.copy2(args.add[1], dest_file)
|
shutil.copy2(args.add[1], dest_file)
|
||||||
print(f"Plugin {args.add[0]} added succesfully.")
|
printer.success(f"Plugin {args.add[0]} added successfully.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed importing plugin file. {e}")
|
printer.error(f"Failed importing plugin file. {e}")
|
||||||
exit(17)
|
exit(17)
|
||||||
else:
|
else:
|
||||||
print("Plugin name should be lowercase letters up to 15 characters.")
|
printer.error("Plugin name should be lowercase letters up to 15 characters.")
|
||||||
exit(15)
|
exit(15)
|
||||||
elif args.update:
|
elif args.update:
|
||||||
if not os.path.exists(args.update[1]):
|
if not os.path.exists(args.update[1]):
|
||||||
print("File {} dosn't exists.".format(args.update[1]))
|
printer.error("File {} dosn't exists.".format(args.update[1]))
|
||||||
exit(14)
|
exit(14)
|
||||||
plugin_file = os.path.join(self.config.defaultdir + "/plugins", args.update[0] + ".py")
|
plugin_file = os.path.join(self.config.defaultdir + "/plugins", args.update[0] + ".py")
|
||||||
disabled_plugin_file = os.path.join(self.config.defaultdir + "/plugins", args.update[0] + ".py.bkp")
|
disabled_plugin_file = os.path.join(self.config.defaultdir + "/plugins", args.update[0] + ".py.bkp")
|
||||||
@@ -686,7 +690,7 @@ class connapp:
|
|||||||
if plugin_exist or disabled_plugin_exist:
|
if plugin_exist or disabled_plugin_exist:
|
||||||
check_bad_script = self.plugins.verify_script(args.update[1])
|
check_bad_script = self.plugins.verify_script(args.update[1])
|
||||||
if check_bad_script:
|
if check_bad_script:
|
||||||
print(check_bad_script)
|
printer.error(check_bad_script)
|
||||||
exit(16)
|
exit(16)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
@@ -696,13 +700,13 @@ class connapp:
|
|||||||
shutil.copy2(args.update[1], disabled_dest_file)
|
shutil.copy2(args.update[1], disabled_dest_file)
|
||||||
else:
|
else:
|
||||||
shutil.copy2(args.update[1], dest_file)
|
shutil.copy2(args.update[1], dest_file)
|
||||||
print(f"Plugin {args.update[0]} updated succesfully.")
|
printer.success(f"Plugin {args.update[0]} updated successfully.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed updating plugin file. {e}")
|
printer.error(f"Failed updating plugin file. {e}")
|
||||||
exit(17)
|
exit(17)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print("Plugin {} dosn't exist.".format(args.update[0]))
|
printer.error("Plugin {} dosn't exist.".format(args.update[0]))
|
||||||
exit(14)
|
exit(14)
|
||||||
elif args.delete:
|
elif args.delete:
|
||||||
plugin_file = os.path.join(self.config.defaultdir + "/plugins", args.delete[0] + ".py")
|
plugin_file = os.path.join(self.config.defaultdir + "/plugins", args.delete[0] + ".py")
|
||||||
@@ -710,7 +714,7 @@ class connapp:
|
|||||||
plugin_exist = os.path.exists(plugin_file)
|
plugin_exist = os.path.exists(plugin_file)
|
||||||
disabled_plugin_exist = os.path.exists(disabled_plugin_file)
|
disabled_plugin_exist = os.path.exists(disabled_plugin_file)
|
||||||
if not plugin_exist and not disabled_plugin_exist:
|
if not plugin_exist and not disabled_plugin_exist:
|
||||||
print("Plugin {} dosn't exist.".format(args.delete[0]))
|
printer.error("Plugin {} dosn't exist.".format(args.delete[0]))
|
||||||
exit(14)
|
exit(14)
|
||||||
question = [inquirer.Confirm("delete", message="Are you sure you want to delete {} plugin?".format(args.delete[0]))]
|
question = [inquirer.Confirm("delete", message="Are you sure you want to delete {} plugin?".format(args.delete[0]))]
|
||||||
confirm = inquirer.prompt(question)
|
confirm = inquirer.prompt(question)
|
||||||
@@ -722,33 +726,33 @@ class connapp:
|
|||||||
os.remove(plugin_file)
|
os.remove(plugin_file)
|
||||||
elif disabled_plugin_exist:
|
elif disabled_plugin_exist:
|
||||||
os.remove(disabled_plugin_file)
|
os.remove(disabled_plugin_file)
|
||||||
print(f"plugin {args.delete[0]} deleted succesfully.")
|
printer.success(f"plugin {args.delete[0]} deleted successfully.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed deleting plugin file. {e}")
|
printer.error(f"Failed deleting plugin file. {e}")
|
||||||
exit(17)
|
exit(17)
|
||||||
elif args.disable:
|
elif args.disable:
|
||||||
plugin_file = os.path.join(self.config.defaultdir + "/plugins", args.disable[0] + ".py")
|
plugin_file = os.path.join(self.config.defaultdir + "/plugins", args.disable[0] + ".py")
|
||||||
disabled_plugin_file = os.path.join(self.config.defaultdir + "/plugins", args.disable[0] + ".py.bkp")
|
disabled_plugin_file = os.path.join(self.config.defaultdir + "/plugins", args.disable[0] + ".py.bkp")
|
||||||
if not os.path.exists(plugin_file) or os.path.exists(disabled_plugin_file):
|
if not os.path.exists(plugin_file) or os.path.exists(disabled_plugin_file):
|
||||||
print("Plugin {} dosn't exist or it's disabled.".format(args.disable[0]))
|
printer.error("Plugin {} dosn't exist or it's disabled.".format(args.disable[0]))
|
||||||
exit(14)
|
exit(14)
|
||||||
try:
|
try:
|
||||||
os.rename(plugin_file, disabled_plugin_file)
|
os.rename(plugin_file, disabled_plugin_file)
|
||||||
print(f"plugin {args.disable[0]} disabled succesfully.")
|
printer.success(f"plugin {args.disable[0]} disabled successfully.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed disabling plugin file. {e}")
|
printer.error(f"Failed disabling plugin file. {e}")
|
||||||
exit(17)
|
exit(17)
|
||||||
elif args.enable:
|
elif args.enable:
|
||||||
plugin_file = os.path.join(self.config.defaultdir + "/plugins", args.enable[0] + ".py")
|
plugin_file = os.path.join(self.config.defaultdir + "/plugins", args.enable[0] + ".py")
|
||||||
disabled_plugin_file = os.path.join(self.config.defaultdir + "/plugins", args.enable[0] + ".py.bkp")
|
disabled_plugin_file = os.path.join(self.config.defaultdir + "/plugins", args.enable[0] + ".py.bkp")
|
||||||
if os.path.exists(plugin_file) or not os.path.exists(disabled_plugin_file):
|
if os.path.exists(plugin_file) or not os.path.exists(disabled_plugin_file):
|
||||||
print("Plugin {} dosn't exist or it's enabled.".format(args.enable[0]))
|
printer.error("Plugin {} dosn't exist or it's enabled.".format(args.enable[0]))
|
||||||
exit(14)
|
exit(14)
|
||||||
try:
|
try:
|
||||||
os.rename(disabled_plugin_file, plugin_file)
|
os.rename(disabled_plugin_file, plugin_file)
|
||||||
print(f"plugin {args.enable[0]} enabled succesfully.")
|
printer.success(f"plugin {args.enable[0]} enabled successfully.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed enabling plugin file. {e}")
|
printer.error(f"Failed enabling plugin file. {e}")
|
||||||
exit(17)
|
exit(17)
|
||||||
elif args.list:
|
elif args.list:
|
||||||
enabled_files = []
|
enabled_files = []
|
||||||
@@ -768,18 +772,19 @@ class connapp:
|
|||||||
if disabled_files:
|
if disabled_files:
|
||||||
plugins["Disabled"] = disabled_files
|
plugins["Disabled"] = disabled_files
|
||||||
if plugins:
|
if plugins:
|
||||||
|
printer.custom("plugins","")
|
||||||
print(yaml.dump(plugins, sort_keys=False))
|
print(yaml.dump(plugins, sort_keys=False))
|
||||||
else:
|
else:
|
||||||
print("There are no plugins added.")
|
printer.warning("There are no plugins added.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _func_import(self, args):
|
def _func_import(self, args):
|
||||||
if not os.path.exists(args.data[0]):
|
if not os.path.exists(args.data[0]):
|
||||||
print("File {} dosn't exist".format(args.data[0]))
|
printer.error("File {} dosn't exist".format(args.data[0]))
|
||||||
exit(14)
|
exit(14)
|
||||||
print("This could overwrite your current configuration!")
|
printer.warning("This could overwrite your current configuration!")
|
||||||
question = [inquirer.Confirm("import", message="Are you sure you want to import {} file?".format(args.data[0]))]
|
question = [inquirer.Confirm("import", message="Are you sure you want to import {} file?".format(args.data[0]))]
|
||||||
confirm = inquirer.prompt(question)
|
confirm = inquirer.prompt(question)
|
||||||
if confirm == None:
|
if confirm == None:
|
||||||
@@ -789,7 +794,7 @@ class connapp:
|
|||||||
with open(args.data[0]) as file:
|
with open(args.data[0]) as file:
|
||||||
imported = yaml.load(file, Loader=yaml.FullLoader)
|
imported = yaml.load(file, Loader=yaml.FullLoader)
|
||||||
except:
|
except:
|
||||||
print("failed reading file {}".format(args.data[0]))
|
printer.error("failed reading file {}".format(args.data[0]))
|
||||||
exit(10)
|
exit(10)
|
||||||
for k,v in imported.items():
|
for k,v in imported.items():
|
||||||
uniques = self.config._explode_unique(k)
|
uniques = self.config._explode_unique(k)
|
||||||
@@ -808,12 +813,12 @@ class connapp:
|
|||||||
uniques.update(v)
|
uniques.update(v)
|
||||||
self.config._connections_add(**uniques)
|
self.config._connections_add(**uniques)
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
print("File {} imported succesfully".format(args.data[0]))
|
printer.success("File {} imported successfully".format(args.data[0]))
|
||||||
return
|
return
|
||||||
|
|
||||||
def _func_export(self, args):
|
def _func_export(self, args):
|
||||||
if os.path.exists(args.data[0]):
|
if os.path.exists(args.data[0]):
|
||||||
print("File {} already exists".format(args.data[0]))
|
printer.error("File {} already exists".format(args.data[0]))
|
||||||
exit(14)
|
exit(14)
|
||||||
if len(args.data[1:]) == 0:
|
if len(args.data[1:]) == 0:
|
||||||
foldercons = self.config._getallnodesfull(extract = False)
|
foldercons = self.config._getallnodesfull(extract = False)
|
||||||
@@ -821,13 +826,13 @@ class connapp:
|
|||||||
for folder in args.data[1:]:
|
for folder in args.data[1:]:
|
||||||
matches = list(filter(lambda k: k == folder, self.folders))
|
matches = list(filter(lambda k: k == folder, self.folders))
|
||||||
if len(matches) == 0 and folder != "@":
|
if len(matches) == 0 and folder != "@":
|
||||||
print("{} folder not found".format(folder))
|
printer.error("{} folder not found".format(folder))
|
||||||
exit(2)
|
exit(2)
|
||||||
foldercons = self.config._getallnodesfull(args.data[1:], extract = False)
|
foldercons = self.config._getallnodesfull(args.data[1:], extract = False)
|
||||||
with open(args.data[0], "w") as file:
|
with open(args.data[0], "w") as file:
|
||||||
yaml.dump(foldercons, file, Dumper=NoAliasDumper, default_flow_style=False)
|
yaml.dump(foldercons, file, Dumper=NoAliasDumper, default_flow_style=False)
|
||||||
file.close()
|
file.close()
|
||||||
print("File {} generated succesfully".format(args.data[0]))
|
printer.success("File {} generated successfully".format(args.data[0]))
|
||||||
exit()
|
exit()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -977,13 +982,13 @@ class connapp:
|
|||||||
|
|
||||||
def _yaml_generate(self, args):
|
def _yaml_generate(self, args):
|
||||||
if os.path.exists(args.data[0]):
|
if os.path.exists(args.data[0]):
|
||||||
print("File {} already exists".format(args.data[0]))
|
printer.error("File {} already exists".format(args.data[0]))
|
||||||
exit(14)
|
exit(14)
|
||||||
else:
|
else:
|
||||||
with open(args.data[0], "w") as file:
|
with open(args.data[0], "w") as file:
|
||||||
file.write(self._help("generate"))
|
file.write(self._help("generate"))
|
||||||
file.close()
|
file.close()
|
||||||
print("File {} generated succesfully".format(args.data[0]))
|
printer.success("File {} generated successfully".format(args.data[0]))
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
def _yaml_run(self, args):
|
def _yaml_run(self, args):
|
||||||
@@ -991,7 +996,7 @@ class connapp:
|
|||||||
with open(args.data[0]) as file:
|
with open(args.data[0]) as file:
|
||||||
scripts = yaml.load(file, Loader=yaml.FullLoader)
|
scripts = yaml.load(file, Loader=yaml.FullLoader)
|
||||||
except:
|
except:
|
||||||
print("failed reading file {}".format(args.data[0]))
|
printer.error("failed reading file {}".format(args.data[0]))
|
||||||
exit(10)
|
exit(10)
|
||||||
for script in scripts["tasks"]:
|
for script in scripts["tasks"]:
|
||||||
self._cli_run(script)
|
self._cli_run(script)
|
||||||
@@ -1007,11 +1012,11 @@ class connapp:
|
|||||||
if action == "test":
|
if action == "test":
|
||||||
args["expected"] = script["expected"]
|
args["expected"] = script["expected"]
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
print("'{}' is mandatory".format(e.args[0]))
|
printer.error("'{}' is mandatory".format(e.args[0]))
|
||||||
exit(11)
|
exit(11)
|
||||||
nodes = self.config._getallnodes(nodelist)
|
nodes = self.config._getallnodes(nodelist)
|
||||||
if len(nodes) == 0:
|
if len(nodes) == 0:
|
||||||
print("{} don't match any node".format(nodelist))
|
printer.error("{} don't match any node".format(nodelist))
|
||||||
exit(2)
|
exit(2)
|
||||||
nodes = self.nodes(self.config.getitems(nodes), config = self.config)
|
nodes = self.nodes(self.config.getitems(nodes), config = self.config)
|
||||||
stdout = False
|
stdout = False
|
||||||
@@ -1037,32 +1042,47 @@ class connapp:
|
|||||||
columns = int(p.group(1))
|
columns = int(p.group(1))
|
||||||
except:
|
except:
|
||||||
columns = 80
|
columns = 80
|
||||||
|
|
||||||
|
|
||||||
|
PANEL_WIDTH = columns
|
||||||
|
|
||||||
if action == "run":
|
if action == "run":
|
||||||
nodes.run(**args)
|
nodes.run(**args)
|
||||||
print(script["name"].upper() + "-" * (columns - len(script["name"])))
|
header = f"{script['name'].upper()}"
|
||||||
for i in nodes.status.keys():
|
|
||||||
print(" " + i + " " + "-" * (columns - len(i) - 13) + (" PASS(0)" if nodes.status[i] == 0 else " FAIL({})".format(nodes.status[i])))
|
|
||||||
if stdout:
|
|
||||||
for line in nodes.output[i].splitlines():
|
|
||||||
print(" " + line)
|
|
||||||
elif action == "test":
|
elif action == "test":
|
||||||
nodes.test(**args)
|
nodes.test(**args)
|
||||||
print(script["name"].upper() + "-" * (columns - len(script["name"])))
|
header = f"{script['name'].upper()}"
|
||||||
for i in nodes.status.keys():
|
|
||||||
print(" " + i + " " + "-" * (columns - len(i) - 13) + (" PASS(0)" if nodes.status[i] == 0 else " FAIL({})".format(nodes.status[i])))
|
|
||||||
if nodes.status[i] == 0:
|
|
||||||
max_length = max(len(s) for s in nodes.result[i].keys())
|
|
||||||
for k,v in nodes.result[i].items():
|
|
||||||
print(" TEST for '{}'".format(k) + " "*(max_length - len(k) + 1) + "--> " + str(v).upper())
|
|
||||||
if stdout:
|
|
||||||
if nodes.status[i] == 0:
|
|
||||||
print(" " + "-" * (max_length + 21))
|
|
||||||
for line in nodes.output[i].splitlines():
|
|
||||||
print(" " + line)
|
|
||||||
else:
|
else:
|
||||||
print("Wrong action '{}'".format(action))
|
printer.error(f"Wrong action '{action}'")
|
||||||
exit(13)
|
exit(13)
|
||||||
|
|
||||||
|
mdprint(Rule(header, style="white"))
|
||||||
|
|
||||||
|
for node in nodes.status:
|
||||||
|
status_str = "[✓] PASS(0)" if nodes.status[node] == 0 else f"[x] FAIL({nodes.status[node]})"
|
||||||
|
title_line = f"{node} — {status_str}"
|
||||||
|
|
||||||
|
test_output = Text()
|
||||||
|
if action == "test" and nodes.status[node] == 0:
|
||||||
|
results = nodes.result[node]
|
||||||
|
test_output.append("TEST RESULTS:\n")
|
||||||
|
max_key_len = max(len(k) for k in results.keys())
|
||||||
|
for k, v in results.items():
|
||||||
|
status = "[✓]" if str(v).upper() == "TRUE" else "[x]"
|
||||||
|
test_output.append(f" {k.ljust(max_key_len)} {status}\n")
|
||||||
|
|
||||||
|
output = nodes.output[node].strip()
|
||||||
|
code_block = Text()
|
||||||
|
if stdout and output:
|
||||||
|
code_block = Text(output + "\n")
|
||||||
|
|
||||||
|
if action == "test" and nodes.status[node] == 0:
|
||||||
|
highlight_words = [k for k, v in nodes.result[node].items() if str(v).upper() == "TRUE"]
|
||||||
|
code_block.highlight_words(highlight_words, style=Style(color="green", bold=True, underline=True))
|
||||||
|
|
||||||
|
panel_content = Group(test_output, Text(""), code_block)
|
||||||
|
mdprint(Panel(panel_content, title=title_line, width=PANEL_WIDTH, border_style="white"))
|
||||||
|
|
||||||
def _choose(self, list, name, action):
|
def _choose(self, list, name, action):
|
||||||
#Generates an inquirer list to pick
|
#Generates an inquirer list to pick
|
||||||
if FzfPrompt and self.fzf:
|
if FzfPrompt and self.fzf:
|
||||||
@@ -1429,7 +1449,7 @@ class connapp:
|
|||||||
if subparser.description != None:
|
if subparser.description != None:
|
||||||
commands.append(subcommand)
|
commands.append(subcommand)
|
||||||
commands = ",".join(commands)
|
commands = ",".join(commands)
|
||||||
usage_help = f"conn [-h] [--add | --del | --mod | --show | --debug] [node|folder] [--sftp]\n conn {{{commands}}} ..."
|
usage_help = f"connpy [-h] [--add | --del | --mod | --show | --debug] [node|folder] [--sftp]\n connpy {{{commands}}} ..."
|
||||||
return usage_help
|
return usage_help
|
||||||
if type == "end":
|
if type == "end":
|
||||||
help_dict = {}
|
help_dict = {}
|
||||||
@@ -1602,5 +1622,4 @@ Here are some important instructions and tips for configuring your new node:
|
|||||||
Please follow these instructions carefully to ensure proper configuration of your new node.
|
Please follow these instructions carefully to ensure proper configuration of your new node.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# print(instructions)
|
|
||||||
mdprint(Markdown(instructions))
|
mdprint(Markdown(instructions))
|
||||||
|
@@ -13,6 +13,7 @@ import threading
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from .hooks import ClassHook, MethodHook
|
from .hooks import ClassHook, MethodHook
|
||||||
|
from . import printer
|
||||||
import io
|
import io
|
||||||
|
|
||||||
#functions and classes
|
#functions and classes
|
||||||
@@ -28,7 +29,7 @@ class node:
|
|||||||
- result(bool): True if expected value is found after running
|
- result(bool): True if expected value is found after running
|
||||||
the commands using test method.
|
the commands using test method.
|
||||||
|
|
||||||
- status (int): 0 if the method run or test run succesfully.
|
- status (int): 0 if the method run or test run successfully.
|
||||||
1 if connection failed.
|
1 if connection failed.
|
||||||
2 if expect timeouts without prompt or EOF.
|
2 if expect timeouts without prompt or EOF.
|
||||||
|
|
||||||
@@ -254,7 +255,7 @@ class node:
|
|||||||
if connect == True:
|
if connect == True:
|
||||||
size = re.search('columns=([0-9]+).*lines=([0-9]+)',str(os.get_terminal_size()))
|
size = re.search('columns=([0-9]+).*lines=([0-9]+)',str(os.get_terminal_size()))
|
||||||
self.child.setwinsize(int(size.group(2)),int(size.group(1)))
|
self.child.setwinsize(int(size.group(2)),int(size.group(1)))
|
||||||
print("Connected to " + self.unique + " at " + self.host + (":" if self.port != '' else '') + self.port + " via: " + self.protocol)
|
printer.success("Connected to " + self.unique + " at " + self.host + (":" if self.port != '' else '') + self.port + " via: " + self.protocol)
|
||||||
if 'logfile' in dir(self):
|
if 'logfile' in dir(self):
|
||||||
# Initialize self.mylog
|
# Initialize self.mylog
|
||||||
if not 'mylog' in dir(self):
|
if not 'mylog' in dir(self):
|
||||||
@@ -279,7 +280,7 @@ class node:
|
|||||||
f.write(self._logclean(self.mylog.getvalue().decode(), True))
|
f.write(self._logclean(self.mylog.getvalue().decode(), True))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(connect)
|
printer.error(connect)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
@MethodHook
|
@MethodHook
|
||||||
@@ -585,7 +586,7 @@ class node:
|
|||||||
if isinstance(self.tags, dict) and self.tags.get("console"):
|
if isinstance(self.tags, dict) and self.tags.get("console"):
|
||||||
child.sendline()
|
child.sendline()
|
||||||
if debug:
|
if debug:
|
||||||
print(cmd)
|
printer.debug(f"Command:\n{cmd}")
|
||||||
self.mylog = io.BytesIO()
|
self.mylog = io.BytesIO()
|
||||||
child.logfile_read = self.mylog
|
child.logfile_read = self.mylog
|
||||||
|
|
||||||
@@ -645,6 +646,8 @@ class node:
|
|||||||
sleep(1)
|
sleep(1)
|
||||||
child.readline(0)
|
child.readline(0)
|
||||||
self.child = child
|
self.child = child
|
||||||
|
from pexpect import fdpexpect
|
||||||
|
self.raw_child = fdpexpect.fdspawn(self.child.child_fd)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ClassHook
|
@ClassHook
|
||||||
@@ -666,7 +669,7 @@ class nodes:
|
|||||||
Created after running method test.
|
Created after running method test.
|
||||||
|
|
||||||
- status (dict): Dictionary formed by nodes unique as keys, value:
|
- status (dict): Dictionary formed by nodes unique as keys, value:
|
||||||
0 if method run or test ended succesfully.
|
0 if method run or test ended successfully.
|
||||||
1 if connection failed.
|
1 if connection failed.
|
||||||
2 if expect timeouts without prompt or EOF.
|
2 if expect timeouts without prompt or EOF.
|
||||||
|
|
||||||
|
387
connpy/core_plugins/capture.py
Normal file
387
connpy/core_plugins/capture.py
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import random
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from pexpect import TIMEOUT
|
||||||
|
from connpy import printer
|
||||||
|
|
||||||
|
class RemoteCapture:
|
||||||
|
def __init__(self, connapp, node_name, interface, namespace=None, use_wireshark=False, tcpdump_filter=None, tcpdump_args=None):
|
||||||
|
self.connapp = connapp
|
||||||
|
self.node_name = node_name
|
||||||
|
self.interface = interface
|
||||||
|
self.namespace = namespace
|
||||||
|
self.use_wireshark = use_wireshark
|
||||||
|
self.tcpdump_filter = tcpdump_filter or []
|
||||||
|
self.tcpdump_args = tcpdump_args if isinstance(tcpdump_args, list) else []
|
||||||
|
|
||||||
|
if node_name.startswith("@"): # fuzzy match
|
||||||
|
matches = [k for k in connapp.nodes_list if node_name in k]
|
||||||
|
else:
|
||||||
|
matches = [k for k in connapp.nodes_list if k.startswith(node_name)]
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
printer.error(f"Node '{node_name}' not found.")
|
||||||
|
sys.exit(2)
|
||||||
|
elif len(matches) > 1:
|
||||||
|
matches[0] = connapp._choose(matches, "node", "capture")
|
||||||
|
|
||||||
|
if matches[0] is None:
|
||||||
|
sys.exit(7)
|
||||||
|
|
||||||
|
node_data = connapp.config.getitem(matches[0])
|
||||||
|
self.node = connapp.node(matches[0], **node_data, config=connapp.config)
|
||||||
|
|
||||||
|
if self.node.protocol != "ssh":
|
||||||
|
printer.error(f"Node '{self.node.unique}' must be an SSH connection.")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
self.wireshark_path = connapp.config.config.get("wireshark_path")
|
||||||
|
|
||||||
|
def _start_local_listener(self, port, ws_proc=None):
|
||||||
|
self.listener_active = True
|
||||||
|
self.listener_conn = None
|
||||||
|
self.listener_connected = threading.Event()
|
||||||
|
|
||||||
|
def listen():
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
s.bind(("localhost", port))
|
||||||
|
s.listen(1)
|
||||||
|
printer.start(f"Listening on localhost:{port}")
|
||||||
|
|
||||||
|
conn, addr = s.accept()
|
||||||
|
self.listener_conn = conn
|
||||||
|
printer.start(f"Connection from {addr}")
|
||||||
|
self.listener_connected.set()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while self.listener_active:
|
||||||
|
data = conn.recv(4096)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
|
||||||
|
if self.use_wireshark and ws_proc:
|
||||||
|
try:
|
||||||
|
ws_proc.stdin.write(data)
|
||||||
|
ws_proc.stdin.flush()
|
||||||
|
except BrokenPipeError:
|
||||||
|
printer.info("Wireshark closed the pipe.")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
sys.stdout.buffer.write(data)
|
||||||
|
sys.stdout.buffer.flush()
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, BrokenPipeError):
|
||||||
|
printer.info("Listener closed due to broken pipe.")
|
||||||
|
else:
|
||||||
|
printer.error(f"Listener error: {e}")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
self.listener_conn = None
|
||||||
|
|
||||||
|
self.listener_thread = threading.Thread(target=listen)
|
||||||
|
self.listener_thread.daemon = True
|
||||||
|
self.listener_thread.start()
|
||||||
|
|
||||||
|
def _is_port_in_use(self, port):
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
return s.connect_ex(('localhost', port)) == 0
|
||||||
|
|
||||||
|
def _find_free_port(self, start=20000, end=30000):
|
||||||
|
for _ in range(10):
|
||||||
|
port = random.randint(start, end)
|
||||||
|
if not self._is_port_in_use(port):
|
||||||
|
return port
|
||||||
|
raise RuntimeError("No free port found for SSH tunnel.")
|
||||||
|
|
||||||
|
def _monitor_wireshark(self, ws_proc):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
ws_proc.wait(timeout=1)
|
||||||
|
self.listener_active = False
|
||||||
|
if self.listener_conn:
|
||||||
|
printer.info("Wireshark exited, stopping listener.")
|
||||||
|
try:
|
||||||
|
self.listener_conn.shutdown(socket.SHUT_RDWR)
|
||||||
|
self.listener_conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
break
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
if not self.listener_active:
|
||||||
|
break
|
||||||
|
time.sleep(0.2)
|
||||||
|
except Exception as e:
|
||||||
|
printer.warning(f"Error in monitor_wireshark: {e}")
|
||||||
|
|
||||||
|
def _detect_sudo_requirement(self):
|
||||||
|
base_cmd = f"tcpdump -i {self.interface} -w - -U -c 1"
|
||||||
|
if self.namespace:
|
||||||
|
base_cmd = f"ip netns exec {self.namespace} {base_cmd}"
|
||||||
|
|
||||||
|
cmds = [base_cmd, f"sudo {base_cmd}"]
|
||||||
|
|
||||||
|
printer.info(f"Verifying sudo requirement")
|
||||||
|
for cmd in cmds:
|
||||||
|
try:
|
||||||
|
self.node.child.sendline(cmd)
|
||||||
|
start_time = time.time()
|
||||||
|
while time.time() - start_time < 5:
|
||||||
|
try:
|
||||||
|
index = self.node.child.expect([
|
||||||
|
r'listening on',
|
||||||
|
r'permission denied',
|
||||||
|
r'cannot',
|
||||||
|
r'No such file or directory',
|
||||||
|
], timeout=1)
|
||||||
|
|
||||||
|
if index == 0:
|
||||||
|
self.node.child.send("\x03")
|
||||||
|
return "sudo" in cmd
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.node.child.send("\x03")
|
||||||
|
time.sleep(0.5)
|
||||||
|
try:
|
||||||
|
self.node.child.read_nonblocking(size=1024, timeout=0.5)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
printer.warning(f"Error during sudo detection: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
printer.error(f"Failed to run tcpdump on remote node '{self.node.unique}'")
|
||||||
|
sys.exit(4)
|
||||||
|
|
||||||
|
def _monitor_capture_output(self):
|
||||||
|
try:
|
||||||
|
index = self.node.child.expect([
|
||||||
|
r'Broken pipe',
|
||||||
|
r'packet[s]? captured'
|
||||||
|
], timeout=None)
|
||||||
|
if index == 0:
|
||||||
|
printer.error("Tcpdump failed: Broken pipe.")
|
||||||
|
else:
|
||||||
|
printer.success("Tcpdump finished capturing packets.")
|
||||||
|
|
||||||
|
self.listener_active = False
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _sendline_until_connected(self, cmd, retries=5, interval=2):
|
||||||
|
for attempt in range(1, retries + 1):
|
||||||
|
printer.info(f"Attempt {attempt}/{retries} to connect listener...")
|
||||||
|
self.node.child.sendline(cmd)
|
||||||
|
|
||||||
|
try:
|
||||||
|
index = self.node.child.expect([
|
||||||
|
r'listening on',
|
||||||
|
TIMEOUT,
|
||||||
|
r'permission',
|
||||||
|
r'not permitted',
|
||||||
|
r'invalid',
|
||||||
|
r'unrecognized',
|
||||||
|
r'Unable',
|
||||||
|
r'No such',
|
||||||
|
r'illegal',
|
||||||
|
r'syntax error'
|
||||||
|
], timeout=5)
|
||||||
|
|
||||||
|
if index == 0:
|
||||||
|
|
||||||
|
self.monitor_end = threading.Thread(target=self._monitor_capture_output)
|
||||||
|
self.monitor_end.daemon = True
|
||||||
|
self.monitor_end.start()
|
||||||
|
|
||||||
|
if self.listener_connected.wait(timeout=interval):
|
||||||
|
printer.success("Listener successfully received a connection.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
printer.warning("No connection yet. Retrying...")
|
||||||
|
|
||||||
|
elif index == 1:
|
||||||
|
error = f"tcpdump did not respond within the expected time.\n" \
|
||||||
|
f"Command used:\n{cmd}\n" \
|
||||||
|
f"→ Please verify the command syntax."
|
||||||
|
return f"{error}"
|
||||||
|
else:
|
||||||
|
before_last_line = self.node.child.before.decode().splitlines()[-1]
|
||||||
|
error = f"Tcpdump error detected:" \
|
||||||
|
f"{before_last_line}{self.node.child.after.decode()}{self.node.child.readline().decode()}".rstrip()
|
||||||
|
return f"{error}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
printer.warning(f"Unexpected error during tcpdump startup: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _build_tcpdump_command(self):
|
||||||
|
base = f"tcpdump -i {self.interface}"
|
||||||
|
if self.use_wireshark:
|
||||||
|
base += " -w - -U"
|
||||||
|
else:
|
||||||
|
base += " -l"
|
||||||
|
|
||||||
|
if self.namespace:
|
||||||
|
base = f"ip netns exec {self.namespace} {base}"
|
||||||
|
|
||||||
|
if self.requires_sudo:
|
||||||
|
base = f"sudo {base}"
|
||||||
|
|
||||||
|
if self.tcpdump_args:
|
||||||
|
base += " " + " ".join(self.tcpdump_args)
|
||||||
|
|
||||||
|
if self.tcpdump_filter:
|
||||||
|
base += " " + " ".join(self.tcpdump_filter)
|
||||||
|
|
||||||
|
base += f" | nc localhost {self.local_port}"
|
||||||
|
return base
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if self.use_wireshark:
|
||||||
|
if not self.wireshark_path:
|
||||||
|
printer.error("Wireshark path not set in config.\nUse '--set-wireshark-path /full/path/to/wireshark' to configure it.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
self.local_port = self._find_free_port()
|
||||||
|
self.node.options += f" -o ExitOnForwardFailure=yes -R {self.local_port}:localhost:{self.local_port}"
|
||||||
|
|
||||||
|
connection = self.node._connect()
|
||||||
|
if connection is not True:
|
||||||
|
printer.error(f"Could not connect to {self.node.unique}\n{connection}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
self.requires_sudo = self._detect_sudo_requirement()
|
||||||
|
tcpdump_cmd = self._build_tcpdump_command()
|
||||||
|
|
||||||
|
ws_proc = None
|
||||||
|
monitor_thread = None
|
||||||
|
|
||||||
|
if self.use_wireshark:
|
||||||
|
|
||||||
|
printer.info(f"Live capture from {self.node.unique}:{self.interface}, launching Wireshark...")
|
||||||
|
try:
|
||||||
|
ws_proc = subprocess.Popen(
|
||||||
|
[self.wireshark_path, "-k", "-i", "-"],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
printer.error(f"Failed to launch Wireshark: {e}\nMake sure the path is correct and Wireshark is installed.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
monitor_thread = threading.Thread(target=self._monitor_wireshark, args=(ws_proc,))
|
||||||
|
monitor_thread.daemon = True
|
||||||
|
monitor_thread.start()
|
||||||
|
else:
|
||||||
|
printer.info(f"Live text capture from {self.node.unique}:{self.interface}")
|
||||||
|
printer.info("Press Ctrl+C to stop.\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._start_local_listener(self.local_port, ws_proc=ws_proc)
|
||||||
|
time.sleep(1) # small delay before retry attempts
|
||||||
|
|
||||||
|
result = self._sendline_until_connected(tcpdump_cmd, retries=5, interval=2)
|
||||||
|
if result is not True:
|
||||||
|
if isinstance(result, str):
|
||||||
|
printer.error(f"{result}")
|
||||||
|
else:
|
||||||
|
printer.error("Listener connection failed after all retries.")
|
||||||
|
self.listener_active = False
|
||||||
|
return
|
||||||
|
|
||||||
|
while self.listener_active:
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("")
|
||||||
|
printer.warning("Capture interrupted by user.")
|
||||||
|
self.listener_active = False
|
||||||
|
finally:
|
||||||
|
if self.listener_conn:
|
||||||
|
try:
|
||||||
|
self.listener_conn.shutdown(socket.SHUT_RDWR)
|
||||||
|
self.listener_conn.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if hasattr(self.node, "child"):
|
||||||
|
self.node.child.close(force=True)
|
||||||
|
if self.listener_thread.is_alive():
|
||||||
|
self.listener_thread.join()
|
||||||
|
if monitor_thread and monitor_thread.is_alive():
|
||||||
|
monitor_thread.join()
|
||||||
|
|
||||||
|
|
||||||
|
class Parser:
|
||||||
|
def __init__(self):
|
||||||
|
self.parser = argparse.ArgumentParser(description="Capture packets remotely using a saved SSH node", epilog="All unknown arguments will be passed to tcpdump.")
|
||||||
|
|
||||||
|
self.parser.add_argument("node", nargs='?', help="Name of the saved node (must use SSH)")
|
||||||
|
self.parser.add_argument("interface", nargs='?', help="Network interface to capture on")
|
||||||
|
self.parser.add_argument("--ns", "--namespace", dest="namespace", help="Optional network namespace")
|
||||||
|
self.parser.add_argument("-w","--wireshark", action="store_true", help="Open live capture in Wireshark")
|
||||||
|
self.parser.add_argument("--set-wireshark-path", metavar="PATH", help="Set the default path to Wireshark binary")
|
||||||
|
self.parser.add_argument(
|
||||||
|
"-f", "--filter",
|
||||||
|
dest="tcpdump_filter",
|
||||||
|
metavar="ARG",
|
||||||
|
nargs="*",
|
||||||
|
default=["not", "port", "22"],
|
||||||
|
help="tcpdump filter expression (e.g., -f port 443 and udp). Default: not port 22"
|
||||||
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
"--unknown-args",
|
||||||
|
action="store_true",
|
||||||
|
default=True,
|
||||||
|
help=argparse.SUPPRESS
|
||||||
|
)
|
||||||
|
|
||||||
|
class Entrypoint:
|
||||||
|
def __init__(self, args, parser, connapp):
|
||||||
|
if "--" in args.unknown_args:
|
||||||
|
args.unknown_args.remove("--")
|
||||||
|
if args.set_wireshark_path:
|
||||||
|
connapp._change_settings("wireshark_path", args.set_wireshark_path)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not args.node or not args.interface:
|
||||||
|
parser.error("node and interface are required unless --set-wireshark-path is used")
|
||||||
|
|
||||||
|
capture = RemoteCapture(
|
||||||
|
connapp=connapp,
|
||||||
|
node_name=args.node,
|
||||||
|
interface=args.interface,
|
||||||
|
namespace=args.namespace,
|
||||||
|
use_wireshark=args.wireshark,
|
||||||
|
tcpdump_filter=args.tcpdump_filter,
|
||||||
|
tcpdump_args=args.unknown_args
|
||||||
|
)
|
||||||
|
capture.run()
|
||||||
|
|
||||||
|
def _connpy_completion(wordsnumber, words, info = None):
|
||||||
|
if wordsnumber == 3:
|
||||||
|
result = ["--help", "--set-wireshark-path"]
|
||||||
|
result.extend(info["nodes"])
|
||||||
|
elif wordsnumber == 5 and words[1] in info["nodes"]:
|
||||||
|
result = ['--wireshark', '--namespace', '--filter', '--help']
|
||||||
|
elif wordsnumber == 6 and words[3] in ["-w", "--wireshark"]:
|
||||||
|
result = ['--namespace', '--filter', '--help']
|
||||||
|
elif wordsnumber == 7 and words[3] in ["-n", "--namespace"]:
|
||||||
|
result = ['--wireshark', '--filter', '--help']
|
||||||
|
elif wordsnumber == 8:
|
||||||
|
if any(w in words for w in ["-w", "--wireshark"]) and any(w in words for w in ["-n", "--namespace"]):
|
||||||
|
result = ['--filter', '--help']
|
||||||
|
else:
|
||||||
|
result = []
|
||||||
|
|
||||||
|
return result
|
@@ -1,6 +1,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import yaml
|
import yaml
|
||||||
import re
|
import re
|
||||||
|
from connpy import printer
|
||||||
|
|
||||||
|
|
||||||
class context_manager:
|
class context_manager:
|
||||||
@@ -14,10 +15,10 @@ class context_manager:
|
|||||||
|
|
||||||
def add_context(self, context, regex):
|
def add_context(self, context, regex):
|
||||||
if not context.isalnum():
|
if not context.isalnum():
|
||||||
print("Context name has to be alphanumeric.")
|
printer.error("Context name has to be alphanumeric.")
|
||||||
exit(1)
|
exit(1)
|
||||||
elif context in self.contexts:
|
elif context in self.contexts:
|
||||||
print(f"Context {context} already exists.")
|
printer.error(f"Context {context} already exists.")
|
||||||
exit(2)
|
exit(2)
|
||||||
else:
|
else:
|
||||||
self.contexts[context] = regex
|
self.contexts[context] = regex
|
||||||
@@ -25,10 +26,10 @@ class context_manager:
|
|||||||
|
|
||||||
def modify_context(self, context, regex):
|
def modify_context(self, context, regex):
|
||||||
if context == "all":
|
if context == "all":
|
||||||
print("Can't modify default context: all")
|
printer.error("Can't modify default context: all")
|
||||||
exit(3)
|
exit(3)
|
||||||
elif context not in self.contexts:
|
elif context not in self.contexts:
|
||||||
print(f"Context {context} doesn't exist.")
|
printer.error(f"Context {context} doesn't exist.")
|
||||||
exit(4)
|
exit(4)
|
||||||
else:
|
else:
|
||||||
self.contexts[context] = regex
|
self.contexts[context] = regex
|
||||||
@@ -36,13 +37,13 @@ class context_manager:
|
|||||||
|
|
||||||
def delete_context(self, context):
|
def delete_context(self, context):
|
||||||
if context == "all":
|
if context == "all":
|
||||||
print("Can't delete default context: all")
|
printer.error("Can't delete default context: all")
|
||||||
exit(3)
|
exit(3)
|
||||||
elif context not in self.contexts:
|
elif context not in self.contexts:
|
||||||
print(f"Context {context} doesn't exist.")
|
printer.error(f"Context {context} doesn't exist.")
|
||||||
exit(4)
|
exit(4)
|
||||||
if context == self.current_context:
|
if context == self.current_context:
|
||||||
print(f"Can't delete current context: {self.current_context}")
|
printer.error(f"Can't delete current context: {self.current_context}")
|
||||||
exit(5)
|
exit(5)
|
||||||
else:
|
else:
|
||||||
self.contexts.pop(context)
|
self.contexts.pop(context)
|
||||||
@@ -51,26 +52,27 @@ class context_manager:
|
|||||||
def list_contexts(self):
|
def list_contexts(self):
|
||||||
for key in self.contexts.keys():
|
for key in self.contexts.keys():
|
||||||
if key == self.current_context:
|
if key == self.current_context:
|
||||||
print(f"{key} * (active)")
|
printer.success(f"{key} (active)")
|
||||||
else:
|
else:
|
||||||
print(key)
|
printer.custom(" ",key)
|
||||||
|
|
||||||
def set_context(self, context):
|
def set_context(self, context):
|
||||||
if context not in self.contexts:
|
if context not in self.contexts:
|
||||||
print(f"Context {context} doesn't exist.")
|
printer.error(f"Context {context} doesn't exist.")
|
||||||
exit(4)
|
exit(4)
|
||||||
elif context == self.current_context:
|
elif context == self.current_context:
|
||||||
print(f"Context {context} already set")
|
printer.info(f"Context {context} already set")
|
||||||
exit(0)
|
exit(0)
|
||||||
else:
|
else:
|
||||||
self.connapp._change_settings("current_context", context)
|
self.connapp._change_settings("current_context", context)
|
||||||
|
|
||||||
def show_context(self, context):
|
def show_context(self, context):
|
||||||
if context not in self.contexts:
|
if context not in self.contexts:
|
||||||
print(f"Context {context} doesn't exist.")
|
printer.error(f"Context {context} doesn't exist.")
|
||||||
exit(4)
|
exit(4)
|
||||||
else:
|
else:
|
||||||
yaml_output = yaml.dump(self.contexts[context], sort_keys=False, default_flow_style=False)
|
yaml_output = yaml.dump(self.contexts[context], sort_keys=False, default_flow_style=False)
|
||||||
|
printer.custom(context,"")
|
||||||
print(yaml_output)
|
print(yaml_output)
|
||||||
|
|
||||||
|
|
||||||
@@ -113,18 +115,17 @@ class Preload:
|
|||||||
class Parser:
|
class Parser:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.parser = argparse.ArgumentParser(description="Manage contexts with regex matching", formatter_class=argparse.RawTextHelpFormatter)
|
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
|
# Define the context name as a positional argument
|
||||||
self.parser.add_argument("context_name", help="Name of the context", nargs='?')
|
self.parser.add_argument("context_name", help="Name of the context", nargs='?')
|
||||||
|
|
||||||
group = self.parser.add_mutually_exclusive_group(required=True)
|
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("-a", "--add", nargs='+', help='Add a new context with regex values.\nUsage: 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("-r", "--rm", "--del", action='store_true', help="Delete a context.\nUsage: context -d name")
|
||||||
group.add_argument("--ls", action='store_true', help="List all contexts. Usage: context --list")
|
group.add_argument("--ls", action='store_true', help="List all contexts.\nUsage: context --ls")
|
||||||
group.add_argument("--set", action='store_true', help="Set the used context. Usage: context --set name")
|
group.add_argument("--set", action='store_true', help="Set the used context.\nUsage: 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("-s", "--show", action='store_true', help="Show the defined regex of a context.\nUsage: context --show name")
|
||||||
group.add_argument("-e", "--edit", "--mod", nargs='+', help='Modify an existing context. Usage: context --mod name "regex1" "regex2"')
|
group.add_argument("-e", "--edit", "--mod", nargs='+', help='Modify an existing context.\nUsage: context --mod name "regex1" "regex2"')
|
||||||
|
|
||||||
class Entrypoint:
|
class Entrypoint:
|
||||||
def __init__(self, args, parser, connapp):
|
def __init__(self, args, parser, connapp):
|
||||||
|
@@ -7,6 +7,7 @@ import tempfile
|
|||||||
import io
|
import io
|
||||||
import yaml
|
import yaml
|
||||||
import threading
|
import threading
|
||||||
|
from connpy import printer
|
||||||
from google.oauth2.credentials import Credentials
|
from google.oauth2.credentials import Credentials
|
||||||
from google.auth.transport.requests import Request
|
from google.auth.transport.requests import Request
|
||||||
from googleapiclient.discovery import build
|
from googleapiclient.discovery import build
|
||||||
@@ -50,33 +51,33 @@ class sync:
|
|||||||
with open(self.token_file, 'w') as token:
|
with open(self.token_file, 'w') as token:
|
||||||
token.write(creds.to_json())
|
token.write(creds.to_json())
|
||||||
|
|
||||||
print("Logged in successfully.")
|
printer.success("Logged in successfully.")
|
||||||
|
|
||||||
except RefreshError as e:
|
except RefreshError as e:
|
||||||
# If refresh fails, delete the invalid token file and start a new login flow
|
# If refresh fails, delete the invalid token file and start a new login flow
|
||||||
if os.path.exists(self.token_file):
|
if os.path.exists(self.token_file):
|
||||||
os.remove(self.token_file)
|
os.remove(self.token_file)
|
||||||
print("Existing token was invalid and has been removed. Please log in again.")
|
printer.warning("Existing token was invalid and has been removed. Please log in again.")
|
||||||
flow = InstalledAppFlow.from_client_secrets_file(
|
flow = InstalledAppFlow.from_client_secrets_file(
|
||||||
self.google_client, self.scopes)
|
self.google_client, self.scopes)
|
||||||
creds = flow.run_local_server(port=0, access_type='offline')
|
creds = flow.run_local_server(port=0, access_type='offline')
|
||||||
with open(self.token_file, 'w') as token:
|
with open(self.token_file, 'w') as token:
|
||||||
token.write(creds.to_json())
|
token.write(creds.to_json())
|
||||||
print("Logged in successfully after re-authentication.")
|
printer.success("Logged in successfully after re-authentication.")
|
||||||
|
|
||||||
def logout(self):
|
def logout(self):
|
||||||
if os.path.exists(self.token_file):
|
if os.path.exists(self.token_file):
|
||||||
os.remove(self.token_file)
|
os.remove(self.token_file)
|
||||||
print("Logged out successfully.")
|
printer.success("Logged out successfully.")
|
||||||
else:
|
else:
|
||||||
print("No credentials file found. Already logged out.")
|
printer.info("No credentials file found. Already logged out.")
|
||||||
|
|
||||||
def get_credentials(self):
|
def get_credentials(self):
|
||||||
# Load credentials from token.json
|
# Load credentials from token.json
|
||||||
if os.path.exists(self.token_file):
|
if os.path.exists(self.token_file):
|
||||||
creds = Credentials.from_authorized_user_file(self.token_file, self.scopes)
|
creds = Credentials.from_authorized_user_file(self.token_file, self.scopes)
|
||||||
else:
|
else:
|
||||||
print("Credentials file not found.")
|
printer.error("Credentials file not found.")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# If there are no valid credentials available, ask the user to log in again
|
# If there are no valid credentials available, ask the user to log in again
|
||||||
@@ -85,10 +86,10 @@ class sync:
|
|||||||
try:
|
try:
|
||||||
creds.refresh(Request())
|
creds.refresh(Request())
|
||||||
except RefreshError:
|
except RefreshError:
|
||||||
print("Could not refresh access token. Please log in again.")
|
printer.warning("Could not refresh access token. Please log in again.")
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
print("Credentials are missing or invalid. Please log in.")
|
printer.warning("Credentials are missing or invalid. Please log in.")
|
||||||
return 0
|
return 0
|
||||||
return creds
|
return creds
|
||||||
|
|
||||||
@@ -114,8 +115,8 @@ class sync:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def status(self):
|
def status(self):
|
||||||
print(f"Login: {self.check_login_status()}")
|
printer.info(f"Login: {self.check_login_status()}")
|
||||||
print(f"Sync: {self.sync}")
|
printer.info(f"Sync: {self.sync}")
|
||||||
|
|
||||||
|
|
||||||
def get_appdata_files(self):
|
def get_appdata_files(self):
|
||||||
@@ -151,17 +152,18 @@ class sync:
|
|||||||
return files_info
|
return files_info
|
||||||
|
|
||||||
except HttpError as error:
|
except HttpError as error:
|
||||||
print(f"An error occurred: {error}")
|
printer.error(f"An error occurred: {error}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def dump_appdata_files_yaml(self):
|
def dump_appdata_files_yaml(self):
|
||||||
files_info = self.get_appdata_files()
|
files_info = self.get_appdata_files()
|
||||||
if not files_info:
|
if not files_info:
|
||||||
print("Failed to retrieve files or no files found.")
|
printer.error("Failed to retrieve files or no files found.")
|
||||||
return
|
return
|
||||||
# Pretty print as YAML
|
# Pretty print as YAML
|
||||||
yaml_output = yaml.dump(files_info, sort_keys=False, default_flow_style=False)
|
yaml_output = yaml.dump(files_info, sort_keys=False, default_flow_style=False)
|
||||||
|
printer.custom("backups","")
|
||||||
print(yaml_output)
|
print(yaml_output)
|
||||||
|
|
||||||
|
|
||||||
@@ -233,16 +235,16 @@ class sync:
|
|||||||
oldest_file = min(app_data_files, key=lambda x: x['timestamp'])
|
oldest_file = min(app_data_files, key=lambda x: x['timestamp'])
|
||||||
delete_old = self.delete_file_by_id(oldest_file['id'])
|
delete_old = self.delete_file_by_id(oldest_file['id'])
|
||||||
if delete_old:
|
if delete_old:
|
||||||
print(delete_old)
|
printer.error(delete_old)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# Upload the new file
|
# Upload the new file
|
||||||
upload_new = self.backup_file_to_drive(zip_path, timestamp)
|
upload_new = self.backup_file_to_drive(zip_path, timestamp)
|
||||||
if upload_new:
|
if upload_new:
|
||||||
print(upload_new)
|
printer.error(upload_new)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
print("Backup to google uploaded successfully.")
|
printer.success("Backup to google uploaded successfully.")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def decompress_zip(self, zip_path):
|
def decompress_zip(self, zip_path):
|
||||||
@@ -253,7 +255,7 @@ class sync:
|
|||||||
zipf.extract(".osk", os.path.dirname(self.key))
|
zipf.extract(".osk", os.path.dirname(self.key))
|
||||||
return 0
|
return 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"An error occurred: {e}")
|
printer.error(f"An error occurred: {e}")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def download_file_by_id(self, file_id, destination_path):
|
def download_file_by_id(self, file_id, destination_path):
|
||||||
@@ -282,14 +284,14 @@ class sync:
|
|||||||
# Get the files in the app data folder
|
# Get the files in the app data folder
|
||||||
app_data_files = self.get_appdata_files()
|
app_data_files = self.get_appdata_files()
|
||||||
if not app_data_files:
|
if not app_data_files:
|
||||||
print("No files found in app data folder.")
|
printer.error("No files found in app data folder.")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# Check if a specific file_id was provided and if it exists in the list
|
# Check if a specific file_id was provided and if it exists in the list
|
||||||
if file_id:
|
if file_id:
|
||||||
selected_file = next((f for f in app_data_files if f['id'] == file_id), None)
|
selected_file = next((f for f in app_data_files if f['id'] == file_id), None)
|
||||||
if not selected_file:
|
if not selected_file:
|
||||||
print(f"No file found with ID: {file_id}")
|
printer.error(f"No file found with ID: {file_id}")
|
||||||
return 1
|
return 1
|
||||||
else:
|
else:
|
||||||
# Find the latest file based on timestamp
|
# Find the latest file based on timestamp
|
||||||
@@ -302,10 +304,10 @@ class sync:
|
|||||||
|
|
||||||
# Unzip the downloaded file to the destination folder
|
# Unzip the downloaded file to the destination folder
|
||||||
if self.decompress_zip(temp_download_path):
|
if self.decompress_zip(temp_download_path):
|
||||||
print("Failed to decompress the file.")
|
printer.error("Failed to decompress the file.")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
print(f"Backup from Google Drive restored successfully: {selected_file['name']}")
|
printer.success(f"Backup from Google Drive restored successfully: {selected_file['name']}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def config_listener_post(self, args, kwargs):
|
def config_listener_post(self, args, kwargs):
|
||||||
@@ -314,7 +316,7 @@ class sync:
|
|||||||
if not kwargs["result"]:
|
if not kwargs["result"]:
|
||||||
self.compress_and_upload()
|
self.compress_and_upload()
|
||||||
else:
|
else:
|
||||||
print("Sync cannot be performed. Please check your login status.")
|
printer.warning("Sync cannot be performed. Please check your login status.")
|
||||||
return kwargs["result"]
|
return kwargs["result"]
|
||||||
|
|
||||||
def config_listener_pre(self, *args, **kwargs):
|
def config_listener_pre(self, *args, **kwargs):
|
||||||
@@ -337,7 +339,6 @@ class Preload:
|
|||||||
class Parser:
|
class Parser:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.parser = argparse.ArgumentParser(description="Sync config with Google")
|
self.parser = argparse.ArgumentParser(description="Sync config with Google")
|
||||||
self.description = "Sync config with Google"
|
|
||||||
subparsers = self.parser.add_subparsers(title="Commands", dest='command',metavar="")
|
subparsers = self.parser.add_subparsers(title="Commands", dest='command',metavar="")
|
||||||
login_parser = subparsers.add_parser("login", help="Login to Google to enable synchronization")
|
login_parser = subparsers.add_parser("login", help="Login to Google to enable synchronization")
|
||||||
logout_parser = subparsers.add_parser("logout", help="Logout from Google")
|
logout_parser = subparsers.add_parser("logout", help="Logout from Google")
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
#Imports
|
#Imports
|
||||||
from functools import wraps, partial, update_wrapper
|
from functools import wraps, partial, update_wrapper
|
||||||
|
from . import printer
|
||||||
|
|
||||||
#functions and classes
|
#functions and classes
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ class MethodHook:
|
|||||||
try:
|
try:
|
||||||
args, kwargs = hook(*args, **kwargs)
|
args, kwargs = hook(*args, **kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"{self.func.__name__} Pre-hook {hook.__name__} raised an exception: {e}")
|
printer.error(f"{self.func.__name__} Pre-hook {hook.__name__} raised an exception: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.func(*args, **kwargs)
|
result = self.func(*args, **kwargs)
|
||||||
@@ -30,7 +31,7 @@ class MethodHook:
|
|||||||
try:
|
try:
|
||||||
result = hook(*args, **kwargs, result=result) # Pass result to hooks
|
result = hook(*args, **kwargs, result=result) # Pass result to hooks
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"{self.func.__name__} Post-hook {hook.__name__} raised an exception: {e}")
|
printer.error(f"{self.func.__name__} Post-hook {hook.__name__} raised an exception: {e}")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@ import importlib.util
|
|||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
from connpy import printer
|
||||||
|
|
||||||
class Plugins:
|
class Plugins:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -30,8 +31,7 @@ class Plugins:
|
|||||||
### Verifications:
|
### Verifications:
|
||||||
- The presence of only allowed top-level elements.
|
- The presence of only allowed top-level elements.
|
||||||
- The existence of two specific classes: 'Parser' and 'Entrypoint'. and/or specific class: Preload.
|
- The existence of two specific classes: 'Parser' and 'Entrypoint'. and/or specific class: Preload.
|
||||||
- 'Parser' class must only have an '__init__' method and must assign 'self.parser'
|
- 'Parser' class must only have an '__init__' method and must assign 'self.parser'.
|
||||||
and 'self.description'.
|
|
||||||
- 'Entrypoint' class must have an '__init__' method accepting specific arguments.
|
- 'Entrypoint' class must have an '__init__' method accepting specific arguments.
|
||||||
|
|
||||||
If any of these checks fail, the function returns an error message indicating
|
If any of these checks fail, the function returns an error message indicating
|
||||||
@@ -77,11 +77,12 @@ class Plugins:
|
|||||||
if not all(isinstance(method, ast.FunctionDef) and method.name == '__init__' for method in node.body):
|
if not all(isinstance(method, ast.FunctionDef) and method.name == '__init__' for method in node.body):
|
||||||
return "Parser class should only have __init__ method"
|
return "Parser class should only have __init__ method"
|
||||||
|
|
||||||
# Check if 'self.parser' and 'self.description' are assigned in __init__ method
|
# Check if 'self.parser' is assigned in __init__ method
|
||||||
init_method = node.body[0]
|
init_method = node.body[0]
|
||||||
assigned_attrs = [target.attr for expr in init_method.body if isinstance(expr, ast.Assign) for target in expr.targets if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name) and target.value.id == 'self']
|
assigned_attrs = [target.attr for expr in init_method.body if isinstance(expr, ast.Assign) for target in expr.targets if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name) and target.value.id == 'self']
|
||||||
if 'parser' not in assigned_attrs or 'description' not in assigned_attrs:
|
if 'parser' not in assigned_attrs:
|
||||||
return "Parser class should set self.parser and self.description" # 'self.parser' or 'self.description' not assigned in __init__
|
return "Parser class should set self.parser"
|
||||||
|
|
||||||
|
|
||||||
elif node.name == 'Entrypoint':
|
elif node.name == 'Entrypoint':
|
||||||
has_entrypoint = True
|
has_entrypoint = True
|
||||||
@@ -124,13 +125,14 @@ class Plugins:
|
|||||||
filepath = os.path.join(directory, filename)
|
filepath = os.path.join(directory, filename)
|
||||||
check_file = self.verify_script(filepath)
|
check_file = self.verify_script(filepath)
|
||||||
if check_file:
|
if check_file:
|
||||||
print(f"Failed to load plugin: {filename}. Reason: {check_file}")
|
printer.error(f"Failed to load plugin: {filename}. Reason: {check_file}")
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
self.plugins[root_filename] = self._import_from_path(filepath)
|
self.plugins[root_filename] = self._import_from_path(filepath)
|
||||||
if hasattr(self.plugins[root_filename], "Parser"):
|
if hasattr(self.plugins[root_filename], "Parser"):
|
||||||
self.plugin_parsers[root_filename] = self.plugins[root_filename].Parser()
|
self.plugin_parsers[root_filename] = self.plugins[root_filename].Parser()
|
||||||
subparsers.add_parser(root_filename, parents=[self.plugin_parsers[root_filename].parser], add_help=False, description=self.plugin_parsers[root_filename].description)
|
plugin = self.plugin_parsers[root_filename]
|
||||||
|
subparsers.add_parser(root_filename, parents=[self.plugin_parsers[root_filename].parser], add_help=False, usage=plugin.parser.usage, description=plugin.parser.description, epilog=plugin.parser.epilog, formatter_class=plugin.parser.formatter_class)
|
||||||
if hasattr(self.plugins[root_filename], "Preload"):
|
if hasattr(self.plugins[root_filename], "Preload"):
|
||||||
self.preloads[root_filename] = self.plugins[root_filename]
|
self.preloads[root_filename] = self.plugins[root_filename]
|
||||||
|
|
||||||
|
33
connpy/printer.py
Normal file
33
connpy/printer.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
def _format_multiline(tag, message):
|
||||||
|
lines = message.splitlines()
|
||||||
|
if not lines:
|
||||||
|
return f"[{tag}]"
|
||||||
|
formatted = [f"[{tag}] {lines[0]}"]
|
||||||
|
indent = " " * (len(tag) + 3)
|
||||||
|
for line in lines[1:]:
|
||||||
|
formatted.append(f"{indent}{line}")
|
||||||
|
return "\n".join(formatted)
|
||||||
|
|
||||||
|
def info(message):
|
||||||
|
print(_format_multiline("i", message))
|
||||||
|
|
||||||
|
def success(message):
|
||||||
|
print(_format_multiline("✓", message))
|
||||||
|
|
||||||
|
def start(message):
|
||||||
|
print(_format_multiline("+", message))
|
||||||
|
|
||||||
|
def warning(message):
|
||||||
|
print(_format_multiline("!", message))
|
||||||
|
|
||||||
|
def error(message):
|
||||||
|
print(_format_multiline("✗", message), file=sys.stderr)
|
||||||
|
|
||||||
|
def debug(message):
|
||||||
|
print(_format_multiline("d", message))
|
||||||
|
|
||||||
|
def custom(tag, message):
|
||||||
|
print(_format_multiline(tag, message))
|
||||||
|
|
@@ -143,9 +143,8 @@ options:
|
|||||||
<li><strong>Purpose</strong>: Handles parsing of command-line arguments.</li>
|
<li><strong>Purpose</strong>: Handles parsing of command-line arguments.</li>
|
||||||
<li><strong>Requirements</strong>:</li>
|
<li><strong>Requirements</strong>:</li>
|
||||||
<li>Must contain only one method: <code>__init__</code>.</li>
|
<li>Must contain only one method: <code>__init__</code>.</li>
|
||||||
<li>The <code>__init__</code> method must initialize at least two attributes:<ul>
|
<li>The <code>__init__</code> method must initialize at least one attribute:<ul>
|
||||||
<li><code>self.parser</code>: An instance of <code>argparse.ArgumentParser</code>.</li>
|
<li><code>self.parser</code>: An instance of <code>argparse.ArgumentParser</code>.</li>
|
||||||
<li><code>self.description</code>: A string containing the description of the parser.</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -270,6 +269,89 @@ connapp.ai.some_method.register_pre_hook(pre_processing_hook)</p>
|
|||||||
<li><code>if __name__ == "__main__":</code></li>
|
<li><code>if __name__ == "__main__":</code></li>
|
||||||
<li>This block allows the plugin to be run as a standalone script for testing or independent use.</li>
|
<li>This block allows the plugin to be run as a standalone script for testing or independent use.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<h3 id="command-completion-support">Command Completion Support</h3>
|
||||||
|
<p>Plugins can provide intelligent <strong>tab completion</strong> by defining a function called <code>_connpy_completion</code> in the plugin script. This function will be called by Connpy to assist with command-line completion when the user types partial input.</p>
|
||||||
|
<h4 id="function-signature">Function Signature</h4>
|
||||||
|
<pre><code>def _connpy_completion(wordsnumber, words, info=None):
|
||||||
|
...
|
||||||
|
</code></pre>
|
||||||
|
<h4 id="parameters">Parameters</h4>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Parameter</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>wordsnumber</code></td>
|
||||||
|
<td>Integer indicating the number of words (space-separated tokens) currently on the command line. For plugins, this typically starts at 3 (e.g., <code>connpy <plugin> ...</code>).</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>words</code></td>
|
||||||
|
<td>A list of tokens (words) already typed. <code>words[0]</code> is always the name of the plugin, followed by any subcommands or arguments.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>info</code></td>
|
||||||
|
<td>A dictionary of structured context data provided by Connpy to help with suggestions.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h4 id="contents-of-info">Contents of <code>info</code></h4>
|
||||||
|
<p>The <code>info</code> dictionary contains helpful context to generate completions:</p>
|
||||||
|
<pre><code>info = {
|
||||||
|
"config": config_dict, # The full loaded configuration
|
||||||
|
"nodes": node_list, # List of all known node names
|
||||||
|
"folders": folder_list, # List of all defined folder names
|
||||||
|
"profiles": profile_list, # List of all profile names
|
||||||
|
"plugins": plugin_list # List of all plugin names
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
<p>You can use this data to generate suggestions based on the current input.</p>
|
||||||
|
<h4 id="return-value">Return Value</h4>
|
||||||
|
<p>The function must return a list of suggestion strings to be presented to the user.</p>
|
||||||
|
<h4 id="example">Example</h4>
|
||||||
|
<pre><code>def _connpy_completion(wordsnumber, words, info=None):
|
||||||
|
if wordsnumber == 3:
|
||||||
|
return ["--help", "--verbose", "start", "stop"]
|
||||||
|
|
||||||
|
elif wordsnumber == 4 and words[2] == "start":
|
||||||
|
return info["nodes"] # Suggest node names
|
||||||
|
|
||||||
|
return []
|
||||||
|
</code></pre>
|
||||||
|
<blockquote>
|
||||||
|
<p>In this example, if the user types <code><a title="connpy" href="#connpy">connpy</a> myplugin start </code> and presses Tab, it will suggest node names.</p>
|
||||||
|
</blockquote>
|
||||||
|
<h3 id="handling-unknown-arguments">Handling Unknown Arguments</h3>
|
||||||
|
<p>Plugins can choose to accept and process unknown arguments that are <strong>not explicitly defined</strong> in the parser. To enable this behavior, the plugin must define the following hidden argument in its <code>Parser</code> class:</p>
|
||||||
|
<pre><code>self.parser.add_argument(
|
||||||
|
"--unknown-args",
|
||||||
|
action="store_true",
|
||||||
|
default=True,
|
||||||
|
help=argparse.SUPPRESS
|
||||||
|
)
|
||||||
|
</code></pre>
|
||||||
|
<h4 id="behavior">Behavior:</h4>
|
||||||
|
<ul>
|
||||||
|
<li>When this argument is present, Connpy will parse the known arguments and capture any extra (unknown) ones.</li>
|
||||||
|
<li>These unknown arguments will be passed to the plugin as <code>args.unknown_args</code> inside the <code>Entrypoint</code>.</li>
|
||||||
|
<li>If the user does not pass any unknown arguments, <code>args.unknown_args</code> will contain the default value (<code>True</code>, unless overridden).</li>
|
||||||
|
</ul>
|
||||||
|
<h4 id="example_1">Example:</h4>
|
||||||
|
<p>If a plugin accepts unknown tcpdump flags like this:</p>
|
||||||
|
<pre><code>connpy myplugin -nn -s0
|
||||||
|
</code></pre>
|
||||||
|
<p>And defines the hidden <code>--unknown-args</code> flag as shown above, then:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>args.unknown_args</code> inside <code>Entrypoint.__init__()</code> will be: <code>['-nn', '-s0']</code></li>
|
||||||
|
</ul>
|
||||||
|
<blockquote>
|
||||||
|
<p>This allows the plugin to receive and process arguments intended for external tools (e.g., <code>tcpdump</code>) without argparse raising an error.</p>
|
||||||
|
</blockquote>
|
||||||
|
<h4 id="note">Note:</h4>
|
||||||
|
<p>If a plugin does <strong>not</strong> define <code>--unknown-args</code>, any extra arguments passed will cause argparse to fail with an unrecognized arguments error.</p>
|
||||||
<h3 id="script-verification">Script Verification</h3>
|
<h3 id="script-verification">Script Verification</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>The <code>verify_script</code> method in <code>plugins.py</code> is used to check the plugin script's compliance with these standards.</li>
|
<li>The <code>verify_script</code> method in <code>plugins.py</code> is used to check the plugin script's compliance with these standards.</li>
|
||||||
@@ -481,8 +563,7 @@ print(result)
|
|||||||
### Verifications:
|
### Verifications:
|
||||||
- The presence of only allowed top-level elements.
|
- The presence of only allowed top-level elements.
|
||||||
- The existence of two specific classes: 'Parser' and 'Entrypoint'. and/or specific class: Preload.
|
- The existence of two specific classes: 'Parser' and 'Entrypoint'. and/or specific class: Preload.
|
||||||
- 'Parser' class must only have an '__init__' method and must assign 'self.parser'
|
- 'Parser' class must only have an '__init__' method and must assign 'self.parser'.
|
||||||
and 'self.description'.
|
|
||||||
- 'Entrypoint' class must have an '__init__' method accepting specific arguments.
|
- 'Entrypoint' class must have an '__init__' method accepting specific arguments.
|
||||||
|
|
||||||
If any of these checks fail, the function returns an error message indicating
|
If any of these checks fail, the function returns an error message indicating
|
||||||
@@ -528,11 +609,12 @@ print(result)
|
|||||||
if not all(isinstance(method, ast.FunctionDef) and method.name == '__init__' for method in node.body):
|
if not all(isinstance(method, ast.FunctionDef) and method.name == '__init__' for method in node.body):
|
||||||
return "Parser class should only have __init__ method"
|
return "Parser class should only have __init__ method"
|
||||||
|
|
||||||
# Check if 'self.parser' and 'self.description' are assigned in __init__ method
|
# Check if 'self.parser' is assigned in __init__ method
|
||||||
init_method = node.body[0]
|
init_method = node.body[0]
|
||||||
assigned_attrs = [target.attr for expr in init_method.body if isinstance(expr, ast.Assign) for target in expr.targets if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name) and target.value.id == 'self']
|
assigned_attrs = [target.attr for expr in init_method.body if isinstance(expr, ast.Assign) for target in expr.targets if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name) and target.value.id == 'self']
|
||||||
if 'parser' not in assigned_attrs or 'description' not in assigned_attrs:
|
if 'parser' not in assigned_attrs:
|
||||||
return "Parser class should set self.parser and self.description" # 'self.parser' or 'self.description' not assigned in __init__
|
return "Parser class should set self.parser"
|
||||||
|
|
||||||
|
|
||||||
elif node.name == 'Entrypoint':
|
elif node.name == 'Entrypoint':
|
||||||
has_entrypoint = True
|
has_entrypoint = True
|
||||||
@@ -575,13 +657,14 @@ print(result)
|
|||||||
filepath = os.path.join(directory, filename)
|
filepath = os.path.join(directory, filename)
|
||||||
check_file = self.verify_script(filepath)
|
check_file = self.verify_script(filepath)
|
||||||
if check_file:
|
if check_file:
|
||||||
print(f"Failed to load plugin: {filename}. Reason: {check_file}")
|
printer.error(f"Failed to load plugin: {filename}. Reason: {check_file}")
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
self.plugins[root_filename] = self._import_from_path(filepath)
|
self.plugins[root_filename] = self._import_from_path(filepath)
|
||||||
if hasattr(self.plugins[root_filename], "Parser"):
|
if hasattr(self.plugins[root_filename], "Parser"):
|
||||||
self.plugin_parsers[root_filename] = self.plugins[root_filename].Parser()
|
self.plugin_parsers[root_filename] = self.plugins[root_filename].Parser()
|
||||||
subparsers.add_parser(root_filename, parents=[self.plugin_parsers[root_filename].parser], add_help=False, description=self.plugin_parsers[root_filename].description)
|
plugin = self.plugin_parsers[root_filename]
|
||||||
|
subparsers.add_parser(root_filename, parents=[self.plugin_parsers[root_filename].parser], add_help=False, usage=plugin.parser.usage, description=plugin.parser.description, epilog=plugin.parser.epilog, formatter_class=plugin.parser.formatter_class)
|
||||||
if hasattr(self.plugins[root_filename], "Preload"):
|
if hasattr(self.plugins[root_filename], "Preload"):
|
||||||
self.preloads[root_filename] = self.plugins[root_filename]</code></pre>
|
self.preloads[root_filename] = self.plugins[root_filename]</code></pre>
|
||||||
</details>
|
</details>
|
||||||
@@ -615,8 +698,7 @@ print(result)
|
|||||||
### Verifications:
|
### Verifications:
|
||||||
- The presence of only allowed top-level elements.
|
- The presence of only allowed top-level elements.
|
||||||
- The existence of two specific classes: 'Parser' and 'Entrypoint'. and/or specific class: Preload.
|
- The existence of two specific classes: 'Parser' and 'Entrypoint'. and/or specific class: Preload.
|
||||||
- 'Parser' class must only have an '__init__' method and must assign 'self.parser'
|
- 'Parser' class must only have an '__init__' method and must assign 'self.parser'.
|
||||||
and 'self.description'.
|
|
||||||
- 'Entrypoint' class must have an '__init__' method accepting specific arguments.
|
- 'Entrypoint' class must have an '__init__' method accepting specific arguments.
|
||||||
|
|
||||||
If any of these checks fail, the function returns an error message indicating
|
If any of these checks fail, the function returns an error message indicating
|
||||||
@@ -662,11 +744,12 @@ print(result)
|
|||||||
if not all(isinstance(method, ast.FunctionDef) and method.name == '__init__' for method in node.body):
|
if not all(isinstance(method, ast.FunctionDef) and method.name == '__init__' for method in node.body):
|
||||||
return "Parser class should only have __init__ method"
|
return "Parser class should only have __init__ method"
|
||||||
|
|
||||||
# Check if 'self.parser' and 'self.description' are assigned in __init__ method
|
# Check if 'self.parser' is assigned in __init__ method
|
||||||
init_method = node.body[0]
|
init_method = node.body[0]
|
||||||
assigned_attrs = [target.attr for expr in init_method.body if isinstance(expr, ast.Assign) for target in expr.targets if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name) and target.value.id == 'self']
|
assigned_attrs = [target.attr for expr in init_method.body if isinstance(expr, ast.Assign) for target in expr.targets if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name) and target.value.id == 'self']
|
||||||
if 'parser' not in assigned_attrs or 'description' not in assigned_attrs:
|
if 'parser' not in assigned_attrs:
|
||||||
return "Parser class should set self.parser and self.description" # 'self.parser' or 'self.description' not assigned in __init__
|
return "Parser class should set self.parser"
|
||||||
|
|
||||||
|
|
||||||
elif node.name == 'Entrypoint':
|
elif node.name == 'Entrypoint':
|
||||||
has_entrypoint = True
|
has_entrypoint = True
|
||||||
@@ -706,8 +789,7 @@ and that it includes mandatory classes with specific attributes and methods.</p>
|
|||||||
<h3 id="verifications">Verifications:</h3>
|
<h3 id="verifications">Verifications:</h3>
|
||||||
<pre><code>- The presence of only allowed top-level elements.
|
<pre><code>- The presence of only allowed top-level elements.
|
||||||
- The existence of two specific classes: 'Parser' and 'Entrypoint'. and/or specific class: Preload.
|
- The existence of two specific classes: 'Parser' and 'Entrypoint'. and/or specific class: Preload.
|
||||||
- 'Parser' class must only have an '__init__' method and must assign 'self.parser'
|
- 'Parser' class must only have an '__init__' method and must assign 'self.parser'.
|
||||||
and 'self.description'.
|
|
||||||
- 'Entrypoint' class must have an '__init__' method accepting specific arguments.
|
- 'Entrypoint' class must have an '__init__' method accepting specific arguments.
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<p>If any of these checks fail, the function returns an error message indicating
|
<p>If any of these checks fail, the function returns an error message indicating
|
||||||
@@ -2110,7 +2192,7 @@ class node:
|
|||||||
- result(bool): True if expected value is found after running
|
- result(bool): True if expected value is found after running
|
||||||
the commands using test method.
|
the commands using test method.
|
||||||
|
|
||||||
- status (int): 0 if the method run or test run succesfully.
|
- status (int): 0 if the method run or test run successfully.
|
||||||
1 if connection failed.
|
1 if connection failed.
|
||||||
2 if expect timeouts without prompt or EOF.
|
2 if expect timeouts without prompt or EOF.
|
||||||
|
|
||||||
@@ -2336,7 +2418,7 @@ class node:
|
|||||||
if connect == True:
|
if connect == True:
|
||||||
size = re.search('columns=([0-9]+).*lines=([0-9]+)',str(os.get_terminal_size()))
|
size = re.search('columns=([0-9]+).*lines=([0-9]+)',str(os.get_terminal_size()))
|
||||||
self.child.setwinsize(int(size.group(2)),int(size.group(1)))
|
self.child.setwinsize(int(size.group(2)),int(size.group(1)))
|
||||||
print("Connected to " + self.unique + " at " + self.host + (":" if self.port != '' else '') + self.port + " via: " + self.protocol)
|
printer.success("Connected to " + self.unique + " at " + self.host + (":" if self.port != '' else '') + self.port + " via: " + self.protocol)
|
||||||
if 'logfile' in dir(self):
|
if 'logfile' in dir(self):
|
||||||
# Initialize self.mylog
|
# Initialize self.mylog
|
||||||
if not 'mylog' in dir(self):
|
if not 'mylog' in dir(self):
|
||||||
@@ -2361,7 +2443,7 @@ class node:
|
|||||||
f.write(self._logclean(self.mylog.getvalue().decode(), True))
|
f.write(self._logclean(self.mylog.getvalue().decode(), True))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(connect)
|
printer.error(connect)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
@MethodHook
|
@MethodHook
|
||||||
@@ -2667,7 +2749,7 @@ class node:
|
|||||||
if isinstance(self.tags, dict) and self.tags.get("console"):
|
if isinstance(self.tags, dict) and self.tags.get("console"):
|
||||||
child.sendline()
|
child.sendline()
|
||||||
if debug:
|
if debug:
|
||||||
print(cmd)
|
printer.debug(f"Command:\n{cmd}")
|
||||||
self.mylog = io.BytesIO()
|
self.mylog = io.BytesIO()
|
||||||
child.logfile_read = self.mylog
|
child.logfile_read = self.mylog
|
||||||
|
|
||||||
@@ -2727,6 +2809,8 @@ class node:
|
|||||||
sleep(1)
|
sleep(1)
|
||||||
child.readline(0)
|
child.readline(0)
|
||||||
self.child = child
|
self.child = child
|
||||||
|
from pexpect import fdpexpect
|
||||||
|
self.raw_child = fdpexpect.fdspawn(self.child.child_fd)
|
||||||
return True</code></pre>
|
return True</code></pre>
|
||||||
</details>
|
</details>
|
||||||
<div class="desc"><p>This class generates a node object. Containts all the information and methods to connect and interact with a device using ssh or telnet.</p>
|
<div class="desc"><p>This class generates a node object. Containts all the information and methods to connect and interact with a device using ssh or telnet.</p>
|
||||||
@@ -2737,7 +2821,7 @@ class node:
|
|||||||
- result(bool): True if expected value is found after running
|
- result(bool): True if expected value is found after running
|
||||||
the commands using test method.
|
the commands using test method.
|
||||||
|
|
||||||
- status (int): 0 if the method run or test run succesfully.
|
- status (int): 0 if the method run or test run successfully.
|
||||||
1 if connection failed.
|
1 if connection failed.
|
||||||
2 if expect timeouts without prompt or EOF.
|
2 if expect timeouts without prompt or EOF.
|
||||||
</code></pre>
|
</code></pre>
|
||||||
@@ -2796,7 +2880,7 @@ def interact(self, debug = False):
|
|||||||
if connect == True:
|
if connect == True:
|
||||||
size = re.search('columns=([0-9]+).*lines=([0-9]+)',str(os.get_terminal_size()))
|
size = re.search('columns=([0-9]+).*lines=([0-9]+)',str(os.get_terminal_size()))
|
||||||
self.child.setwinsize(int(size.group(2)),int(size.group(1)))
|
self.child.setwinsize(int(size.group(2)),int(size.group(1)))
|
||||||
print("Connected to " + self.unique + " at " + self.host + (":" if self.port != '' else '') + self.port + " via: " + self.protocol)
|
printer.success("Connected to " + self.unique + " at " + self.host + (":" if self.port != '' else '') + self.port + " via: " + self.protocol)
|
||||||
if 'logfile' in dir(self):
|
if 'logfile' in dir(self):
|
||||||
# Initialize self.mylog
|
# Initialize self.mylog
|
||||||
if not 'mylog' in dir(self):
|
if not 'mylog' in dir(self):
|
||||||
@@ -2821,7 +2905,7 @@ def interact(self, debug = False):
|
|||||||
f.write(self._logclean(self.mylog.getvalue().decode(), True))
|
f.write(self._logclean(self.mylog.getvalue().decode(), True))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(connect)
|
printer.error(connect)
|
||||||
exit(1)</code></pre>
|
exit(1)</code></pre>
|
||||||
</details>
|
</details>
|
||||||
<div class="desc"><p>Allow user to interact with the node directly, mostly used by connection manager.</p>
|
<div class="desc"><p>Allow user to interact with the node directly, mostly used by connection manager.</p>
|
||||||
@@ -3143,7 +3227,7 @@ class nodes:
|
|||||||
Created after running method test.
|
Created after running method test.
|
||||||
|
|
||||||
- status (dict): Dictionary formed by nodes unique as keys, value:
|
- status (dict): Dictionary formed by nodes unique as keys, value:
|
||||||
0 if method run or test ended succesfully.
|
0 if method run or test ended successfully.
|
||||||
1 if connection failed.
|
1 if connection failed.
|
||||||
2 if expect timeouts without prompt or EOF.
|
2 if expect timeouts without prompt or EOF.
|
||||||
|
|
||||||
@@ -3365,7 +3449,7 @@ class nodes:
|
|||||||
Created after running method test.
|
Created after running method test.
|
||||||
|
|
||||||
- status (dict): Dictionary formed by nodes unique as keys, value:
|
- status (dict): Dictionary formed by nodes unique as keys, value:
|
||||||
0 if method run or test ended succesfully.
|
0 if method run or test ended successfully.
|
||||||
1 if connection failed.
|
1 if connection failed.
|
||||||
2 if expect timeouts without prompt or EOF.
|
2 if expect timeouts without prompt or EOF.
|
||||||
|
|
||||||
@@ -3673,6 +3757,20 @@ def test(self, commands, expected, vars = None,*, prompt = None, parallel = 10,
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="#executable-block">Executable Block</a></li>
|
<li><a href="#executable-block">Executable Block</a></li>
|
||||||
|
<li><a href="#command-completion-support">Command Completion Support</a><ul>
|
||||||
|
<li><a href="#function-signature">Function Signature</a></li>
|
||||||
|
<li><a href="#parameters">Parameters</a></li>
|
||||||
|
<li><a href="#contents-of-info">Contents of info</a></li>
|
||||||
|
<li><a href="#return-value">Return Value</a></li>
|
||||||
|
<li><a href="#example">Example</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><a href="#handling-unknown-arguments">Handling Unknown Arguments</a><ul>
|
||||||
|
<li><a href="#behavior">Behavior:</a></li>
|
||||||
|
<li><a href="#example_1">Example:</a></li>
|
||||||
|
<li><a href="#note">Note:</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
<li><a href="#script-verification">Script Verification</a></li>
|
<li><a href="#script-verification">Script Verification</a></li>
|
||||||
<li><a href="#example-script">Example Script</a></li>
|
<li><a href="#example-script">Example Script</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@@ -3,7 +3,7 @@ Flask_Cors>=4.0.1
|
|||||||
google_api_python_client>=2.125.0
|
google_api_python_client>=2.125.0
|
||||||
google_auth_oauthlib>=1.2.0
|
google_auth_oauthlib>=1.2.0
|
||||||
inquirer>=3.3.0
|
inquirer>=3.3.0
|
||||||
openai>=0.27.8
|
openai>=1.98.0
|
||||||
pexpect>=4.8.0
|
pexpect>=4.8.0
|
||||||
protobuf>=5.27.2
|
protobuf>=5.27.2
|
||||||
pycryptodome>=3.18.0
|
pycryptodome>=3.18.0
|
||||||
|
Reference in New Issue
Block a user