diff --git a/connpy/__init__.py b/connpy/__init__.py index 220919f..49be761 100644 --- a/connpy/__init__.py +++ b/connpy/__init__.py @@ -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": "" +} +``` + +* `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": "", + "nodes": "", + "commands": "", + "expected": "", + "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 } diff --git a/connpy/api.py b/connpy/api.py index 8bb8650..b1fd089 100644 --- a/connpy/api.py +++ b/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: - pid = int(f.read().strip()) - + 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: - f.write(str(pid)) - print(f'Server is running') + try: + with open(PID_FILE1, "w") as f: + 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}.') diff --git a/connpy/configfile.py b/connpy/configfile.py index 03c6313..8a3c519 100755 --- a/connpy/configfile.py +++ b/connpy/configfile.py @@ -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 + diff --git a/connpy/connapp.py b/connpy/connapp.py index f948337..03656d1 100755 --- a/connpy/connapp.py +++ b/connpy/connapp.py @@ -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 diff --git a/docs/connpy/index.html b/docs/connpy/index.html index 03f9102..3aa3d51 100644 --- a/docs/connpy/index.html +++ b/docs/connpy/index.html @@ -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

Manage profiles

usage: conn profile [-h] (--add | --del | --mod | --show) profile
@@ -85,6 +86,50 @@ 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:

+
{
+  "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:

+
{
+  "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

Standalone module

@@ -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 } @@ -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] + 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

Methods

@@ -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:
-def start(self, argv=['-f', '-o', 'docs/', '--html', 'connpy']) +def start(self, argv=['--html', 'connpy', '-o', 'docs', '-f'])

Parameters:

@@ -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:
  • Examples
  • +
  • http API +
  • Automation module