This commit is contained in:
fluzzi 2023-04-14 11:44:56 -03:00
parent 68b63baeac
commit 51bdc4e59a
5 changed files with 447 additions and 156 deletions

View File

@ -41,6 +41,7 @@ Commands:
bulk Add nodes in bulk bulk Add nodes in bulk
run Run scripts or commands on nodes run Run scripts or commands on nodes
config Manage app config config Manage app config
api Start and stop connpy api
``` ```
### Manage profiles ### Manage profiles
@ -70,7 +71,62 @@ options:
conn pc@office conn pc@office
conn server conn server
``` ```
## http API
With the Connpy API you can run commands on devices using http requests
### 1. List Nodes
**Endpoint**: `/list_nodes`
**Method**: `POST`
**Description**: This route returns a list of nodes. It can also filter the list based on a given keyword.
#### Request Body:
```json
{
"filter": "<keyword>"
}
```
* `filter` (optional): A keyword to filter the list of nodes. It returns only the nodes that contain the keyword. If not provided, the route will return the entire list of nodes.
#### Response:
- A JSON array containing the filtered list of nodes.
---
### 2. Run Commands
**Endpoint**: `/run_commands`
**Method**: `POST`
**Description**: This route runs commands on selected nodes based on the provided action, nodes, and commands. It also supports executing tests by providing expected results.
#### Request Body:
```json
{
"action": "<action>",
"nodes": "<nodes>",
"commands": "<commands>",
"expected": "<expected>",
"options": "<options>"
}
```
* `action` (required): The action to be performed. Possible values: `run` or `test`.
* `nodes` (required): A list of nodes or a single node on which the commands will be executed. The nodes can be specified as individual node names or a node group with the `@` prefix. Node groups can also be specified as arrays with a list of nodes inside the group.
* `commands` (required): A list of commands to be executed on the specified nodes.
* `expected` (optional, only used when the action is `test`): A single expected result for the test.
* `options` (optional): Array to pass options to the run command, options are: `prompt`, `parallel`, `timeout`
#### Response:
- A JSON object with the results of the executed commands on the nodes.
## Automation module ## Automation module
the automation module the automation module
@ -157,4 +213,5 @@ __author__ = "Federico Luzzi"
__pdoc__ = { __pdoc__ = {
'core': False, 'core': False,
'completion': False, 'completion': False,
'api': False
} }

View File

@ -7,77 +7,139 @@ import signal
app = Flask(__name__) app = Flask(__name__)
conf = configfile() conf = configfile()
PID_FILE = ".connpy_server.pid" PID_FILE1 = "/run/connpy.pid"
PID_FILE2 = "/tmp/connpy.pid"
@app.route("/") @app.route("/")
def hello(): def root():
return "Welcome to the Connpy API!" return jsonify({
'message': 'Welcome to Connpy api',
'version': '1.0',
'documentation': 'https://fluzzi.github.io/connpy/'
})
@app.route("/add_node", methods=["POST"]) @app.route("/list_nodes", methods=["POST"])
def add_node(): def list_nodes():
node_data = request.get_json() conf = app.custom_config
unique = node_data["unique"] output = conf._getallnodes()
host = node_data["host"] case = conf.config["case"]
user = node_data["user"] try:
password = node_data["password"] data = request.get_json()
filter = data["filter"]
conf.add(unique, host=host, user=user, password=password) if not case:
filter = filter.lower()
return jsonify({"message": f"Node {unique} added successfully"}) output = [item for item in output if filter in item]
except:
pass
return jsonify(output)
@app.route("/run_commands", methods=["POST"]) @app.route("/run_commands", methods=["POST"])
def run_commands(): def run_commands():
conf = app.custom_config conf = app.custom_config
data = request.get_json() data = request.get_json()
unique = data["unique"] case = conf.config["case"]
commands = data["commands"] mynodes = {}
node_data = conf.getitem(unique) args = {}
conn_node = node(unique,**node_data, config=conf) try:
action = data["action"]
output = conn_node.run(commands) nodelist = data["nodes"]
args["commands"] = data["commands"]
if action == "test":
args["expected"] = data["expected"]
except KeyError as e:
error = "'{}' is mandatory".format(e.args[0])
return({"DataError": error})
if isinstance(nodelist, list):
for i in nodelist:
if isinstance(i, dict):
name = list(i.keys())[0]
mylist = i[name]
if not case:
name = name.lower()
mylist = [item.lower() for item in mylist]
this = conf.getitem(name, mylist)
mynodes.update(this)
elif i.startswith("@"):
if not case:
i = i.lower()
this = conf.getitem(i)
mynodes.update(this)
else:
if not case:
i = i.lower()
this = conf.getitem(i)
mynodes[i] = this
else:
if not case:
nodelist = nodelist.lower()
if nodelist.startswith("@"):
mynodes = conf.getitem(nodelist)
else:
mynodes[nodelist] = conf.getitem(nodelist)
mynodes = nodes(mynodes, config=conf)
try:
args["vars"] = data["variables"]
except:
pass
try:
options = data["options"]
thisoptions = {k: v for k, v in options.items() if k in ["prompt", "parallel", "timeout"]}
args.update(thisoptions)
except:
options = None
if action == "run":
output = mynodes.run(**args)
elif action == "test":
output = mynodes.test(**args)
else:
error = "Wrong action '{}'".format(action)
return({"DataError": error})
return output return output
@app.route("/run_commands_on_nodes", methods=["POST"])
def run_commands_on_nodes():
data = request.get_json()
unique_list = data["unique_list"]
commands = data["commands"]
nodes_data = {unique: conf.getitem(unique) for unique in unique_list}
conn_nodes = nodes(nodes_data)
output = conn_nodes.run(commands)
return jsonify({"output": output})
def stop_api(): def stop_api():
# Read the process ID (pid) from the file # Read the process ID (pid) from the file
with open(PID_FILE, "r") as f: try:
pid = int(f.read().strip()) with open(PID_FILE1, "r") as f:
pid = int(f.read().strip())
PID_FILE=PID_FILE1
except:
try:
with open(PID_FILE2, "r") as f:
pid = int(f.read().strip())
PID_FILE=PID_FILE2
except:
print("Connpy api server is not running.")
return
# Send a SIGTERM signal to the process # Send a SIGTERM signal to the process
os.kill(pid, signal.SIGTERM) os.kill(pid, signal.SIGTERM)
# Delete the PID file # Delete the PID file
os.remove(PID_FILE) os.remove(PID_FILE)
print(f"Server with process ID {pid} stopped.") print(f"Server with process ID {pid} stopped.")
def start_server(folder): def start_server():
folder = folder.rstrip('/') app.custom_config = configfile()
file = folder + '/config.json'
key = folder + '/.osk'
app.custom_config = configfile(file, key)
serve(app, host='0.0.0.0', port=8048) serve(app, host='0.0.0.0', port=8048)
def start_api(folder): def start_api():
if os.path.exists(PID_FILE1) or os.path.exists(PID_FILE2):
print("Connpy server is already running.")
return
pid = os.fork() pid = os.fork()
if pid == 0: if pid == 0:
start_server(folder) start_server()
else: else:
with open(PID_FILE, "w") as f: try:
f.write(str(pid)) with open(PID_FILE1, "w") as f:
print(f'Server is running') f.write(str(pid))
except:
try:
with open(PID_FILE2, "w") as f:
f.write(str(pid))
except:
print("Cound't create PID file")
return
print(f'Server is running with process ID {pid}.')

View File

@ -49,9 +49,17 @@ class configfile:
''' '''
home = os.path.expanduser("~") home = os.path.expanduser("~")
defaultdir = home + '/.config/conn' defaultdir = home + '/.config/conn'
defaultfile = defaultdir + '/config.json'
defaultkey = defaultdir + '/.osk'
Path(defaultdir).mkdir(parents=True, exist_ok=True) Path(defaultdir).mkdir(parents=True, exist_ok=True)
pathfile = defaultdir + '/.folder'
try:
with open(pathfile, "r") as f:
configdir = f.read().strip()
except:
with open(pathfile, "w") as f:
f.write(str(defaultdir))
configdir = defaultdir
defaultfile = configdir + '/config.json'
defaultkey = configdir + '/.osk'
if conf == None: if conf == None:
self.file = defaultfile self.file = defaultfile
else: else:
@ -233,3 +241,44 @@ class configfile:
def _profiles_del(self,*, id ): def _profiles_del(self,*, id ):
#Delete profile from config #Delete profile from config
del self.profiles[id] del self.profiles[id]
def _getallnodes(self):
#get all nodes on configfile
nodes = []
layer1 = [k for k,v in self.connections.items() if isinstance(v, dict) and v["type"] == "connection"]
folders = [k for k,v in self.connections.items() if isinstance(v, dict) and v["type"] == "folder"]
nodes.extend(layer1)
for f in folders:
layer2 = [k + "@" + f for k,v in self.connections[f].items() if isinstance(v, dict) and v["type"] == "connection"]
nodes.extend(layer2)
subfolders = [k for k,v in self.connections[f].items() if isinstance(v, dict) and v["type"] == "subfolder"]
for s in subfolders:
layer3 = [k + "@" + s + "@" + f for k,v in self.connections[f][s].items() if isinstance(v, dict) and v["type"] == "connection"]
nodes.extend(layer3)
return nodes
def _getallfolders(self):
#get all folders on configfile
folders = ["@" + k for k,v in self.connections.items() if isinstance(v, dict) and v["type"] == "folder"]
subfolders = []
for f in folders:
s = ["@" + k + f for k,v in self.connections[f[1:]].items() if isinstance(v, dict) and v["type"] == "subfolder"]
subfolders.extend(s)
folders.extend(subfolders)
return folders
def _profileused(self, profile):
#Check if profile is used before deleting it
nodes = []
layer1 = [k for k,v in self.connections.items() if isinstance(v, dict) and v["type"] == "connection" and ("@" + profile in v.values() or ( isinstance(v["password"],list) and "@" + profile in v["password"]))]
folders = [k for k,v in self.connections.items() if isinstance(v, dict) and v["type"] == "folder"]
nodes.extend(layer1)
for f in folders:
layer2 = [k + "@" + f for k,v in self.connections[f].items() if isinstance(v, dict) and v["type"] == "connection" and ("@" + profile in v.values() or ( isinstance(v["password"],list) and "@" + profile in v["password"]))]
nodes.extend(layer2)
subfolders = [k for k,v in self.connections[f].items() if isinstance(v, dict) and v["type"] == "subfolder"]
for s in subfolders:
layer3 = [k + "@" + s + "@" + f for k,v in self.connections[f][s].items() if isinstance(v, dict) and v["type"] == "connection" and ("@" + profile in v.values() or ( isinstance(v["password"],list) and "@" + profile in v["password"]))]
nodes.extend(layer3)
return nodes

View File

@ -40,8 +40,8 @@ class connapp:
self.node = node self.node = node
self.connnodes = nodes self.connnodes = nodes
self.config = config self.config = config
self.nodes = self._getallnodes() self.nodes = self.config._getallnodes()
self.folders = self._getallfolders() self.folders = self.config._getallfolders()
self.profiles = list(self.config.profiles.keys()) self.profiles = list(self.config.profiles.keys())
self.case = self.config.config["case"] self.case = self.config.config["case"]
try: try:
@ -106,7 +106,8 @@ class connapp:
#APIPARSER #APIPARSER
apiparser = subparsers.add_parser("api", help="Start and stop connpy api") apiparser = subparsers.add_parser("api", help="Start and stop connpy api")
apicrud = apiparser.add_mutually_exclusive_group(required=True) apicrud = apiparser.add_mutually_exclusive_group(required=True)
apicrud.add_argument("--start", dest="start", nargs="?", action=self._store_type, help="Start connpy api", default="desfaultdir") apicrud.add_argument("--start", dest="start", nargs=0, action=self._store_type, help="Start conppy api")
apicrud.add_argument("--restart", dest="restart", nargs=0, action=self._store_type, help="Restart conppy api")
apicrud.add_argument("--stop", dest="stop", nargs=0, action=self._store_type, help="Stop conppy api") apicrud.add_argument("--stop", dest="stop", nargs=0, action=self._store_type, help="Stop conppy api")
apiparser.set_defaults(func=self._func_api) apiparser.set_defaults(func=self._func_api)
#CONFIGPARSER #CONFIGPARSER
@ -116,6 +117,7 @@ class connapp:
configcrud.add_argument("--fzf", dest="fzf", nargs=1, action=self._store_type, help="Use fzf for lists", choices=["true","false"]) configcrud.add_argument("--fzf", dest="fzf", nargs=1, action=self._store_type, help="Use fzf for lists", choices=["true","false"])
configcrud.add_argument("--keepalive", dest="idletime", nargs=1, action=self._store_type, help="Set keepalive time in seconds, 0 to disable", type=int, metavar="INT") configcrud.add_argument("--keepalive", dest="idletime", nargs=1, action=self._store_type, help="Set keepalive time in seconds, 0 to disable", type=int, metavar="INT")
configcrud.add_argument("--completion", dest="completion", nargs=1, choices=["bash","zsh"], action=self._store_type, help="Get terminal completion configuration for conn") configcrud.add_argument("--completion", dest="completion", nargs=1, choices=["bash","zsh"], action=self._store_type, help="Get terminal completion configuration for conn")
configcrud.add_argument("--configfolder", dest="configfolder", nargs=1, action=self._store_type, help="Set the default location for config file", metavar="FOLDER")
configparser.set_defaults(func=self._func_others) configparser.set_defaults(func=self._func_others)
#Manage sys arguments #Manage sys arguments
commands = ["node", "profile", "mv", "move","copy", "cp", "bulk", "ls", "list", "run", "config", "api"] commands = ["node", "profile", "mv", "move","copy", "cp", "bulk", "ls", "list", "run", "config", "api"]
@ -307,7 +309,7 @@ class connapp:
if matches[0] == "default": if matches[0] == "default":
print("Can't delete default profile") print("Can't delete default profile")
exit(6) exit(6)
usedprofile = self._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])) print("Profile {} used in the following nodes:".format(matches[0]))
print(", ".join(usedprofile)) print(", ".join(usedprofile))
@ -369,7 +371,7 @@ class connapp:
def _func_others(self, args): def _func_others(self, args):
#Function called when using other commands #Function called when using other commands
actions = {"ls": self._ls, "move": self._mvcp, "cp": self._mvcp, "bulk": self._bulk, "completion": self._completion, "case": self._case, "fzf": self._fzf, "idletime": self._idletime} actions = {"ls": self._ls, "move": self._mvcp, "cp": self._mvcp, "bulk": self._bulk, "completion": self._completion, "case": self._case, "fzf": self._fzf, "idletime": self._idletime, "configfolder": self._configfolder}
return actions.get(args.command)(args) return actions.get(args.command)(args)
def _ls(self, args): def _ls(self, args):
@ -443,7 +445,7 @@ class connapp:
newnode["password"] = newnodes["password"] newnode["password"] = newnodes["password"]
count +=1 count +=1
self.config._connections_add(**newnode) self.config._connections_add(**newnode)
self.nodes = self._getallnodes() self.nodes = 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)) print("Succesfully added {} nodes".format(count))
@ -475,6 +477,16 @@ class connapp:
args.data[0] = 0 args.data[0] = 0
self._change_settings(args.command, args.data[0]) self._change_settings(args.command, args.data[0])
def _configfolder(self, args):
if not os.path.isdir(args.data[0]):
raise argparse.ArgumentTypeError(f"readable_dir:{args.data[0]} is not a valid path")
else:
pathfile = defaultdir + "/.folder"
folder = os.path.abspath(args.data[0]).rstrip('/')
with open(pathfile, "w") as f:
f.write(str(folder))
print("Config saved")
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)
@ -487,15 +499,10 @@ class connapp:
return actions.get(args.action)(args) return actions.get(args.action)(args)
def _func_api(self, args): def _func_api(self, args):
if args.command == "start": if args.command == "stop" or args.command == "restart":
if args.data == None:
args.data = defaultdir
else:
if not os.path.isdir(args.data):
raise argparse.ArgumentTypeError(f"readable_dir:{args.data} is not a valid path")
start_api(args.data)
if args.command == "stop":
stop_api() stop_api()
if args.command == "start" or args.command == "restart":
start_api()
return return
def _node_run(self, args): def _node_run(self, args):
@ -888,7 +895,7 @@ class connapp:
if type == "usage": if type == "usage":
return "conn [-h] [--add | --del | --mod | --show | --debug] [node|folder]\n conn {profile,move,mv,copy,cp,list,ls,bulk,config} ..." return "conn [-h] [--add | --del | --mod | --show | --debug] [node|folder]\n conn {profile,move,mv,copy,cp,list,ls,bulk,config} ..."
if type == "end": if type == "end":
return "Commands:\n profile Manage profiles\n move (mv) Move node\n copy (cp) Copy node\n list (ls) List profiles, nodes or folders\n bulk Add nodes in bulk\n run Run scripts or commands on nodes\n config Manage app config" return "Commands:\n profile Manage profiles\n move (mv) Move node\n copy (cp) Copy node\n list (ls) List profiles, nodes or folders\n bulk Add nodes in bulk\n run Run scripts or commands on nodes\n config Manage app config\n api Start and stop connpy api"
if type == "bashcompletion": if type == "bashcompletion":
return ''' return '''
#Here starts bash completion for conn #Here starts bash completion for conn
@ -988,46 +995,6 @@ tasks:
output: null output: null
...''' ...'''
def _getallnodes(self):
#get all nodes on configfile
nodes = []
layer1 = [k for k,v in self.config.connections.items() if isinstance(v, dict) and v["type"] == "connection"]
folders = [k for k,v in self.config.connections.items() if isinstance(v, dict) and v["type"] == "folder"]
nodes.extend(layer1)
for f in folders:
layer2 = [k + "@" + f for k,v in self.config.connections[f].items() if isinstance(v, dict) and v["type"] == "connection"]
nodes.extend(layer2)
subfolders = [k for k,v in self.config.connections[f].items() if isinstance(v, dict) and v["type"] == "subfolder"]
for s in subfolders:
layer3 = [k + "@" + s + "@" + f for k,v in self.config.connections[f][s].items() if isinstance(v, dict) and v["type"] == "connection"]
nodes.extend(layer3)
return nodes
def _getallfolders(self):
#get all folders on configfile
folders = ["@" + k for k,v in self.config.connections.items() if isinstance(v, dict) and v["type"] == "folder"]
subfolders = []
for f in folders:
s = ["@" + k + f for k,v in self.config.connections[f[1:]].items() if isinstance(v, dict) and v["type"] == "subfolder"]
subfolders.extend(s)
folders.extend(subfolders)
return folders
def _profileused(self, profile):
#Check if profile is used before deleting it
nodes = []
layer1 = [k for k,v in self.config.connections.items() if isinstance(v, dict) and v["type"] == "connection" and ("@" + profile in v.values() or ( isinstance(v["password"],list) and "@" + profile in v["password"]))]
folders = [k for k,v in self.config.connections.items() if isinstance(v, dict) and v["type"] == "folder"]
nodes.extend(layer1)
for f in folders:
layer2 = [k + "@" + f for k,v in self.config.connections[f].items() if isinstance(v, dict) and v["type"] == "connection" and ("@" + profile in v.values() or ( isinstance(v["password"],list) and "@" + profile in v["password"]))]
nodes.extend(layer2)
subfolders = [k for k,v in self.config.connections[f].items() if isinstance(v, dict) and v["type"] == "subfolder"]
for s in subfolders:
layer3 = [k + "@" + s + "@" + f for k,v in self.config.connections[f][s].items() if isinstance(v, dict) and v["type"] == "connection" and ("@" + profile in v.values() or ( isinstance(v["password"],list) and "@" + profile in v["password"]))]
nodes.extend(layer3)
return nodes
def encrypt(self, password, keyfile=None): def encrypt(self, password, keyfile=None):
''' '''
Encrypts password using RSA keyfile Encrypts password using RSA keyfile

View File

@ -60,6 +60,7 @@ Commands:
bulk Add nodes in bulk bulk Add nodes in bulk
run Run scripts or commands on nodes run Run scripts or commands on nodes
config Manage app config config Manage app config
api Start and stop connpy api
</code></pre> </code></pre>
<h3 id="manage-profiles">Manage profiles</h3> <h3 id="manage-profiles">Manage profiles</h3>
<pre><code>usage: conn profile [-h] (--add | --del | --mod | --show) profile <pre><code>usage: conn profile [-h] (--add | --del | --mod | --show) profile
@ -85,6 +86,50 @@ options:
conn pc@office conn pc@office
conn server conn server
</code></pre> </code></pre>
<h2 id="http-api">http API</h2>
<p>With the Connpy API you can run commands on devices using http requests</p>
<h3 id="1-list-nodes">1. List Nodes</h3>
<p><strong>Endpoint</strong>: <code>/list_nodes</code></p>
<p><strong>Method</strong>: <code>POST</code></p>
<p><strong>Description</strong>: This route returns a list of nodes. It can also filter the list based on a given keyword.</p>
<h4 id="request-body">Request Body:</h4>
<pre><code class="language-json">{
&quot;filter&quot;: &quot;&lt;keyword&gt;&quot;
}
</code></pre>
<ul>
<li><code>filter</code> (optional): A keyword to filter the list of nodes. It returns only the nodes that contain the keyword. If not provided, the route will return the entire list of nodes.</li>
</ul>
<h4 id="response">Response:</h4>
<ul>
<li>A JSON array containing the filtered list of nodes.</li>
</ul>
<hr>
<h3 id="2-run-commands">2. Run Commands</h3>
<p><strong>Endpoint</strong>: <code>/run_commands</code></p>
<p><strong>Method</strong>: <code>POST</code></p>
<p><strong>Description</strong>: This route runs commands on selected nodes based on the provided action, nodes, and commands. It also supports executing tests by providing expected results.</p>
<h4 id="request-body_1">Request Body:</h4>
<pre><code class="language-json">{
&quot;action&quot;: &quot;&lt;action&gt;&quot;,
&quot;nodes&quot;: &quot;&lt;nodes&gt;&quot;,
&quot;commands&quot;: &quot;&lt;commands&gt;&quot;,
&quot;expected&quot;: &quot;&lt;expected&gt;&quot;,
&quot;options&quot;: &quot;&lt;options&gt;&quot;
}
</code></pre>
<ul>
<li><code>action</code> (required): The action to be performed. Possible values: <code>run</code> or <code>test</code>.</li>
<li><code><a title="connpy.nodes" href="#connpy.nodes">nodes</a></code> (required): A list of nodes or a single node on which the commands will be executed. The nodes can be specified as individual node names or a node group with the <code>@</code> prefix. Node groups can also be specified as arrays with a list of nodes inside the group.</li>
<li><code>commands</code> (required): A list of commands to be executed on the specified nodes.</li>
<li><code>expected</code> (optional, only used when the action is <code>test</code>): A single expected result for the test.</li>
<li><code>options</code> (optional): Array to pass options to the run command, options are: <code>prompt</code>, <code>parallel</code>, <code>timeout</code>
</li>
</ul>
<h4 id="response_1">Response:</h4>
<ul>
<li>A JSON object with the results of the executed commands on the nodes.</li>
</ul>
<h2 id="automation-module">Automation module</h2> <h2 id="automation-module">Automation module</h2>
<p>the automation module</p> <p>the automation module</p>
<h3 id="standalone-module">Standalone module</h3> <h3 id="standalone-module">Standalone module</h3>
@ -200,6 +245,7 @@ Commands:
bulk Add nodes in bulk bulk Add nodes in bulk
run Run scripts or commands on nodes run Run scripts or commands on nodes
config Manage app config config Manage app config
api Start and stop connpy api
``` ```
### Manage profiles ### Manage profiles
@ -229,7 +275,62 @@ options:
conn pc@office conn pc@office
conn server conn server
``` ```
## http API
With the Connpy API you can run commands on devices using http requests
### 1. List Nodes
**Endpoint**: `/list_nodes`
**Method**: `POST`
**Description**: This route returns a list of nodes. It can also filter the list based on a given keyword.
#### Request Body:
```json
{
&#34;filter&#34;: &#34;&lt;keyword&gt;&#34;
}
```
* `filter` (optional): A keyword to filter the list of nodes. It returns only the nodes that contain the keyword. If not provided, the route will return the entire list of nodes.
#### Response:
- A JSON array containing the filtered list of nodes.
---
### 2. Run Commands
**Endpoint**: `/run_commands`
**Method**: `POST`
**Description**: This route runs commands on selected nodes based on the provided action, nodes, and commands. It also supports executing tests by providing expected results.
#### Request Body:
```json
{
&#34;action&#34;: &#34;&lt;action&gt;&#34;,
&#34;nodes&#34;: &#34;&lt;nodes&gt;&#34;,
&#34;commands&#34;: &#34;&lt;commands&gt;&#34;,
&#34;expected&#34;: &#34;&lt;expected&gt;&#34;,
&#34;options&#34;: &#34;&lt;options&gt;&#34;
}
```
* `action` (required): The action to be performed. Possible values: `run` or `test`.
* `nodes` (required): A list of nodes or a single node on which the commands will be executed. The nodes can be specified as individual node names or a node group with the `@` prefix. Node groups can also be specified as arrays with a list of nodes inside the group.
* `commands` (required): A list of commands to be executed on the specified nodes.
* `expected` (optional, only used when the action is `test`): A single expected result for the test.
* `options` (optional): Array to pass options to the run command, options are: `prompt`, `parallel`, `timeout`
#### Response:
- A JSON object with the results of the executed commands on the nodes.
## Automation module ## Automation module
the automation module the automation module
@ -316,6 +417,7 @@ __author__ = &#34;Federico Luzzi&#34;
__pdoc__ = { __pdoc__ = {
&#39;core&#39;: False, &#39;core&#39;: False,
&#39;completion&#39;: False, &#39;completion&#39;: False,
&#39;api&#39;: False
}</code></pre> }</code></pre>
</details> </details>
</section> </section>
@ -404,9 +506,17 @@ __pdoc__ = {
&#39;&#39;&#39; &#39;&#39;&#39;
home = os.path.expanduser(&#34;~&#34;) home = os.path.expanduser(&#34;~&#34;)
defaultdir = home + &#39;/.config/conn&#39; defaultdir = home + &#39;/.config/conn&#39;
defaultfile = defaultdir + &#39;/config.json&#39;
defaultkey = defaultdir + &#39;/.osk&#39;
Path(defaultdir).mkdir(parents=True, exist_ok=True) Path(defaultdir).mkdir(parents=True, exist_ok=True)
pathfile = defaultdir + &#39;/.folder&#39;
try:
with open(pathfile, &#34;r&#34;) as f:
configdir = f.read().strip()
except:
with open(pathfile, &#34;w&#34;) as f:
f.write(str(defaultdir))
configdir = defaultdir
defaultfile = configdir + &#39;/config.json&#39;
defaultkey = configdir + &#39;/.osk&#39;
if conf == None: if conf == None:
self.file = defaultfile self.file = defaultfile
else: else:
@ -587,7 +697,47 @@ __pdoc__ = {
def _profiles_del(self,*, id ): def _profiles_del(self,*, id ):
#Delete profile from config #Delete profile from config
del self.profiles[id]</code></pre> del self.profiles[id]
def _getallnodes(self):
#get all nodes on configfile
nodes = []
layer1 = [k for k,v in self.connections.items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;connection&#34;]
folders = [k for k,v in self.connections.items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;folder&#34;]
nodes.extend(layer1)
for f in folders:
layer2 = [k + &#34;@&#34; + f for k,v in self.connections[f].items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;connection&#34;]
nodes.extend(layer2)
subfolders = [k for k,v in self.connections[f].items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;subfolder&#34;]
for s in subfolders:
layer3 = [k + &#34;@&#34; + s + &#34;@&#34; + f for k,v in self.connections[f][s].items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;connection&#34;]
nodes.extend(layer3)
return nodes
def _getallfolders(self):
#get all folders on configfile
folders = [&#34;@&#34; + k for k,v in self.connections.items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;folder&#34;]
subfolders = []
for f in folders:
s = [&#34;@&#34; + k + f for k,v in self.connections[f[1:]].items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;subfolder&#34;]
subfolders.extend(s)
folders.extend(subfolders)
return folders
def _profileused(self, profile):
#Check if profile is used before deleting it
nodes = []
layer1 = [k for k,v in self.connections.items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;connection&#34; and (&#34;@&#34; + profile in v.values() or ( isinstance(v[&#34;password&#34;],list) and &#34;@&#34; + profile in v[&#34;password&#34;]))]
folders = [k for k,v in self.connections.items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;folder&#34;]
nodes.extend(layer1)
for f in folders:
layer2 = [k + &#34;@&#34; + f for k,v in self.connections[f].items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;connection&#34; and (&#34;@&#34; + profile in v.values() or ( isinstance(v[&#34;password&#34;],list) and &#34;@&#34; + profile in v[&#34;password&#34;]))]
nodes.extend(layer2)
subfolders = [k for k,v in self.connections[f].items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;subfolder&#34;]
for s in subfolders:
layer3 = [k + &#34;@&#34; + s + &#34;@&#34; + f for k,v in self.connections[f][s].items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;connection&#34; and (&#34;@&#34; + profile in v.values() or ( isinstance(v[&#34;password&#34;],list) and &#34;@&#34; + profile in v[&#34;password&#34;]))]
nodes.extend(layer3)
return nodes</code></pre>
</details> </details>
<h3>Methods</h3> <h3>Methods</h3>
<dl> <dl>
@ -703,8 +853,8 @@ __pdoc__ = {
self.node = node self.node = node
self.connnodes = nodes self.connnodes = nodes
self.config = config self.config = config
self.nodes = self._getallnodes() self.nodes = self.config._getallnodes()
self.folders = self._getallfolders() self.folders = self.config._getallfolders()
self.profiles = list(self.config.profiles.keys()) self.profiles = list(self.config.profiles.keys())
self.case = self.config.config[&#34;case&#34;] self.case = self.config.config[&#34;case&#34;]
try: try:
@ -766,6 +916,13 @@ __pdoc__ = {
runparser.add_argument(&#34;run&#34;, nargs=&#39;+&#39;, action=self._store_type, help=self._help(&#34;run&#34;), default=&#34;run&#34;) runparser.add_argument(&#34;run&#34;, nargs=&#39;+&#39;, action=self._store_type, help=self._help(&#34;run&#34;), default=&#34;run&#34;)
runparser.add_argument(&#34;-g&#34;,&#34;--generate&#34;, dest=&#34;action&#34;, action=&#34;store_const&#34;, help=&#34;Generate yaml file template&#34;, const=&#34;generate&#34;, default=&#34;run&#34;) runparser.add_argument(&#34;-g&#34;,&#34;--generate&#34;, dest=&#34;action&#34;, action=&#34;store_const&#34;, help=&#34;Generate yaml file template&#34;, const=&#34;generate&#34;, default=&#34;run&#34;)
runparser.set_defaults(func=self._func_run) runparser.set_defaults(func=self._func_run)
#APIPARSER
apiparser = subparsers.add_parser(&#34;api&#34;, help=&#34;Start and stop connpy api&#34;)
apicrud = apiparser.add_mutually_exclusive_group(required=True)
apicrud.add_argument(&#34;--start&#34;, dest=&#34;start&#34;, nargs=0, action=self._store_type, help=&#34;Start conppy api&#34;)
apicrud.add_argument(&#34;--restart&#34;, dest=&#34;restart&#34;, nargs=0, action=self._store_type, help=&#34;Restart conppy api&#34;)
apicrud.add_argument(&#34;--stop&#34;, dest=&#34;stop&#34;, nargs=0, action=self._store_type, help=&#34;Stop conppy api&#34;)
apiparser.set_defaults(func=self._func_api)
#CONFIGPARSER #CONFIGPARSER
configparser = subparsers.add_parser(&#34;config&#34;, help=&#34;Manage app config&#34;) configparser = subparsers.add_parser(&#34;config&#34;, help=&#34;Manage app config&#34;)
configcrud = configparser.add_mutually_exclusive_group(required=True) configcrud = configparser.add_mutually_exclusive_group(required=True)
@ -773,9 +930,10 @@ __pdoc__ = {
configcrud.add_argument(&#34;--fzf&#34;, dest=&#34;fzf&#34;, nargs=1, action=self._store_type, help=&#34;Use fzf for lists&#34;, choices=[&#34;true&#34;,&#34;false&#34;]) configcrud.add_argument(&#34;--fzf&#34;, dest=&#34;fzf&#34;, nargs=1, action=self._store_type, help=&#34;Use fzf for lists&#34;, choices=[&#34;true&#34;,&#34;false&#34;])
configcrud.add_argument(&#34;--keepalive&#34;, dest=&#34;idletime&#34;, nargs=1, action=self._store_type, help=&#34;Set keepalive time in seconds, 0 to disable&#34;, type=int, metavar=&#34;INT&#34;) configcrud.add_argument(&#34;--keepalive&#34;, dest=&#34;idletime&#34;, nargs=1, action=self._store_type, help=&#34;Set keepalive time in seconds, 0 to disable&#34;, type=int, metavar=&#34;INT&#34;)
configcrud.add_argument(&#34;--completion&#34;, dest=&#34;completion&#34;, nargs=1, choices=[&#34;bash&#34;,&#34;zsh&#34;], action=self._store_type, help=&#34;Get terminal completion configuration for conn&#34;) configcrud.add_argument(&#34;--completion&#34;, dest=&#34;completion&#34;, nargs=1, choices=[&#34;bash&#34;,&#34;zsh&#34;], action=self._store_type, help=&#34;Get terminal completion configuration for conn&#34;)
configcrud.add_argument(&#34;--configfolder&#34;, dest=&#34;configfolder&#34;, nargs=1, action=self._store_type, help=&#34;Set the default location for config file&#34;, metavar=&#34;FOLDER&#34;)
configparser.set_defaults(func=self._func_others) configparser.set_defaults(func=self._func_others)
#Manage sys arguments #Manage sys arguments
commands = [&#34;node&#34;, &#34;profile&#34;, &#34;mv&#34;, &#34;move&#34;,&#34;copy&#34;, &#34;cp&#34;, &#34;bulk&#34;, &#34;ls&#34;, &#34;list&#34;, &#34;run&#34;, &#34;config&#34;] commands = [&#34;node&#34;, &#34;profile&#34;, &#34;mv&#34;, &#34;move&#34;,&#34;copy&#34;, &#34;cp&#34;, &#34;bulk&#34;, &#34;ls&#34;, &#34;list&#34;, &#34;run&#34;, &#34;config&#34;, &#34;api&#34;]
profilecmds = [&#34;--add&#34;, &#34;-a&#34;, &#34;--del&#34;, &#34;--rm&#34;, &#34;-r&#34;, &#34;--mod&#34;, &#34;--edit&#34;, &#34;-e&#34;, &#34;--show&#34;, &#34;-s&#34;] profilecmds = [&#34;--add&#34;, &#34;-a&#34;, &#34;--del&#34;, &#34;--rm&#34;, &#34;-r&#34;, &#34;--mod&#34;, &#34;--edit&#34;, &#34;-e&#34;, &#34;--show&#34;, &#34;-s&#34;]
if len(argv) &gt;= 2 and argv[1] == &#34;profile&#34; and argv[0] in profilecmds: if len(argv) &gt;= 2 and argv[1] == &#34;profile&#34; and argv[0] in profilecmds:
argv[1] = argv[0] argv[1] = argv[0]
@ -964,7 +1122,7 @@ __pdoc__ = {
if matches[0] == &#34;default&#34;: if matches[0] == &#34;default&#34;:
print(&#34;Can&#39;t delete default profile&#34;) print(&#34;Can&#39;t delete default profile&#34;)
exit(6) exit(6)
usedprofile = self._profileused(matches[0]) usedprofile = self.config._profileused(matches[0])
if len(usedprofile) &gt; 0: if len(usedprofile) &gt; 0:
print(&#34;Profile {} used in the following nodes:&#34;.format(matches[0])) print(&#34;Profile {} used in the following nodes:&#34;.format(matches[0]))
print(&#34;, &#34;.join(usedprofile)) print(&#34;, &#34;.join(usedprofile))
@ -1026,7 +1184,7 @@ __pdoc__ = {
def _func_others(self, args): def _func_others(self, args):
#Function called when using other commands #Function called when using other commands
actions = {&#34;ls&#34;: self._ls, &#34;move&#34;: self._mvcp, &#34;cp&#34;: self._mvcp, &#34;bulk&#34;: self._bulk, &#34;completion&#34;: self._completion, &#34;case&#34;: self._case, &#34;fzf&#34;: self._fzf, &#34;idletime&#34;: self._idletime} actions = {&#34;ls&#34;: self._ls, &#34;move&#34;: self._mvcp, &#34;cp&#34;: self._mvcp, &#34;bulk&#34;: self._bulk, &#34;completion&#34;: self._completion, &#34;case&#34;: self._case, &#34;fzf&#34;: self._fzf, &#34;idletime&#34;: self._idletime, &#34;configfolder&#34;: self._configfolder}
return actions.get(args.command)(args) return actions.get(args.command)(args)
def _ls(self, args): def _ls(self, args):
@ -1100,7 +1258,7 @@ __pdoc__ = {
newnode[&#34;password&#34;] = newnodes[&#34;password&#34;] newnode[&#34;password&#34;] = newnodes[&#34;password&#34;]
count +=1 count +=1
self.config._connections_add(**newnode) self.config._connections_add(**newnode)
self.nodes = self._getallnodes() self.nodes = self.config._getallnodes()
if count &gt; 0: if count &gt; 0:
self.config._saveconfig(self.config.file) self.config._saveconfig(self.config.file)
print(&#34;Succesfully added {} nodes&#34;.format(count)) print(&#34;Succesfully added {} nodes&#34;.format(count))
@ -1132,6 +1290,16 @@ __pdoc__ = {
args.data[0] = 0 args.data[0] = 0
self._change_settings(args.command, args.data[0]) self._change_settings(args.command, args.data[0])
def _configfolder(self, args):
if not os.path.isdir(args.data[0]):
raise argparse.ArgumentTypeError(f&#34;readable_dir:{args.data[0]} is not a valid path&#34;)
else:
pathfile = defaultdir + &#34;/.folder&#34;
folder = os.path.abspath(args.data[0]).rstrip(&#39;/&#39;)
with open(pathfile, &#34;w&#34;) as f:
f.write(str(folder))
print(&#34;Config saved&#34;)
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)
@ -1143,6 +1311,13 @@ __pdoc__ = {
actions = {&#34;noderun&#34;: self._node_run, &#34;generate&#34;: self._yaml_generate, &#34;run&#34;: self._yaml_run} actions = {&#34;noderun&#34;: self._node_run, &#34;generate&#34;: self._yaml_generate, &#34;run&#34;: self._yaml_run}
return actions.get(args.action)(args) return actions.get(args.action)(args)
def _func_api(self, args):
if args.command == &#34;stop&#34; or args.command == &#34;restart&#34;:
stop_api()
if args.command == &#34;start&#34; or args.command == &#34;restart&#34;:
start_api()
return
def _node_run(self, args): def _node_run(self, args):
command = &#34; &#34;.join(args.data[1:]) command = &#34; &#34;.join(args.data[1:])
command = command.split(&#34;-&#34;) command = command.split(&#34;-&#34;)
@ -1533,7 +1708,7 @@ __pdoc__ = {
if type == &#34;usage&#34;: if type == &#34;usage&#34;:
return &#34;conn [-h] [--add | --del | --mod | --show | --debug] [node|folder]\n conn {profile,move,mv,copy,cp,list,ls,bulk,config} ...&#34; return &#34;conn [-h] [--add | --del | --mod | --show | --debug] [node|folder]\n conn {profile,move,mv,copy,cp,list,ls,bulk,config} ...&#34;
if type == &#34;end&#34;: if type == &#34;end&#34;:
return &#34;Commands:\n profile Manage profiles\n move (mv) Move node\n copy (cp) Copy node\n list (ls) List profiles, nodes or folders\n bulk Add nodes in bulk\n run Run scripts or commands on nodes\n config Manage app config&#34; return &#34;Commands:\n profile Manage profiles\n move (mv) Move node\n copy (cp) Copy node\n list (ls) List profiles, nodes or folders\n bulk Add nodes in bulk\n run Run scripts or commands on nodes\n config Manage app config\n api Start and stop connpy api&#34;
if type == &#34;bashcompletion&#34;: if type == &#34;bashcompletion&#34;:
return &#39;&#39;&#39; return &#39;&#39;&#39;
#Here starts bash completion for conn #Here starts bash completion for conn
@ -1633,46 +1808,6 @@ tasks:
output: null output: null
...&#39;&#39;&#39; ...&#39;&#39;&#39;
def _getallnodes(self):
#get all nodes on configfile
nodes = []
layer1 = [k for k,v in self.config.connections.items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;connection&#34;]
folders = [k for k,v in self.config.connections.items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;folder&#34;]
nodes.extend(layer1)
for f in folders:
layer2 = [k + &#34;@&#34; + f for k,v in self.config.connections[f].items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;connection&#34;]
nodes.extend(layer2)
subfolders = [k for k,v in self.config.connections[f].items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;subfolder&#34;]
for s in subfolders:
layer3 = [k + &#34;@&#34; + s + &#34;@&#34; + f for k,v in self.config.connections[f][s].items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;connection&#34;]
nodes.extend(layer3)
return nodes
def _getallfolders(self):
#get all folders on configfile
folders = [&#34;@&#34; + k for k,v in self.config.connections.items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;folder&#34;]
subfolders = []
for f in folders:
s = [&#34;@&#34; + k + f for k,v in self.config.connections[f[1:]].items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;subfolder&#34;]
subfolders.extend(s)
folders.extend(subfolders)
return folders
def _profileused(self, profile):
#Check if profile is used before deleting it
nodes = []
layer1 = [k for k,v in self.config.connections.items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;connection&#34; and (&#34;@&#34; + profile in v.values() or ( isinstance(v[&#34;password&#34;],list) and &#34;@&#34; + profile in v[&#34;password&#34;]))]
folders = [k for k,v in self.config.connections.items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;folder&#34;]
nodes.extend(layer1)
for f in folders:
layer2 = [k + &#34;@&#34; + f for k,v in self.config.connections[f].items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;connection&#34; and (&#34;@&#34; + profile in v.values() or ( isinstance(v[&#34;password&#34;],list) and &#34;@&#34; + profile in v[&#34;password&#34;]))]
nodes.extend(layer2)
subfolders = [k for k,v in self.config.connections[f].items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;subfolder&#34;]
for s in subfolders:
layer3 = [k + &#34;@&#34; + s + &#34;@&#34; + f for k,v in self.config.connections[f][s].items() if isinstance(v, dict) and v[&#34;type&#34;] == &#34;connection&#34; and (&#34;@&#34; + profile in v.values() or ( isinstance(v[&#34;password&#34;],list) and &#34;@&#34; + profile in v[&#34;password&#34;]))]
nodes.extend(layer3)
return nodes
def encrypt(self, password, keyfile=None): def encrypt(self, password, keyfile=None):
&#39;&#39;&#39; &#39;&#39;&#39;
Encrypts password using RSA keyfile Encrypts password using RSA keyfile
@ -1751,7 +1886,7 @@ tasks:
</details> </details>
</dd> </dd>
<dt id="connpy.connapp.start"><code class="name flex"> <dt id="connpy.connapp.start"><code class="name flex">
<span>def <span class="ident">start</span></span>(<span>self, argv=['-f', '-o', 'docs/', '--html', 'connpy'])</span> <span>def <span class="ident">start</span></span>(<span>self, argv=['--html', 'connpy', '-o', 'docs', '-f'])</span>
</code></dt> </code></dt>
<dd> <dd>
<div class="desc"><h3 id="parameters">Parameters:</h3> <div class="desc"><h3 id="parameters">Parameters:</h3>
@ -1815,6 +1950,13 @@ tasks:
runparser.add_argument(&#34;run&#34;, nargs=&#39;+&#39;, action=self._store_type, help=self._help(&#34;run&#34;), default=&#34;run&#34;) runparser.add_argument(&#34;run&#34;, nargs=&#39;+&#39;, action=self._store_type, help=self._help(&#34;run&#34;), default=&#34;run&#34;)
runparser.add_argument(&#34;-g&#34;,&#34;--generate&#34;, dest=&#34;action&#34;, action=&#34;store_const&#34;, help=&#34;Generate yaml file template&#34;, const=&#34;generate&#34;, default=&#34;run&#34;) runparser.add_argument(&#34;-g&#34;,&#34;--generate&#34;, dest=&#34;action&#34;, action=&#34;store_const&#34;, help=&#34;Generate yaml file template&#34;, const=&#34;generate&#34;, default=&#34;run&#34;)
runparser.set_defaults(func=self._func_run) runparser.set_defaults(func=self._func_run)
#APIPARSER
apiparser = subparsers.add_parser(&#34;api&#34;, help=&#34;Start and stop connpy api&#34;)
apicrud = apiparser.add_mutually_exclusive_group(required=True)
apicrud.add_argument(&#34;--start&#34;, dest=&#34;start&#34;, nargs=0, action=self._store_type, help=&#34;Start conppy api&#34;)
apicrud.add_argument(&#34;--restart&#34;, dest=&#34;restart&#34;, nargs=0, action=self._store_type, help=&#34;Restart conppy api&#34;)
apicrud.add_argument(&#34;--stop&#34;, dest=&#34;stop&#34;, nargs=0, action=self._store_type, help=&#34;Stop conppy api&#34;)
apiparser.set_defaults(func=self._func_api)
#CONFIGPARSER #CONFIGPARSER
configparser = subparsers.add_parser(&#34;config&#34;, help=&#34;Manage app config&#34;) configparser = subparsers.add_parser(&#34;config&#34;, help=&#34;Manage app config&#34;)
configcrud = configparser.add_mutually_exclusive_group(required=True) configcrud = configparser.add_mutually_exclusive_group(required=True)
@ -1822,9 +1964,10 @@ tasks:
configcrud.add_argument(&#34;--fzf&#34;, dest=&#34;fzf&#34;, nargs=1, action=self._store_type, help=&#34;Use fzf for lists&#34;, choices=[&#34;true&#34;,&#34;false&#34;]) configcrud.add_argument(&#34;--fzf&#34;, dest=&#34;fzf&#34;, nargs=1, action=self._store_type, help=&#34;Use fzf for lists&#34;, choices=[&#34;true&#34;,&#34;false&#34;])
configcrud.add_argument(&#34;--keepalive&#34;, dest=&#34;idletime&#34;, nargs=1, action=self._store_type, help=&#34;Set keepalive time in seconds, 0 to disable&#34;, type=int, metavar=&#34;INT&#34;) configcrud.add_argument(&#34;--keepalive&#34;, dest=&#34;idletime&#34;, nargs=1, action=self._store_type, help=&#34;Set keepalive time in seconds, 0 to disable&#34;, type=int, metavar=&#34;INT&#34;)
configcrud.add_argument(&#34;--completion&#34;, dest=&#34;completion&#34;, nargs=1, choices=[&#34;bash&#34;,&#34;zsh&#34;], action=self._store_type, help=&#34;Get terminal completion configuration for conn&#34;) configcrud.add_argument(&#34;--completion&#34;, dest=&#34;completion&#34;, nargs=1, choices=[&#34;bash&#34;,&#34;zsh&#34;], action=self._store_type, help=&#34;Get terminal completion configuration for conn&#34;)
configcrud.add_argument(&#34;--configfolder&#34;, dest=&#34;configfolder&#34;, nargs=1, action=self._store_type, help=&#34;Set the default location for config file&#34;, metavar=&#34;FOLDER&#34;)
configparser.set_defaults(func=self._func_others) configparser.set_defaults(func=self._func_others)
#Manage sys arguments #Manage sys arguments
commands = [&#34;node&#34;, &#34;profile&#34;, &#34;mv&#34;, &#34;move&#34;,&#34;copy&#34;, &#34;cp&#34;, &#34;bulk&#34;, &#34;ls&#34;, &#34;list&#34;, &#34;run&#34;, &#34;config&#34;] commands = [&#34;node&#34;, &#34;profile&#34;, &#34;mv&#34;, &#34;move&#34;,&#34;copy&#34;, &#34;cp&#34;, &#34;bulk&#34;, &#34;ls&#34;, &#34;list&#34;, &#34;run&#34;, &#34;config&#34;, &#34;api&#34;]
profilecmds = [&#34;--add&#34;, &#34;-a&#34;, &#34;--del&#34;, &#34;--rm&#34;, &#34;-r&#34;, &#34;--mod&#34;, &#34;--edit&#34;, &#34;-e&#34;, &#34;--show&#34;, &#34;-s&#34;] profilecmds = [&#34;--add&#34;, &#34;-a&#34;, &#34;--del&#34;, &#34;--rm&#34;, &#34;-r&#34;, &#34;--mod&#34;, &#34;--edit&#34;, &#34;-e&#34;, &#34;--show&#34;, &#34;-s&#34;]
if len(argv) &gt;= 2 and argv[1] == &#34;profile&#34; and argv[0] in profilecmds: if len(argv) &gt;= 2 and argv[1] == &#34;profile&#34; and argv[0] in profilecmds:
argv[1] = argv[0] argv[1] = argv[0]
@ -3180,6 +3323,19 @@ tasks:
<li><a href="#examples">Examples</a></li> <li><a href="#examples">Examples</a></li>
</ul> </ul>
</li> </li>
<li><a href="#http-api">http API</a><ul>
<li><a href="#1-list-nodes">1. List Nodes</a><ul>
<li><a href="#request-body">Request Body:</a></li>
<li><a href="#response">Response:</a></li>
</ul>
</li>
<li><a href="#2-run-commands">2. Run Commands</a><ul>
<li><a href="#request-body_1">Request Body:</a></li>
<li><a href="#response_1">Response:</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#automation-module">Automation module</a><ul> <li><a href="#automation-module">Automation module</a><ul>
<li><a href="#standalone-module">Standalone module</a></li> <li><a href="#standalone-module">Standalone module</a></li>
<li><a href="#using-manager-configuration">Using manager configuration</a></li> <li><a href="#using-manager-configuration">Using manager configuration</a></li>