add api
This commit is contained in:
parent
68b63baeac
commit
51bdc4e59a
@ -41,6 +41,7 @@ Commands:
|
||||
bulk Add nodes in bulk
|
||||
run Run scripts or commands on nodes
|
||||
config Manage app config
|
||||
api Start and stop connpy api
|
||||
```
|
||||
|
||||
### Manage profiles
|
||||
@ -70,7 +71,62 @@ options:
|
||||
conn pc@office
|
||||
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
|
||||
the automation module
|
||||
|
||||
@ -157,4 +213,5 @@ __author__ = "Federico Luzzi"
|
||||
__pdoc__ = {
|
||||
'core': False,
|
||||
'completion': False,
|
||||
'api': False
|
||||
}
|
||||
|
152
connpy/api.py
152
connpy/api.py
@ -7,77 +7,139 @@ import signal
|
||||
app = Flask(__name__)
|
||||
conf = configfile()
|
||||
|
||||
PID_FILE = ".connpy_server.pid"
|
||||
PID_FILE1 = "/run/connpy.pid"
|
||||
PID_FILE2 = "/tmp/connpy.pid"
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def hello():
|
||||
return "Welcome to the Connpy API!"
|
||||
def root():
|
||||
return jsonify({
|
||||
'message': 'Welcome to Connpy api',
|
||||
'version': '1.0',
|
||||
'documentation': 'https://fluzzi.github.io/connpy/'
|
||||
})
|
||||
|
||||
@app.route("/add_node", methods=["POST"])
|
||||
def add_node():
|
||||
node_data = request.get_json()
|
||||
unique = node_data["unique"]
|
||||
host = node_data["host"]
|
||||
user = node_data["user"]
|
||||
password = node_data["password"]
|
||||
|
||||
conf.add(unique, host=host, user=user, password=password)
|
||||
|
||||
return jsonify({"message": f"Node {unique} added successfully"})
|
||||
@app.route("/list_nodes", methods=["POST"])
|
||||
def list_nodes():
|
||||
conf = app.custom_config
|
||||
output = conf._getallnodes()
|
||||
case = conf.config["case"]
|
||||
try:
|
||||
data = request.get_json()
|
||||
filter = data["filter"]
|
||||
if not case:
|
||||
filter = filter.lower()
|
||||
output = [item for item in output if filter in item]
|
||||
except:
|
||||
pass
|
||||
return jsonify(output)
|
||||
|
||||
@app.route("/run_commands", methods=["POST"])
|
||||
def run_commands():
|
||||
conf = app.custom_config
|
||||
data = request.get_json()
|
||||
unique = data["unique"]
|
||||
commands = data["commands"]
|
||||
node_data = conf.getitem(unique)
|
||||
conn_node = node(unique,**node_data, config=conf)
|
||||
|
||||
output = conn_node.run(commands)
|
||||
case = conf.config["case"]
|
||||
mynodes = {}
|
||||
args = {}
|
||||
try:
|
||||
action = data["action"]
|
||||
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
|
||||
|
||||
@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():
|
||||
# Read the process ID (pid) from the file
|
||||
with open(PID_FILE, "r") as f:
|
||||
try:
|
||||
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
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
|
||||
# Delete the PID file
|
||||
os.remove(PID_FILE)
|
||||
|
||||
print(f"Server with process ID {pid} stopped.")
|
||||
|
||||
def start_server(folder):
|
||||
folder = folder.rstrip('/')
|
||||
file = folder + '/config.json'
|
||||
key = folder + '/.osk'
|
||||
app.custom_config = configfile(file, key)
|
||||
def start_server():
|
||||
app.custom_config = configfile()
|
||||
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()
|
||||
if pid == 0:
|
||||
start_server(folder)
|
||||
start_server()
|
||||
else:
|
||||
with open(PID_FILE, "w") as f:
|
||||
try:
|
||||
with open(PID_FILE1, "w") as f:
|
||||
f.write(str(pid))
|
||||
print(f'Server is running')
|
||||
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}.')
|
||||
|
||||
|
@ -49,9 +49,17 @@ class configfile:
|
||||
'''
|
||||
home = os.path.expanduser("~")
|
||||
defaultdir = home + '/.config/conn'
|
||||
defaultfile = defaultdir + '/config.json'
|
||||
defaultkey = defaultdir + '/.osk'
|
||||
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:
|
||||
self.file = defaultfile
|
||||
else:
|
||||
@ -233,3 +241,44 @@ class configfile:
|
||||
def _profiles_del(self,*, id ):
|
||||
#Delete profile from config
|
||||
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
|
||||
|
||||
|
@ -40,8 +40,8 @@ class connapp:
|
||||
self.node = node
|
||||
self.connnodes = nodes
|
||||
self.config = config
|
||||
self.nodes = self._getallnodes()
|
||||
self.folders = self._getallfolders()
|
||||
self.nodes = self.config._getallnodes()
|
||||
self.folders = self.config._getallfolders()
|
||||
self.profiles = list(self.config.profiles.keys())
|
||||
self.case = self.config.config["case"]
|
||||
try:
|
||||
@ -106,7 +106,8 @@ class connapp:
|
||||
#APIPARSER
|
||||
apiparser = subparsers.add_parser("api", help="Start and stop connpy api")
|
||||
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")
|
||||
apiparser.set_defaults(func=self._func_api)
|
||||
#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("--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("--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)
|
||||
#Manage sys arguments
|
||||
commands = ["node", "profile", "mv", "move","copy", "cp", "bulk", "ls", "list", "run", "config", "api"]
|
||||
@ -307,7 +309,7 @@ class connapp:
|
||||
if matches[0] == "default":
|
||||
print("Can't delete default profile")
|
||||
exit(6)
|
||||
usedprofile = self._profileused(matches[0])
|
||||
usedprofile = self.config._profileused(matches[0])
|
||||
if len(usedprofile) > 0:
|
||||
print("Profile {} used in the following nodes:".format(matches[0]))
|
||||
print(", ".join(usedprofile))
|
||||
@ -369,7 +371,7 @@ class connapp:
|
||||
|
||||
def _func_others(self, args):
|
||||
#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)
|
||||
|
||||
def _ls(self, args):
|
||||
@ -443,7 +445,7 @@ class connapp:
|
||||
newnode["password"] = newnodes["password"]
|
||||
count +=1
|
||||
self.config._connections_add(**newnode)
|
||||
self.nodes = self._getallnodes()
|
||||
self.nodes = self.config._getallnodes()
|
||||
if count > 0:
|
||||
self.config._saveconfig(self.config.file)
|
||||
print("Succesfully added {} nodes".format(count))
|
||||
@ -475,6 +477,16 @@ class connapp:
|
||||
args.data[0] = 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):
|
||||
self.config.config[name] = value
|
||||
self.config._saveconfig(self.config.file)
|
||||
@ -487,15 +499,10 @@ class connapp:
|
||||
return actions.get(args.action)(args)
|
||||
|
||||
def _func_api(self, args):
|
||||
if args.command == "start":
|
||||
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":
|
||||
if args.command == "stop" or args.command == "restart":
|
||||
stop_api()
|
||||
if args.command == "start" or args.command == "restart":
|
||||
start_api()
|
||||
return
|
||||
|
||||
def _node_run(self, args):
|
||||
@ -888,7 +895,7 @@ class connapp:
|
||||
if type == "usage":
|
||||
return "conn [-h] [--add | --del | --mod | --show | --debug] [node|folder]\n conn {profile,move,mv,copy,cp,list,ls,bulk,config} ..."
|
||||
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":
|
||||
return '''
|
||||
#Here starts bash completion for conn
|
||||
@ -988,46 +995,6 @@ tasks:
|
||||
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):
|
||||
'''
|
||||
Encrypts password using RSA keyfile
|
||||
|
@ -60,6 +60,7 @@ Commands:
|
||||
bulk Add nodes in bulk
|
||||
run Run scripts or commands on nodes
|
||||
config Manage app config
|
||||
api Start and stop connpy api
|
||||
</code></pre>
|
||||
<h3 id="manage-profiles">Manage profiles</h3>
|
||||
<pre><code>usage: conn profile [-h] (--add | --del | --mod | --show) profile
|
||||
@ -85,6 +86,50 @@ options:
|
||||
conn pc@office
|
||||
conn server
|
||||
</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">{
|
||||
"filter": "<keyword>"
|
||||
}
|
||||
</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">{
|
||||
"action": "<action>",
|
||||
"nodes": "<nodes>",
|
||||
"commands": "<commands>",
|
||||
"expected": "<expected>",
|
||||
"options": "<options>"
|
||||
}
|
||||
</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>
|
||||
<p>the automation module</p>
|
||||
<h3 id="standalone-module">Standalone module</h3>
|
||||
@ -200,6 +245,7 @@ Commands:
|
||||
bulk Add nodes in bulk
|
||||
run Run scripts or commands on nodes
|
||||
config Manage app config
|
||||
api Start and stop connpy api
|
||||
```
|
||||
|
||||
### Manage profiles
|
||||
@ -229,7 +275,62 @@ options:
|
||||
conn pc@office
|
||||
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
|
||||
the automation module
|
||||
|
||||
@ -316,6 +417,7 @@ __author__ = "Federico Luzzi"
|
||||
__pdoc__ = {
|
||||
'core': False,
|
||||
'completion': False,
|
||||
'api': False
|
||||
}</code></pre>
|
||||
</details>
|
||||
</section>
|
||||
@ -404,9 +506,17 @@ __pdoc__ = {
|
||||
'''
|
||||
home = os.path.expanduser("~")
|
||||
defaultdir = home + '/.config/conn'
|
||||
defaultfile = defaultdir + '/config.json'
|
||||
defaultkey = defaultdir + '/.osk'
|
||||
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:
|
||||
self.file = defaultfile
|
||||
else:
|
||||
@ -587,7 +697,47 @@ __pdoc__ = {
|
||||
|
||||
def _profiles_del(self,*, id ):
|
||||
#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["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</code></pre>
|
||||
</details>
|
||||
<h3>Methods</h3>
|
||||
<dl>
|
||||
@ -703,8 +853,8 @@ __pdoc__ = {
|
||||
self.node = node
|
||||
self.connnodes = nodes
|
||||
self.config = config
|
||||
self.nodes = self._getallnodes()
|
||||
self.folders = self._getallfolders()
|
||||
self.nodes = self.config._getallnodes()
|
||||
self.folders = self.config._getallfolders()
|
||||
self.profiles = list(self.config.profiles.keys())
|
||||
self.case = self.config.config["case"]
|
||||
try:
|
||||
@ -766,6 +916,13 @@ __pdoc__ = {
|
||||
runparser.add_argument("run", nargs='+', action=self._store_type, help=self._help("run"), default="run")
|
||||
runparser.add_argument("-g","--generate", dest="action", action="store_const", help="Generate yaml file template", const="generate", default="run")
|
||||
runparser.set_defaults(func=self._func_run)
|
||||
#APIPARSER
|
||||
apiparser = subparsers.add_parser("api", help="Start and stop connpy api")
|
||||
apicrud = apiparser.add_mutually_exclusive_group(required=True)
|
||||
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")
|
||||
apiparser.set_defaults(func=self._func_api)
|
||||
#CONFIGPARSER
|
||||
configparser = subparsers.add_parser("config", help="Manage app config")
|
||||
configcrud = configparser.add_mutually_exclusive_group(required=True)
|
||||
@ -773,9 +930,10 @@ __pdoc__ = {
|
||||
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("--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)
|
||||
#Manage sys arguments
|
||||
commands = ["node", "profile", "mv", "move","copy", "cp", "bulk", "ls", "list", "run", "config"]
|
||||
commands = ["node", "profile", "mv", "move","copy", "cp", "bulk", "ls", "list", "run", "config", "api"]
|
||||
profilecmds = ["--add", "-a", "--del", "--rm", "-r", "--mod", "--edit", "-e", "--show", "-s"]
|
||||
if len(argv) >= 2 and argv[1] == "profile" and argv[0] in profilecmds:
|
||||
argv[1] = argv[0]
|
||||
@ -964,7 +1122,7 @@ __pdoc__ = {
|
||||
if matches[0] == "default":
|
||||
print("Can't delete default profile")
|
||||
exit(6)
|
||||
usedprofile = self._profileused(matches[0])
|
||||
usedprofile = self.config._profileused(matches[0])
|
||||
if len(usedprofile) > 0:
|
||||
print("Profile {} used in the following nodes:".format(matches[0]))
|
||||
print(", ".join(usedprofile))
|
||||
@ -1026,7 +1184,7 @@ __pdoc__ = {
|
||||
|
||||
def _func_others(self, args):
|
||||
#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)
|
||||
|
||||
def _ls(self, args):
|
||||
@ -1100,7 +1258,7 @@ __pdoc__ = {
|
||||
newnode["password"] = newnodes["password"]
|
||||
count +=1
|
||||
self.config._connections_add(**newnode)
|
||||
self.nodes = self._getallnodes()
|
||||
self.nodes = self.config._getallnodes()
|
||||
if count > 0:
|
||||
self.config._saveconfig(self.config.file)
|
||||
print("Succesfully added {} nodes".format(count))
|
||||
@ -1132,6 +1290,16 @@ __pdoc__ = {
|
||||
args.data[0] = 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):
|
||||
self.config.config[name] = value
|
||||
self.config._saveconfig(self.config.file)
|
||||
@ -1143,6 +1311,13 @@ __pdoc__ = {
|
||||
actions = {"noderun": self._node_run, "generate": self._yaml_generate, "run": self._yaml_run}
|
||||
return actions.get(args.action)(args)
|
||||
|
||||
def _func_api(self, args):
|
||||
if args.command == "stop" or args.command == "restart":
|
||||
stop_api()
|
||||
if args.command == "start" or args.command == "restart":
|
||||
start_api()
|
||||
return
|
||||
|
||||
def _node_run(self, args):
|
||||
command = " ".join(args.data[1:])
|
||||
command = command.split("-")
|
||||
@ -1533,7 +1708,7 @@ __pdoc__ = {
|
||||
if type == "usage":
|
||||
return "conn [-h] [--add | --del | --mod | --show | --debug] [node|folder]\n conn {profile,move,mv,copy,cp,list,ls,bulk,config} ..."
|
||||
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":
|
||||
return '''
|
||||
#Here starts bash completion for conn
|
||||
@ -1633,46 +1808,6 @@ tasks:
|
||||
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):
|
||||
'''
|
||||
Encrypts password using RSA keyfile
|
||||
@ -1751,7 +1886,7 @@ tasks:
|
||||
</details>
|
||||
</dd>
|
||||
<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>
|
||||
<dd>
|
||||
<div class="desc"><h3 id="parameters">Parameters:</h3>
|
||||
@ -1815,6 +1950,13 @@ tasks:
|
||||
runparser.add_argument("run", nargs='+', action=self._store_type, help=self._help("run"), default="run")
|
||||
runparser.add_argument("-g","--generate", dest="action", action="store_const", help="Generate yaml file template", const="generate", default="run")
|
||||
runparser.set_defaults(func=self._func_run)
|
||||
#APIPARSER
|
||||
apiparser = subparsers.add_parser("api", help="Start and stop connpy api")
|
||||
apicrud = apiparser.add_mutually_exclusive_group(required=True)
|
||||
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")
|
||||
apiparser.set_defaults(func=self._func_api)
|
||||
#CONFIGPARSER
|
||||
configparser = subparsers.add_parser("config", help="Manage app config")
|
||||
configcrud = configparser.add_mutually_exclusive_group(required=True)
|
||||
@ -1822,9 +1964,10 @@ tasks:
|
||||
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("--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)
|
||||
#Manage sys arguments
|
||||
commands = ["node", "profile", "mv", "move","copy", "cp", "bulk", "ls", "list", "run", "config"]
|
||||
commands = ["node", "profile", "mv", "move","copy", "cp", "bulk", "ls", "list", "run", "config", "api"]
|
||||
profilecmds = ["--add", "-a", "--del", "--rm", "-r", "--mod", "--edit", "-e", "--show", "-s"]
|
||||
if len(argv) >= 2 and argv[1] == "profile" and argv[0] in profilecmds:
|
||||
argv[1] = argv[0]
|
||||
@ -3180,6 +3323,19 @@ tasks:
|
||||
<li><a href="#examples">Examples</a></li>
|
||||
</ul>
|
||||
</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="#standalone-module">Standalone module</a></li>
|
||||
<li><a href="#using-manager-configuration">Using manager configuration</a></li>
|
||||
|
Loading…
Reference in New Issue
Block a user