From c2d6ee75887e9776a6a178d956a1243824e6b352 Mon Sep 17 00:00:00 2001 From: dako Date: Wed, 12 Jan 2022 06:40:56 -0500 Subject: [PATCH 01/27] update query to accept multiple librouteros ADN parameters --- plugins/modules/api.py | 85 +++++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index dc58ca50..8ca6cf6e 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -265,37 +265,33 @@ def __init__(self): self.update = self.module.params['update'] self.arbitrary = self.module.params['cmd'] - self.where = None + self.result = dict( + message=[]) + + self.where_list = None self.query = self.module.params['query'] if self.query: where_index = self.query.find(' WHERE ') if where_index < 0: self.query = self.split_params(self.query) else: + self.where_list = [] where = self.query[where_index + len(' WHERE '):] + # create a list of WHERE arguments + for wl in where.split(','): + # where must be of the format ' ' + m = re.match(r'^\s*([^ ]+)\s+([^ ]+)\s+(.*)$', wl) + if not m: + self.errors("invalid syntax for 'WHERE %s'" % wl) + try: + self.where_list.append([ + m.group(1), # attribute + m.group(2), # operator + parse_argument_value(m.group(3).rstrip())[0], # value + ]) + except ParseError as exc: + self.errors("invalid syntax for 'WHERE %s': %s" % (wl, exc)) self.query = self.split_params(self.query[:where_index]) - # where must be of the format ' ' - m = re.match(r'^\s*([^ ]+)\s+([^ ]+)\s+(.*)$', where) - if not m: - self.errors("invalid syntax for 'WHERE %s'" % where) - try: - self.where = [ - m.group(1), # attribute - m.group(2), # operator - parse_argument_value(m.group(3).rstrip())[0], # value - ] - except ParseError as exc: - self.errors("invalid syntax for 'WHERE %s': %s" % (where, exc)) - try: - idx = self.query.index('WHERE') - self.where = self.query[idx + 1:] - self.query = self.query[:idx] - except ValueError: - # Raised when WHERE has not been found - pass - - self.result = dict( - message=[]) # create api base path self.api_path = self.api_add_path(self.api, self.path) @@ -374,27 +370,40 @@ def api_query(self): self.errors("'%s' must be '.id'" % k) keys[k] = Key(k) try: - if self.where: - if self.where[1] == '==': - select = self.api_path.select(*keys).where(keys[self.where[0]] == self.where[2]) - elif self.where[1] == '!=': - select = self.api_path.select(*keys).where(keys[self.where[0]] != self.where[2]) - elif self.where[1] == '>': - select = self.api_path.select(*keys).where(keys[self.where[0]] > self.where[2]) - elif self.where[1] == '<': - select = self.api_path.select(*keys).where(keys[self.where[0]] < self.where[2]) - else: - self.errors("'%s' is not operator for 'where'" - % self.where[1]) + if self.where_list: + where_args = False + for wl in self.where_list: + if wl[1] == '==': + if where_args: + where_args = where_args + (keys[wl[0]] == wl[2],) + else: + where_args = (keys[wl[0]] == wl[2],) + elif wl[1] == '!=': + if where_args: + where_args = where_args + (keys[wl[0]] != wl[2],) + else: + where_args = (keys[wl[0]] != wl[2],) + elif wl[1] == '>': + if where_args: + where_args = where_args + (keys[wl[0]] > wl[2],) + else: + where_args = (keys[wl[0]] > wl[2],) + elif wl[1] == '<': + if where_args: + where_args = where_args + (keys[wl[0]] < wl[2],) + else: + where_args = (keys[wl[0]] < wl[2],) + else: + self.errors("'%s' is not operator for 'WHERE %s'" + % (wl[1],' '.join(wl))) + select = self.api_path.select(*keys).where(*where_args) else: select = self.api_path.select(*keys) for row in select: self.result['message'].append(row) if len(self.result['message']) < 1: msg = "no results for '%s 'query' %s" % (' '.join(self.path), - ' '.join(self.query)) - if self.where: - msg = msg + ' WHERE %s' % ' '.join(self.where) + self.module.params['query']) self.result['message'].append(msg) self.return_result(False) except LibRouterosError as e: From 3ba000b29363e9ae679fda5862522cf98d81081b Mon Sep 17 00:00:00 2001 From: "nikolay@adchev.info" Date: Tue, 18 Jan 2022 06:23:42 -0500 Subject: [PATCH 02/27] update query for new yml strucutre --- plugins/modules/api.py | 106 +++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 52 deletions(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index 8ca6cf6e..71916d65 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -246,7 +246,7 @@ def __init__(self): remove=dict(type='str'), update=dict(type='str'), cmd=dict(type='str'), - query=dict(type='str'), + query=dict(type='dict'), ) module_args.update(api_argument_spec()) @@ -268,30 +268,20 @@ def __init__(self): self.result = dict( message=[]) - self.where_list = None self.query = self.module.params['query'] if self.query: - where_index = self.query.find(' WHERE ') - if where_index < 0: - self.query = self.split_params(self.query) - else: - self.where_list = [] - where = self.query[where_index + len(' WHERE '):] - # create a list of WHERE arguments - for wl in where.split(','): - # where must be of the format ' ' - m = re.match(r'^\s*([^ ]+)\s+([^ ]+)\s+(.*)$', wl) - if not m: - self.errors("invalid syntax for 'WHERE %s'" % wl) - try: - self.where_list.append([ - m.group(1), # attribute - m.group(2), # operator - parse_argument_value(m.group(3).rstrip())[0], # value - ]) - except ParseError as exc: - self.errors("invalid syntax for 'WHERE %s': %s" % (wl, exc)) - self.query = self.split_params(self.query[:where_index]) + if "items" not in self.query.keys(): + self.errors("invalid 'query' syntax: missing 'items'") + if type(self.query["items"]) is not list: + self.errors("invalid 'query':'items' syntax: must be type list") + if "id" in self.query['items']: + self.errors("invalid 'query':'items' syntax: 'id' must be '.id'") + if "or" in self.query.keys() and "where" not in self.query.keys(): + self.errors("invalid 'query':'or' syntax: missing 'where'") + if "where" in self.query.keys(): + self.check_query('where') + if "or" in self.query.keys(): + self.check_query("or") # create api base path self.api_path = self.api_add_path(self.api, self.path) @@ -310,6 +300,18 @@ def __init__(self): else: self.api_get_all() + def check_query(self, check): + if type(self.query[check]) is not list: + self.errors("invalid 'query':'%s' syntax: must be type list" % check) + self.query_op = ["is", "not", "more", "less", "in"] + for w in self.query[check]: + for wk,wv in w.items(): + if wk not in self.query["items"]: + self.errors("invalid 'query':'%s' syntax: '%s' not in 'items': '%s'" % (check, wk, self.query["items"])) + for kwv in wv.keys(): + if kwv not in self.query_op: + self.errors("invalid 'query':'%s' syntax: '%s' for '%s' is not a valid operator" % (check, kwv, w)) + def list_to_dic(self, ldict): return convert_list_to_dictionary(ldict, skip_empty_values=True, require_assignment=True) @@ -365,37 +367,37 @@ def api_update(self): def api_query(self): keys = {} - for k in self.query: - if k == 'id': - self.errors("'%s' must be '.id'" % k) + for k in self.query['items']: keys[k] = Key(k) + where_args = False try: - if self.where_list: - where_args = False - for wl in self.where_list: - if wl[1] == '==': - if where_args: - where_args = where_args + (keys[wl[0]] == wl[2],) - else: - where_args = (keys[wl[0]] == wl[2],) - elif wl[1] == '!=': - if where_args: - where_args = where_args + (keys[wl[0]] != wl[2],) - else: - where_args = (keys[wl[0]] != wl[2],) - elif wl[1] == '>': - if where_args: - where_args = where_args + (keys[wl[0]] > wl[2],) - else: - where_args = (keys[wl[0]] > wl[2],) - elif wl[1] == '<': - if where_args: - where_args = where_args + (keys[wl[0]] < wl[2],) - else: - where_args = (keys[wl[0]] < wl[2],) - else: - self.errors("'%s' is not operator for 'WHERE %s'" - % (wl[1],' '.join(wl))) + if self.query['where']: + for wl in self.query['where']: + for k,v in wl.items(): + for kv,vv in v.items(): + if kv == 'is': + if where_args: + where_args = where_args + (keys[k] == vv,) + else: + where_args = (keys[k] == vv,) + elif kv == 'not': + if where_args: + where_args = where_args + (keys[k] != vv,) + else: + where_args = (keys[k] != vv,) + elif kv == 'less': + if where_args: + where_args = where_args + (keys[k] < vv,) + else: + where_args = (keys[k] < vv,) + elif kv == 'more': + if where_args: + where_args = where_args + (keys[k] > vv,) + else: + where_args = (keys[k] > vv,) + else: + self.errors("'%s' is not operator for 'where %s'" + % (kv,wl)) select = self.api_path.select(*keys).where(*where_args) else: select = self.api_path.select(*keys) From e4ca04cab73d37ebd749f220a37d37211796e481 Mon Sep 17 00:00:00 2001 From: Nikolay Dachev Date: Wed, 19 Jan 2022 04:56:22 -0500 Subject: [PATCH 03/27] add extended_query as separate function:(code in progress) --- plugins/modules/api.py | 228 ++++++++++++++++++++++++++++++----------- 1 file changed, 168 insertions(+), 60 deletions(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index 71916d65..efc62895 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -232,7 +232,7 @@ try: from librouteros.exceptions import LibRouterosError - from librouteros.query import Key + from librouteros.query import Key, Or except Exception: # Handled in api module_utils pass @@ -246,14 +246,15 @@ def __init__(self): remove=dict(type='str'), update=dict(type='str'), cmd=dict(type='str'), - query=dict(type='dict'), + query=dict(type='str'), + extended_query=dict(type='dict'), ) module_args.update(api_argument_spec()) self.module = AnsibleModule(argument_spec=module_args, supports_check_mode=False, mutually_exclusive=(('add', 'remove', 'update', - 'cmd', 'query'),),) + 'cmd', 'query', 'extended_query'),),) check_has_library(self.module) @@ -265,24 +266,13 @@ def __init__(self): self.update = self.module.params['update'] self.arbitrary = self.module.params['cmd'] + self.where = None + self.query = self.module.params['query'] + self.extended_query = self.module.params['extended_query'] + self.result = dict( message=[]) - self.query = self.module.params['query'] - if self.query: - if "items" not in self.query.keys(): - self.errors("invalid 'query' syntax: missing 'items'") - if type(self.query["items"]) is not list: - self.errors("invalid 'query':'items' syntax: must be type list") - if "id" in self.query['items']: - self.errors("invalid 'query':'items' syntax: 'id' must be '.id'") - if "or" in self.query.keys() and "where" not in self.query.keys(): - self.errors("invalid 'query':'or' syntax: missing 'where'") - if "where" in self.query.keys(): - self.check_query('where') - if "or" in self.query.keys(): - self.check_query("or") - # create api base path self.api_path = self.api_add_path(self.api, self.path) @@ -294,23 +284,70 @@ def __init__(self): elif self.update: self.api_update() elif self.query: + self.check_query() self.api_query() + elif self.extended_query: + self.check_extended_query() + self.api_extended_query() elif self.arbitrary: self.api_arbitrary() else: self.api_get_all() - def check_query(self, check): - if type(self.query[check]) is not list: - self.errors("invalid 'query':'%s' syntax: must be type list" % check) - self.query_op = ["is", "not", "more", "less", "in"] - for w in self.query[check]: - for wk,wv in w.items(): - if wk not in self.query["items"]: - self.errors("invalid 'query':'%s' syntax: '%s' not in 'items': '%s'" % (check, wk, self.query["items"])) - for kwv in wv.keys(): - if kwv not in self.query_op: - self.errors("invalid 'query':'%s' syntax: '%s' for '%s' is not a valid operator" % (check, kwv, w)) + def check_query(self): + where_index = self.query.find(' WHERE ') + if where_index < 0: + self.query = self.split_params(self.query) + else: + where = self.query[where_index + len(' WHERE '):] + self.query = self.split_params(self.query[:where_index]) + # where must be of the format ' ' + m = re.match(r'^\s*([^ ]+)\s+([^ ]+)\s+(.*)$', where) + if not m: + self.errors("invalid syntax for 'WHERE %s'" % where) + try: + self.where = [ + m.group(1), # attribute + m.group(2), # operator + parse_argument_value(m.group(3).rstrip())[0], # value + ] + except ParseError as exc: + self.errors("invalid syntax for 'WHERE %s': %s" % (where, exc)) + try: + idx = self.query.index('WHERE') + self.where = self.query[idx + 1:] + self.query = self.query[:idx] + except ValueError: + # Raised when WHERE has not been found + pass + + def check_extended_query(self): + if "items" not in self.extended_query.keys(): + self.errors("invalid 'query' syntax: missing 'items'") + if type(self.extended_query["items"]) is not list: + self.errors("invalid 'query':'items' syntax: must be type list") + if "id" in self.extended_query['items']: + self.errors("invalid 'query':'items' syntax: 'id' must be '.id'") + if "where" in self.extended_query.keys(): + check = "where" + if type(self.extended_query[check]) is not list: + self.errors("invalid 'query':'%s' syntax: must be type list" % check) + self.query_op = ["is", "not", "more", "less", "or", "in", "==", "!=", ">", "<"] + for w in self.extended_query[check]: + for wk,wv in w.items(): + if wk not in self.extended_query["items"]: + self.errors("invalid 'query':'%s' syntax: '%s' not in 'items': '%s'" % (check, wk, self.extended_query["items"])) + for kwv,wvv in wv.items(): + if kwv == "or": + continue + #or_index = self.extended_query[check].find(wk['or']) + #self.errors("%s %s" % (or_index, self.query[check].find(wv['or'])) + #self.check_query(wk) + #if type(wvv) is not list: + # self.errors("invalid 'query':'%s':'%s':'or':'%s' syntax: must be type list" % (check, wk, wvv)) + + if kwv not in self.query_op: + self.errors("invalid 'query':'%s' syntax: '%s' for '%s' is not a valid operator" % (check, kwv, w)) def list_to_dic(self, ldict): return convert_list_to_dictionary(ldict, skip_empty_values=True, require_assignment=True) @@ -367,45 +404,116 @@ def api_update(self): def api_query(self): keys = {} - for k in self.query['items']: + for k in self.query: + if 'id' in k and k != ".id": + self.errors("'%s' must be '.id'" % k) keys[k] = Key(k) - where_args = False try: - if self.query['where']: - for wl in self.query['where']: - for k,v in wl.items(): - for kv,vv in v.items(): - if kv == 'is': - if where_args: - where_args = where_args + (keys[k] == vv,) - else: - where_args = (keys[k] == vv,) - elif kv == 'not': - if where_args: - where_args = where_args + (keys[k] != vv,) - else: - where_args = (keys[k] != vv,) - elif kv == 'less': - if where_args: - where_args = where_args + (keys[k] < vv,) - else: - where_args = (keys[k] < vv,) - elif kv == 'more': - if where_args: - where_args = where_args + (keys[k] > vv,) - else: - where_args = (keys[k] > vv,) - else: - self.errors("'%s' is not operator for 'where %s'" - % (kv,wl)) - select = self.api_path.select(*keys).where(*where_args) + if self.where: + if self.where[1] == '==': + select = self.api_path.select(*keys).where(keys[self.where[0]] == self.where[2]) + elif self.where[1] == '!=': + select = self.api_path.select(*keys).where(keys[self.where[0]] != self.where[2]) + elif self.where[1] == '>': + select = self.api_path.select(*keys).where(keys[self.where[0]] > self.where[2]) + elif self.where[1] == '<': + select = self.api_path.select(*keys).where(keys[self.where[0]] < self.where[2]) + else: + self.errors("'%s' is not operator for 'where'" + % self.where[1]) else: select = self.api_path.select(*keys) for row in select: self.result['message'].append(row) if len(self.result['message']) < 1: msg = "no results for '%s 'query' %s" % (' '.join(self.path), - self.module.params['query']) + ' '.join(self.query)) + if self.where: + msg = msg + ' WHERE %s' % ' '.join(self.where) + self.result['message'].append(msg) + self.return_result(False) + except LibRouterosError as e: + self.errors(e) + + def build_api_extended_query(self, build_query): + where_args = False + for wl in build_query: + for k,v in wl.items(): + for kv,vv in v.items(): + # check 'or' items + if kv == 'or': + where_or_args = False + for ivv in vv: + for kivv,vivv in ivv.items(): + if kivv == 'is' or kivv == '==': + if where_or_args: + where_or_args = where_or_args + (self.query_keys[k] == vivv,) + else: + where_or_args = (self.query_keys[k] == vivv,) + elif kivv == 'not' or kivv == '!=': + if where_or_args: + where_or_args = where_or_args + (self.query_keys[k] != vivv,) + else: + where_or_args = (self.query_keys[k] != vivv,) + elif kivv == 'less' or kivv == '<': + if where_or_args: + where_or_args = where_or_args + (self.query_keys[k] < vivv,) + else: + where_or_args = (self.query_keys[k] < vivv,) + elif kivv == 'more' or kivv == '>': + if where_or_args: + where_or_args = where_or_args + (self.query_keys[k] > vivv,) + else: + where_or_args = (self.query_keys[k] > vivv,) + else: + self.errors("'%s' is not operator for '%s'" + % (kivv, ivv)) + if where_args: + where_args = where_args + (Or(*where_or_args),) + else: + where_args = (Or(*where_or_args),) + # check top itmes + elif kv == 'is' or kv == '==': + if where_args: + where_args = where_args + (self.query_keys[k] == vv,) + else: + where_args = (self.query_keys[k] == vv,) + elif kv == 'not'or kv == '!=': + if where_args: + where_args = where_args + (self.query_keys[k] != vv,) + else: + where_args = (self.query_keys[k] != vv,) + elif kv == 'less'or kv == '<': + if where_args: + where_args = where_args + (self.query_keys[k] < vv,) + else: + where_args = (self.query_keys[k] < vv,) + elif kv == 'more' or kv == '>': + if where_args: + where_args = where_args + (self.query_keys[k] > vv,) + else: + where_args = (self.query_keys[k] > vv,) + else: + self.errors("'%s' is not operator for '%s'" + % (kv,wl)) + return where_args + + def api_extended_query(self): + self.query_keys = {} + for k in self.extended_query['items']: + self.query_keys[k] = Key(k) + where_args = False + try: + if self.extended_query['where']: + where_args = self.build_api_extended_query(self.extended_query['where']) + select = self.api_path.select(*self.query_keys).where(*where_args) + else: + select = self.api_path.select(*self.query_keys) + for row in select: + self.result['message'].append(row) + if len(self.result['message']) < 1: + msg = "no results for '%s 'query' %s" % (' '.join(self.path), + self.module.params['extended_query']) self.result['message'].append(msg) self.return_result(False) except LibRouterosError as e: From 30235fae598d5d50f28605310ddf728b535e18d4 Mon Sep 17 00:00:00 2001 From: Nikolay Dachev Date: Thu, 20 Jan 2022 10:05:42 -0500 Subject: [PATCH 04/27] extended_query main code is ready for review --- plugins/modules/api.py | 155 ++++++++++++++++++++--------------------- 1 file changed, 74 insertions(+), 81 deletions(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index efc62895..a2e93cbb 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -52,7 +52,7 @@ description: - Query given path for selected query attributes from RouterOS aip. - WHERE is key word which extend query. WHERE format is key operator value - with spaces. - - WHERE valid operators are C(==), C(!=), C(>), C(<). + - WHERE valid operators are C(==) or C(is), C(!=) or C(not), C(>) or C(more), C(<) or C(less). - Example path C(ip address) and query C(.id address) will return only C(.id) and C(address) for all items in C(ip address) path. - Example path C(ip address) and query C(.id address WHERE address == 1.1.1.3/32). will return only C(.id) and C(address) for items in C(ip address) path, where address is eq to 1.1.1.3/32. @@ -60,6 +60,10 @@ return only interfaces C(mtu,name) where mtu is bigger than 1400. - Equivalent in RouterOS CLI C(/interface print where mtu > 1400). type: str + extended_query: + description: + - TODO + type: dict cmd: description: - Execute any/arbitrary command in selected path, after the command we can add C(.id). @@ -322,6 +326,8 @@ def check_query(self): pass def check_extended_query(self): + self.query_op_all = ["is", "not", "more", "less", "or", "in", "==", "!=", ">", "<"] + self.query_op_or = ["is", "not", "more", "less", "==", "!=", ">", "<"] if "items" not in self.extended_query.keys(): self.errors("invalid 'query' syntax: missing 'items'") if type(self.extended_query["items"]) is not list: @@ -332,22 +338,27 @@ def check_extended_query(self): check = "where" if type(self.extended_query[check]) is not list: self.errors("invalid 'query':'%s' syntax: must be type list" % check) - self.query_op = ["is", "not", "more", "less", "or", "in", "==", "!=", ">", "<"] + # we process nested list/dict structure + # for key,valu,item variables folow the loops. + # we start with 'w', for the next operation we add + # 'k' for key, 'v' for value, 'i' for item - at the end of the new variable for w in self.extended_query[check]: for wk,wv in w.items(): if wk not in self.extended_query["items"]: self.errors("invalid 'query':'%s' syntax: '%s' not in 'items': '%s'" % (check, wk, self.extended_query["items"])) - for kwv,wvv in wv.items(): - if kwv == "or": - continue - #or_index = self.extended_query[check].find(wk['or']) - #self.errors("%s %s" % (or_index, self.query[check].find(wv['or'])) - #self.check_query(wk) - #if type(wvv) is not list: - # self.errors("invalid 'query':'%s':'%s':'or':'%s' syntax: must be type list" % (check, wk, wvv)) - - if kwv not in self.query_op: - self.errors("invalid 'query':'%s' syntax: '%s' for '%s' is not a valid operator" % (check, kwv, w)) + for wvk,wvv in wv.items(): + if wvk not in self.query_op_all: + self.errors("invalid 'query':'%s' syntax: '%s' for '%s' is not a valid operator" % (check, wvk, w)) + if wvk == "in": + if type(wvv) is not list: + self.errors("invalid 'query':'%s':'%s':'%s' syntax: must be type list" % (check, wk, wvv)) + if wvk == "or": + if type(wvv) is not list: + self.errors("invalid 'query':'%s':'%s':'%s' syntax: must be type list" % (check, wk, wvv)) + for wvvi in wvv: + for wvvik,wvviv in wvvi.items(): + if wvvik not in self.query_op_or: + self.errors("invalid 'query':'%s':'%s':'%s' '%s' is not a valid operator" % (check, wk, wvk, wvvik)) def list_to_dic(self, ldict): return convert_list_to_dictionary(ldict, skip_empty_values=True, require_assignment=True) @@ -410,13 +421,13 @@ def api_query(self): keys[k] = Key(k) try: if self.where: - if self.where[1] == '==': + if self.where[1] == '==' or self.where[1] == 'is': select = self.api_path.select(*keys).where(keys[self.where[0]] == self.where[2]) - elif self.where[1] == '!=': + elif self.where[1] == '!=' or self.where[1] == 'not': select = self.api_path.select(*keys).where(keys[self.where[0]] != self.where[2]) - elif self.where[1] == '>': + elif self.where[1] == '>' or self.where[1] == 'more': select = self.api_path.select(*keys).where(keys[self.where[0]] > self.where[2]) - elif self.where[1] == '<': + elif self.where[1] == '<' or self.where[1] == 'less': select = self.api_path.select(*keys).where(keys[self.where[0]] < self.where[2]) else: self.errors("'%s' is not operator for 'where'" @@ -435,77 +446,59 @@ def api_query(self): except LibRouterosError as e: self.errors(e) - def build_api_extended_query(self, build_query): - where_args = False - for wl in build_query: - for k,v in wl.items(): - for kv,vv in v.items(): - # check 'or' items - if kv == 'or': - where_or_args = False - for ivv in vv: - for kivv,vivv in ivv.items(): - if kivv == 'is' or kivv == '==': - if where_or_args: - where_or_args = where_or_args + (self.query_keys[k] == vivv,) - else: - where_or_args = (self.query_keys[k] == vivv,) - elif kivv == 'not' or kivv == '!=': - if where_or_args: - where_or_args = where_or_args + (self.query_keys[k] != vivv,) - else: - where_or_args = (self.query_keys[k] != vivv,) - elif kivv == 'less' or kivv == '<': - if where_or_args: - where_or_args = where_or_args + (self.query_keys[k] < vivv,) - else: - where_or_args = (self.query_keys[k] < vivv,) - elif kivv == 'more' or kivv == '>': - if where_or_args: - where_or_args = where_or_args + (self.query_keys[k] > vivv,) - else: - where_or_args = (self.query_keys[k] > vivv,) - else: - self.errors("'%s' is not operator for '%s'" - % (kivv, ivv)) - if where_args: - where_args = where_args + (Or(*where_or_args),) - else: - where_args = (Or(*where_or_args),) - # check top itmes - elif kv == 'is' or kv == '==': - if where_args: - where_args = where_args + (self.query_keys[k] == vv,) - else: - where_args = (self.query_keys[k] == vv,) - elif kv == 'not'or kv == '!=': - if where_args: - where_args = where_args + (self.query_keys[k] != vv,) - else: - where_args = (self.query_keys[k] != vv,) - elif kv == 'less'or kv == '<': - if where_args: - where_args = where_args + (self.query_keys[k] < vv,) - else: - where_args = (self.query_keys[k] < vv,) - elif kv == 'more' or kv == '>': - if where_args: - where_args = where_args + (self.query_keys[k] > vv,) - else: - where_args = (self.query_keys[k] > vv,) - else: - self.errors("'%s' is not operator for '%s'" - % (kv,wl)) - return where_args + def build_api_extended_query(self, where_query, item, item_op, item_value): + if item_op == 'is' or item_op == '==': + return (self.query_keys[item] == item_value,) + elif item_op == 'not'or item_op == '!=': + return (self.query_keys[item] != item_value,) + elif item_op == 'less'or item_op == '<': + return (self.query_keys[item] < item_value,) + elif item_op == 'more' or item_op == '>': + return (self.query_keys[item] > item_value,) + else: + self.errors("'%s' is not operator for '%s'" + % (item_value,where_query)) def api_extended_query(self): self.query_keys = {} for k in self.extended_query['items']: self.query_keys[k] = Key(k) - where_args = False try: if self.extended_query['where']: - where_args = self.build_api_extended_query(self.extended_query['where']) + where_args = False + # we process nested list/dict structure + # for key,valu,item variables folow the loops. + # we start with 'w', for the next operation we add + # 'k' for key, 'v' for value, 'i' for item - at the end of the new variable + for w in self.extended_query['where']: + for wk,wv in w.items(): + for wvk,wvv in wv.items(): + # check 'in' items + if wvk == 'in': + if where_args: + where_args = where_args + (self.query_keys[wk].In(*wvv),) + else: + where_args = (self.query_keys[wk].In(*wvv),) + # check 'or' items + elif wvk == 'or': + where_or_args = False + for wvvi in wvv: + for wvvik,wvviv in wvvi.items(): + if where_or_args: + where_or_args = where_or_args + self.build_api_extended_query(w, wk, wvvik, wvviv) + else: + where_or_args = self.build_api_extended_query(w, wk, wvvik, wvviv) + if where_args: + where_args = where_args + (Or(*where_or_args),) + else: + where_args = (Or(*where_or_args),) + # check top itmes + else: + if where_args: + where_args = where_args + self.build_api_extended_query(w, wk, wvk, wvv) + else: + where_args = self.build_api_extended_query(w, wk, wvk, wvv) + select = self.api_path.select(*self.query_keys).where(*where_args) else: select = self.api_path.select(*self.query_keys) From 9ad1fe219e36939ebd760f84e05d46b3927bc36d Mon Sep 17 00:00:00 2001 From: Nikolay Dachev Date: Fri, 21 Jan 2022 02:56:18 -0500 Subject: [PATCH 05/27] add changelog #63 --- changelogs/fragments/63-add-extended_query.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelogs/fragments/63-add-extended_query.yml diff --git a/changelogs/fragments/63-add-extended_query.yml b/changelogs/fragments/63-add-extended_query.yml new file mode 100644 index 00000000..b65f7dec --- /dev/null +++ b/changelogs/fragments/63-add-extended_query.yml @@ -0,0 +1,3 @@ +minor_changes: + - "extended_query - add new option ``extended query`` more complex queries against routeros api (https://github.com/ansible-collections/community.routeros/pull/63)." + - "query - update ``query`` to accept symbolic parameters (https://github.com/ansible-collections/community.routeros/pull/63)." From 6380f88629d5a88b612d1bd434e10202179628f9 Mon Sep 17 00:00:00 2001 From: Nikolay Dachev Date: Fri, 21 Jan 2022 03:54:27 -0500 Subject: [PATCH 06/27] small fix for code indentation --- plugins/modules/api.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index a2e93cbb..71cdc3e5 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -345,20 +345,25 @@ def check_extended_query(self): for w in self.extended_query[check]: for wk,wv in w.items(): if wk not in self.extended_query["items"]: - self.errors("invalid 'query':'%s' syntax: '%s' not in 'items': '%s'" % (check, wk, self.extended_query["items"])) + self.errors("invalid 'query':'%s' syntax: '%s' not in 'items': '%s'" + % (check, wk, self.extended_query["items"])) for wvk,wvv in wv.items(): if wvk not in self.query_op_all: - self.errors("invalid 'query':'%s' syntax: '%s' for '%s' is not a valid operator" % (check, wvk, w)) + self.errors("invalid 'query':'%s' syntax: '%s' for '%s' is not a valid operator" + % (check, wvk, w)) if wvk == "in": if type(wvv) is not list: - self.errors("invalid 'query':'%s':'%s':'%s' syntax: must be type list" % (check, wk, wvv)) + self.errors("invalid 'query':'%s':'%s':'%s' syntax: must be type list" + % (check, wk, wvv)) if wvk == "or": if type(wvv) is not list: - self.errors("invalid 'query':'%s':'%s':'%s' syntax: must be type list" % (check, wk, wvv)) + self.errors("invalid 'query':'%s':'%s':'%s' syntax: must be type list" + % (check, wk, wvv)) for wvvi in wvv: for wvvik,wvviv in wvvi.items(): if wvvik not in self.query_op_or: - self.errors("invalid 'query':'%s':'%s':'%s' '%s' is not a valid operator" % (check, wk, wvk, wvvik)) + self.errors("invalid 'query':'%s':'%s':'%s' '%s' is not a valid operator" + % (check, wk, wvk, wvvik)) def list_to_dic(self, ldict): return convert_list_to_dictionary(ldict, skip_empty_values=True, require_assignment=True) @@ -485,7 +490,8 @@ def api_extended_query(self): for wvvi in wvv: for wvvik,wvviv in wvvi.items(): if where_or_args: - where_or_args = where_or_args + self.build_api_extended_query(w, wk, wvvik, wvviv) + where_or_args = where_or_args \ + + self.build_api_extended_query(w, wk, wvvik, wvviv) else: where_or_args = self.build_api_extended_query(w, wk, wvvik, wvviv) if where_args: From f2b1429319831e5980e11305a34ad16a86ad3e53 Mon Sep 17 00:00:00 2001 From: Nikolay Dachev Date: Fri, 21 Jan 2022 04:13:42 -0500 Subject: [PATCH 07/27] fix pep --- plugins/modules/api.py | 50 +++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index 71cdc3e5..977c8956 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -330,37 +330,37 @@ def check_extended_query(self): self.query_op_or = ["is", "not", "more", "less", "==", "!=", ">", "<"] if "items" not in self.extended_query.keys(): self.errors("invalid 'query' syntax: missing 'items'") - if type(self.extended_query["items"]) is not list: + if not isinstance(self.extended_query["items"], list): self.errors("invalid 'query':'items' syntax: must be type list") if "id" in self.extended_query['items']: self.errors("invalid 'query':'items' syntax: 'id' must be '.id'") if "where" in self.extended_query.keys(): check = "where" - if type(self.extended_query[check]) is not list: + if not isinstance(self.extended_query[check], list): self.errors("invalid 'query':'%s' syntax: must be type list" % check) # we process nested list/dict structure # for key,valu,item variables folow the loops. # we start with 'w', for the next operation we add # 'k' for key, 'v' for value, 'i' for item - at the end of the new variable for w in self.extended_query[check]: - for wk,wv in w.items(): + for wk, wv in w.items(): if wk not in self.extended_query["items"]: self.errors("invalid 'query':'%s' syntax: '%s' not in 'items': '%s'" % (check, wk, self.extended_query["items"])) - for wvk,wvv in wv.items(): + for wvk, wvv in wv.items(): if wvk not in self.query_op_all: self.errors("invalid 'query':'%s' syntax: '%s' for '%s' is not a valid operator" % (check, wvk, w)) if wvk == "in": - if type(wvv) is not list: + if not isinstance(wvv, list): self.errors("invalid 'query':'%s':'%s':'%s' syntax: must be type list" % (check, wk, wvv)) if wvk == "or": - if type(wvv) is not list: + if not isinstance(wvv, list): self.errors("invalid 'query':'%s':'%s':'%s' syntax: must be type list" % (check, wk, wvv)) for wvvi in wvv: - for wvvik,wvviv in wvvi.items(): + for wvvik, wvviv in wvvi.items(): if wvvik not in self.query_op_or: self.errors("invalid 'query':'%s':'%s':'%s' '%s' is not a valid operator" % (check, wk, wvk, wvvik)) @@ -454,15 +454,15 @@ def api_query(self): def build_api_extended_query(self, where_query, item, item_op, item_value): if item_op == 'is' or item_op == '==': return (self.query_keys[item] == item_value,) - elif item_op == 'not'or item_op == '!=': + elif item_op == 'not' or item_op == '!=': return (self.query_keys[item] != item_value,) - elif item_op == 'less'or item_op == '<': + elif item_op == 'less' or item_op == '<': return (self.query_keys[item] < item_value,) elif item_op == 'more' or item_op == '>': return (self.query_keys[item] > item_value,) else: self.errors("'%s' is not operator for '%s'" - % (item_value,where_query)) + % (item_value, where_query)) def api_extended_query(self): self.query_keys = {} @@ -476,8 +476,8 @@ def api_extended_query(self): # we start with 'w', for the next operation we add # 'k' for key, 'v' for value, 'i' for item - at the end of the new variable for w in self.extended_query['where']: - for wk,wv in w.items(): - for wvk,wvv in wv.items(): + for wk, wv in w.items(): + for wvk, wvv in wv.items(): # check 'in' items if wvk == 'in': if where_args: @@ -486,18 +486,18 @@ def api_extended_query(self): where_args = (self.query_keys[wk].In(*wvv),) # check 'or' items elif wvk == 'or': - where_or_args = False - for wvvi in wvv: - for wvvik,wvviv in wvvi.items(): - if where_or_args: - where_or_args = where_or_args \ - + self.build_api_extended_query(w, wk, wvvik, wvviv) - else: - where_or_args = self.build_api_extended_query(w, wk, wvvik, wvviv) - if where_args: - where_args = where_args + (Or(*where_or_args),) - else: - where_args = (Or(*where_or_args),) + where_or_args = False + for wvvi in wvv: + for wvvik, wvviv in wvvi.items(): + if where_or_args: + where_or_args = where_or_args \ + + self.build_api_extended_query(w, wk, wvvik, wvviv) + else: + where_or_args = self.build_api_extended_query(w, wk, wvvik, wvviv) + if where_args: + where_args = where_args + (Or(*where_or_args),) + else: + where_args = (Or(*where_or_args),) # check top itmes else: if where_args: @@ -512,7 +512,7 @@ def api_extended_query(self): self.result['message'].append(row) if len(self.result['message']) < 1: msg = "no results for '%s 'query' %s" % (' '.join(self.path), - self.module.params['extended_query']) + self.module.params['extended_query']) self.result['message'].append(msg) self.return_result(False) except LibRouterosError as e: From 0df9ce50545ceb18e40bb4f1f91148b2418f635b Mon Sep 17 00:00:00 2001 From: Nikolay Dachev Date: Fri, 21 Jan 2022 06:03:31 -0500 Subject: [PATCH 08/27] clear all pep issues --- plugins/modules/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index 977c8956..2619ed3b 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -491,7 +491,7 @@ def api_extended_query(self): for wvvik, wvviv in wvvi.items(): if where_or_args: where_or_args = where_or_args \ - + self.build_api_extended_query(w, wk, wvvik, wvviv) + + self.build_api_extended_query(w, wk, wvvik, wvviv) else: where_or_args = self.build_api_extended_query(w, wk, wvvik, wvviv) if where_args: From 1351536f94b7e4808243d8265488b501793484e3 Mon Sep 17 00:00:00 2001 From: Nikolay Dachev Date: Thu, 17 Feb 2022 08:04:01 -0500 Subject: [PATCH 09/27] extended_query ready for review (new yml structure) --- plugins/modules/api.py | 154 ++++++++++++++++++----------------------- 1 file changed, 69 insertions(+), 85 deletions(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index 2619ed3b..7cab03db 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -251,7 +251,11 @@ def __init__(self): update=dict(type='str'), cmd=dict(type='str'), query=dict(type='str'), - extended_query=dict(type='dict'), + extended_query=dict(type='dict', options=dict( + attributes=dict(type='list', required=True), + where=dict(type='list') + + )), ) module_args.update(api_argument_spec()) @@ -325,45 +329,35 @@ def check_query(self): # Raised when WHERE has not been found pass + def check_extended_query_syntax(self, test_atr, or_msg=''): + extended_query_op = {'attribute': '', 'is': ["==", "!=", ">", "<", "in", "eq", "not", "more", "less"], 'value': ''} + if not isinstance(test_atr, dict): + self.errors("invalid syntax 'extended_query':'where':%s%s must be a type dict" % (or_msg, test_atr)) + for ik in test_atr.keys(): + if ik not in extended_query_op.keys(): + self.errors("invalid syntax 'extended_query':'where'%s%s must have %s" % (or_msg, test_atr, extended_query_op.keys())) + if test_atr['is'] not in extended_query_op['is']: + self.errors("invalid syntax 'extended_query':'where':%s%s '%s' not a valid operator for 'is' %s" % (or_msg, + test_atr, + test_atr['is'], + extended_query_op['is'])) + if test_atr['is'] == "in" and not isinstance(test_atr['value'], list): + self.errors("invalid syntax 'extended_query':'where':%s%s 'value' must be a type list" % (or_msg, test_atr)) + def check_extended_query(self): - self.query_op_all = ["is", "not", "more", "less", "or", "in", "==", "!=", ">", "<"] - self.query_op_or = ["is", "not", "more", "less", "==", "!=", ">", "<"] - if "items" not in self.extended_query.keys(): - self.errors("invalid 'query' syntax: missing 'items'") - if not isinstance(self.extended_query["items"], list): - self.errors("invalid 'query':'items' syntax: must be type list") - if "id" in self.extended_query['items']: - self.errors("invalid 'query':'items' syntax: 'id' must be '.id'") - if "where" in self.extended_query.keys(): - check = "where" - if not isinstance(self.extended_query[check], list): - self.errors("invalid 'query':'%s' syntax: must be type list" % check) - # we process nested list/dict structure - # for key,valu,item variables folow the loops. - # we start with 'w', for the next operation we add - # 'k' for key, 'v' for value, 'i' for item - at the end of the new variable - for w in self.extended_query[check]: - for wk, wv in w.items(): - if wk not in self.extended_query["items"]: - self.errors("invalid 'query':'%s' syntax: '%s' not in 'items': '%s'" - % (check, wk, self.extended_query["items"])) - for wvk, wvv in wv.items(): - if wvk not in self.query_op_all: - self.errors("invalid 'query':'%s' syntax: '%s' for '%s' is not a valid operator" - % (check, wvk, w)) - if wvk == "in": - if not isinstance(wvv, list): - self.errors("invalid 'query':'%s':'%s':'%s' syntax: must be type list" - % (check, wk, wvv)) - if wvk == "or": - if not isinstance(wvv, list): - self.errors("invalid 'query':'%s':'%s':'%s' syntax: must be type list" - % (check, wk, wvv)) - for wvvi in wvv: - for wvvik, wvviv in wvvi.items(): - if wvvik not in self.query_op_or: - self.errors("invalid 'query':'%s':'%s':'%s' '%s' is not a valid operator" - % (check, wk, wvk, wvvik)) + if self.extended_query["where"]: + for i in self.extended_query['where']: + if "or" in i.keys(): + if not isinstance(i["or"], list): + self.errors("invalid syntax 'extended_query':'where':'or':%s 'or' must be a type list" % i["or"]) + if len(i['or']) < 2: + self.errors("invalid syntax 'extended_query':'where':'or':%s 'or' requires minimum two items" % i["or"]) + for orv in i['or']: + if "or" in orv.keys(): + self.errors("invalid syntax 'extended_query':'where':'or':%s nested 'or' is not allowed" % i["or"]) + self.check_extended_query_syntax(orv, ":'or':") + else: + self.check_extended_query_syntax(i) def list_to_dic(self, ldict): return convert_list_to_dictionary(ldict, skip_empty_values=True, require_assignment=True) @@ -426,7 +420,7 @@ def api_query(self): keys[k] = Key(k) try: if self.where: - if self.where[1] == '==' or self.where[1] == 'is': + if self.where[1] == '==' or self.where[1] == 'eq': select = self.api_path.select(*keys).where(keys[self.where[0]] == self.where[2]) elif self.where[1] == '!=' or self.where[1] == 'not': select = self.api_path.select(*keys).where(keys[self.where[0]] != self.where[2]) @@ -451,60 +445,50 @@ def api_query(self): except LibRouterosError as e: self.errors(e) - def build_api_extended_query(self, where_query, item, item_op, item_value): - if item_op == 'is' or item_op == '==': - return (self.query_keys[item] == item_value,) - elif item_op == 'not' or item_op == '!=': - return (self.query_keys[item] != item_value,) - elif item_op == 'less' or item_op == '<': - return (self.query_keys[item] < item_value,) - elif item_op == 'more' or item_op == '>': - return (self.query_keys[item] > item_value,) + def build_api_extended_query(self, item): + if item['attribute'] not in self.extended_query['attributes']: + self.errors("'%s' attribute is not in attributes:%s" + % (item, self.extended_query['attributes'])) + if item['is'] == 'eq' or item['is'] == '==': + return (self.query_keys[item['attribute']] == item['value'],) + elif item['is'] == 'not' or item['is'] == '!=': + return (self.query_keys[item['attribute']] != item['value'],) + elif item['is'] == 'less' or item['is'] == '<': + return (self.query_keys[item['attribute']] < item['value'],) + elif item['is'] == 'more' or item['is'] == '>': + return (self.query_keys[item['attribute']] > item['value'],) + elif item['is'] == 'in': + return (self.query_keys[item['attribute']].In(*item['value']),) else: - self.errors("'%s' is not operator for '%s'" - % (item_value, where_query)) + self.errors("'%s' is not operator for 'is'" + % item['is']) def api_extended_query(self): self.query_keys = {} - for k in self.extended_query['items']: + for k in self.extended_query['attributes']: + if k == 'id': + self.errors("'extended_query':'attributes':'%s' must be '.id'" % k) self.query_keys[k] = Key(k) try: if self.extended_query['where']: where_args = False - # we process nested list/dict structure - # for key,valu,item variables folow the loops. - # we start with 'w', for the next operation we add - # 'k' for key, 'v' for value, 'i' for item - at the end of the new variable - for w in self.extended_query['where']: - for wk, wv in w.items(): - for wvk, wvv in wv.items(): - # check 'in' items - if wvk == 'in': - if where_args: - where_args = where_args + (self.query_keys[wk].In(*wvv),) - else: - where_args = (self.query_keys[wk].In(*wvv),) - # check 'or' items - elif wvk == 'or': - where_or_args = False - for wvvi in wvv: - for wvvik, wvviv in wvvi.items(): - if where_or_args: - where_or_args = where_or_args \ - + self.build_api_extended_query(w, wk, wvvik, wvviv) - else: - where_or_args = self.build_api_extended_query(w, wk, wvvik, wvviv) - if where_args: - where_args = where_args + (Or(*where_or_args),) - else: - where_args = (Or(*where_or_args),) - # check top itmes + for i in self.extended_query['where']: + if "or" in i.keys(): + where_or_args = False + for ior in i['or']: + if where_or_args: + where_or_args = where_or_args + self.build_api_extended_query(ior) else: - if where_args: - where_args = where_args + self.build_api_extended_query(w, wk, wvk, wvv) - else: - where_args = self.build_api_extended_query(w, wk, wvk, wvv) - + where_or_args = self.build_api_extended_query(ior) + if where_args: + where_args = where_args + (Or(*where_or_args),) + else: + where_args = (Or(*where_or_args),) + else: + if where_args: + where_args = where_args + self.build_api_extended_query(i) + else: + where_args = self.build_api_extended_query(i) select = self.api_path.select(*self.query_keys).where(*where_args) else: select = self.api_path.select(*self.query_keys) From 3c8469a382596df43d7005a1a4dcf42f395860e4 Mon Sep 17 00:00:00 2001 From: Nikolay Dachev Date: Thu, 17 Feb 2022 08:08:09 -0500 Subject: [PATCH 10/27] small doc fix for std query --- plugins/modules/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index 7cab03db..f598e284 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -52,7 +52,7 @@ description: - Query given path for selected query attributes from RouterOS aip. - WHERE is key word which extend query. WHERE format is key operator value - with spaces. - - WHERE valid operators are C(==) or C(is), C(!=) or C(not), C(>) or C(more), C(<) or C(less). + - WHERE valid operators are C(==) or C(eq), C(!=) or C(not), C(>) or C(more), C(<) or C(less). - Example path C(ip address) and query C(.id address) will return only C(.id) and C(address) for all items in C(ip address) path. - Example path C(ip address) and query C(.id address WHERE address == 1.1.1.3/32). will return only C(.id) and C(address) for items in C(ip address) path, where address is eq to 1.1.1.3/32. From f40f6dc0b0c326fa6ab89c381c69e0bff353590e Mon Sep 17 00:00:00 2001 From: Nikolay Dachev Date: Wed, 20 Apr 2022 17:23:45 +0300 Subject: [PATCH 11/27] Update changelogs/fragments/63-add-extended_query.yml Co-authored-by: Felix Fontein --- changelogs/fragments/63-add-extended_query.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/fragments/63-add-extended_query.yml b/changelogs/fragments/63-add-extended_query.yml index b65f7dec..1dd0385f 100644 --- a/changelogs/fragments/63-add-extended_query.yml +++ b/changelogs/fragments/63-add-extended_query.yml @@ -1,3 +1,3 @@ minor_changes: - - "extended_query - add new option ``extended query`` more complex queries against routeros api (https://github.com/ansible-collections/community.routeros/pull/63)." + - "api - add new option ``extended query`` more complex queries against RouterOS API (https://github.com/ansible-collections/community.routeros/pull/63)." - "query - update ``query`` to accept symbolic parameters (https://github.com/ansible-collections/community.routeros/pull/63)." From e34d374fdc5fa44f78c1b1f4ec327378cbb4b14e Mon Sep 17 00:00:00 2001 From: Nikolay Dachev Date: Wed, 20 Apr 2022 17:23:53 +0300 Subject: [PATCH 12/27] Update changelogs/fragments/63-add-extended_query.yml Co-authored-by: Felix Fontein --- changelogs/fragments/63-add-extended_query.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/fragments/63-add-extended_query.yml b/changelogs/fragments/63-add-extended_query.yml index 1dd0385f..dc39caf3 100644 --- a/changelogs/fragments/63-add-extended_query.yml +++ b/changelogs/fragments/63-add-extended_query.yml @@ -1,3 +1,3 @@ minor_changes: - "api - add new option ``extended query`` more complex queries against RouterOS API (https://github.com/ansible-collections/community.routeros/pull/63)." - - "query - update ``query`` to accept symbolic parameters (https://github.com/ansible-collections/community.routeros/pull/63)." + - "api - update ``query`` to accept symbolic parameters (https://github.com/ansible-collections/community.routeros/pull/63)." From d83475864f50f5ffce348fb3e8cb4ceb2a9aea24 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Tue, 3 May 2022 07:02:09 +0200 Subject: [PATCH 13/27] Update argument spec. --- plugins/modules/api.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index f598e284..43320135 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -252,8 +252,24 @@ def __init__(self): cmd=dict(type='str'), query=dict(type='str'), extended_query=dict(type='dict', options=dict( - attributes=dict(type='list', required=True), - where=dict(type='list') + attributes=dict(type='list', elements='str', required=True), + where=dict( + type='list', + elements='dict', + options=dict( + attribute=dict(type='str'), + is=dict(type='str', choices=["==", "!=", ">", "<", "in", "eq", "not", "more", "less"]), + value=dict(type='raw'), + or=dict(type='list', elements='dict', options=dict( + attribute=dict(type='str', required=True), + is=dict(type='str', choices=["==", "!=", ">", "<", "in", "eq", "not", "more", "less"], required=True), + value=dict(type='raw', required=True), + )), + ), + required_together=[('attribute', 'is', 'value')], + mutually_exclusive=[('attribute', 'or')], + required_one_of=[('attribute', 'or')], + ), )), ) From f144290c328ff607053557e1efb19b99c2282d22 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Tue, 3 May 2022 07:03:12 +0200 Subject: [PATCH 14/27] Other suggestions. --- plugins/modules/api.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index 43320135..cb40ad12 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -436,7 +436,7 @@ def api_query(self): keys[k] = Key(k) try: if self.where: - if self.where[1] == '==' or self.where[1] == 'eq': + if self.where[1] in ('==', 'eq'): select = self.api_path.select(*keys).where(keys[self.where[0]] == self.where[2]) elif self.where[1] == '!=' or self.where[1] == 'not': select = self.api_path.select(*keys).where(keys[self.where[0]] != self.where[2]) @@ -487,27 +487,18 @@ def api_extended_query(self): self.query_keys[k] = Key(k) try: if self.extended_query['where']: - where_args = False + where_args = [] for i in self.extended_query['where']: - if "or" in i.keys(): - where_or_args = False + if "or" in i: + where_or_args = [] for ior in i['or']: - if where_or_args: - where_or_args = where_or_args + self.build_api_extended_query(ior) - else: - where_or_args = self.build_api_extended_query(ior) - if where_args: - where_args = where_args + (Or(*where_or_args),) - else: - where_args = (Or(*where_or_args),) + where_or_args.append(self.build_api_extended_query(ior)) + where_args.append(Or(*where_or_args)) else: - if where_args: - where_args = where_args + self.build_api_extended_query(i) - else: - where_args = self.build_api_extended_query(i) + where_args.append(self.build_api_extended_query(i)) select = self.api_path.select(*self.query_keys).where(*where_args) else: - select = self.api_path.select(*self.query_keys) + select = self.api_path.select(*self.extended_query['attributes']) for row in select: self.result['message'].append(row) if len(self.result['message']) < 1: From f132ed14ad150e1099eba732288301ed93d5a642 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Tue, 3 May 2022 07:06:09 +0200 Subject: [PATCH 15/27] Fix syntax errors ('is' and 'or' are keywords). --- plugins/modules/api.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index cb40ad12..90547d90 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -256,16 +256,16 @@ def __init__(self): where=dict( type='list', elements='dict', - options=dict( - attribute=dict(type='str'), - is=dict(type='str', choices=["==", "!=", ">", "<", "in", "eq", "not", "more", "less"]), - value=dict(type='raw'), - or=dict(type='list', elements='dict', options=dict( - attribute=dict(type='str', required=True), - is=dict(type='str', choices=["==", "!=", ">", "<", "in", "eq", "not", "more", "less"], required=True), - value=dict(type='raw', required=True), - )), - ), + options={ + 'attribute': dict(type='str'), + 'is': dict(type='str', choices=["==", "!=", ">", "<", "in", "eq", "not", "more", "less"]), + 'value': dict(type='raw'), + 'or': dict(type='list', elements='dict', options={ + 'attribute': dict(type='str', required=True), + 'is': dict(type='str', choices=["==", "!=", ">", "<", "in", "eq", "not", "more", "less"], required=True), + 'value': dict(type='raw', required=True), + }), + }, required_together=[('attribute', 'is', 'value')], mutually_exclusive=[('attribute', 'or')], required_one_of=[('attribute', 'or')], From 2fa0d2e342f8d67db517f06ec94ab75ed6e57780 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Tue, 3 May 2022 07:20:30 +0200 Subject: [PATCH 16/27] Make everything work again. --- plugins/modules/api.py | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index 90547d90..617aa3b1 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -346,31 +346,16 @@ def check_query(self): pass def check_extended_query_syntax(self, test_atr, or_msg=''): - extended_query_op = {'attribute': '', 'is': ["==", "!=", ">", "<", "in", "eq", "not", "more", "less"], 'value': ''} - if not isinstance(test_atr, dict): - self.errors("invalid syntax 'extended_query':'where':%s%s must be a type dict" % (or_msg, test_atr)) - for ik in test_atr.keys(): - if ik not in extended_query_op.keys(): - self.errors("invalid syntax 'extended_query':'where'%s%s must have %s" % (or_msg, test_atr, extended_query_op.keys())) - if test_atr['is'] not in extended_query_op['is']: - self.errors("invalid syntax 'extended_query':'where':%s%s '%s' not a valid operator for 'is' %s" % (or_msg, - test_atr, - test_atr['is'], - extended_query_op['is'])) if test_atr['is'] == "in" and not isinstance(test_atr['value'], list): self.errors("invalid syntax 'extended_query':'where':%s%s 'value' must be a type list" % (or_msg, test_atr)) def check_extended_query(self): if self.extended_query["where"]: for i in self.extended_query['where']: - if "or" in i.keys(): - if not isinstance(i["or"], list): - self.errors("invalid syntax 'extended_query':'where':'or':%s 'or' must be a type list" % i["or"]) + if i["or"] is not None: if len(i['or']) < 2: self.errors("invalid syntax 'extended_query':'where':'or':%s 'or' requires minimum two items" % i["or"]) for orv in i['or']: - if "or" in orv.keys(): - self.errors("invalid syntax 'extended_query':'where':'or':%s nested 'or' is not allowed" % i["or"]) self.check_extended_query_syntax(orv, ":'or':") else: self.check_extended_query_syntax(i) @@ -463,21 +448,20 @@ def api_query(self): def build_api_extended_query(self, item): if item['attribute'] not in self.extended_query['attributes']: - self.errors("'%s' attribute is not in attributes:%s" + self.errors("'%s' attribute is not in attributes: %s" % (item, self.extended_query['attributes'])) if item['is'] == 'eq' or item['is'] == '==': - return (self.query_keys[item['attribute']] == item['value'],) + return self.query_keys[item['attribute']] == item['value'] elif item['is'] == 'not' or item['is'] == '!=': - return (self.query_keys[item['attribute']] != item['value'],) + return self.query_keys[item['attribute']] != item['value'] elif item['is'] == 'less' or item['is'] == '<': - return (self.query_keys[item['attribute']] < item['value'],) + return self.query_keys[item['attribute']] < item['value'] elif item['is'] == 'more' or item['is'] == '>': - return (self.query_keys[item['attribute']] > item['value'],) + return self.query_keys[item['attribute']] > item['value'] elif item['is'] == 'in': - return (self.query_keys[item['attribute']].In(*item['value']),) + return self.query_keys[item['attribute']].In(*item['value']) else: - self.errors("'%s' is not operator for 'is'" - % item['is']) + self.errors("'%s' is not operator for 'is'" % item['is']) def api_extended_query(self): self.query_keys = {} @@ -489,7 +473,7 @@ def api_extended_query(self): if self.extended_query['where']: where_args = [] for i in self.extended_query['where']: - if "or" in i: + if i['or']: where_or_args = [] for ior in i['or']: where_or_args.append(self.build_api_extended_query(ior)) From 09929aa9363705eedcb225e1790f5ca031c553cc Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Tue, 3 May 2022 18:10:48 +0200 Subject: [PATCH 17/27] Add docs, simplify code. --- plugins/modules/api.py | 73 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index 617aa3b1..5988b3c6 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -64,6 +64,64 @@ description: - TODO type: dict + suboptions: + attributes: + description: + - The list of attributes to return. + - Every attribute used in a I(where) clause need to be listed here. + type: list + elements: str + required: true + where: + description: + - Allows to restrict the objects returned. + - The conditions here must all match. An I(or) condition needs at least one of its conditions to match. + type: list + elements: dict + suboptions: + attribute: + description: + - The attribute to match. Must be part of I(attributes). + - Either I(or) or all of I(attribute), I(is), and I(value) have to be specified. + type: str + is: + description: + - The operator to use for matching. + - For equality use C(==) or C(eq). For less use C(<) or C(less). For more use C(>) or C(more). + - Use C(in) to check whether the value is part of a list. In that case, I(value) must be a list. + - Either I(or) or all of I(attribute), I(is), and I(value) have to be specified. + type: str + choices: ["==", "!=", ">", "<", "in", "eq", "not", "more", "less"] + value: + description: + - The value to compare to. Must be a list for I(is=in). + - Either I(or) or all of I(attribute), I(is), and I(value) have to be specified. + type: raw + or: + description: + - A list of conditions so that at least one of them has to match. + - Either I(or) or all of I(attribute), I(is), and I(value) have to be specified. + type: list + elements: dict + suboptions: + attribute: + description: + - The attribute to match. Must be part of I(attributes). + type: str + required: true + is: + description: + - The operator to use for matching. + - For equality use C(==) or C(eq). For less use C(<) or C(less). For more use C(>) or C(more). + - Use C(in) to check whether the value is part of a list. In that case, I(value) must be a list. + type: str + choices: ["==", "!=", ">", "<", "in", "eq", "not", "more", "less"] + required: true + value: + description: + - The value to compare to. Must be a list for I(is=in). + type: raw + required: true cmd: description: - Execute any/arbitrary command in selected path, after the command we can add C(.id). @@ -270,7 +328,6 @@ def __init__(self): mutually_exclusive=[('attribute', 'or')], required_one_of=[('attribute', 'or')], ), - )), ) module_args.update(api_argument_spec()) @@ -423,11 +480,11 @@ def api_query(self): if self.where: if self.where[1] in ('==', 'eq'): select = self.api_path.select(*keys).where(keys[self.where[0]] == self.where[2]) - elif self.where[1] == '!=' or self.where[1] == 'not': + elif self.where[1] in ('!=', 'not'): select = self.api_path.select(*keys).where(keys[self.where[0]] != self.where[2]) - elif self.where[1] == '>' or self.where[1] == 'more': + elif self.where[1] in ('>', 'more'): select = self.api_path.select(*keys).where(keys[self.where[0]] > self.where[2]) - elif self.where[1] == '<' or self.where[1] == 'less': + elif self.where[1] in ('<', 'less'): select = self.api_path.select(*keys).where(keys[self.where[0]] < self.where[2]) else: self.errors("'%s' is not operator for 'where'" @@ -450,13 +507,13 @@ def build_api_extended_query(self, item): if item['attribute'] not in self.extended_query['attributes']: self.errors("'%s' attribute is not in attributes: %s" % (item, self.extended_query['attributes'])) - if item['is'] == 'eq' or item['is'] == '==': + if item['is'] in ('eq', '=='): return self.query_keys[item['attribute']] == item['value'] - elif item['is'] == 'not' or item['is'] == '!=': + elif item['is'] in ('not', '!='): return self.query_keys[item['attribute']] != item['value'] - elif item['is'] == 'less' or item['is'] == '<': + elif item['is'] in ('less', '<'): return self.query_keys[item['attribute']] < item['value'] - elif item['is'] == 'more' or item['is'] == '>': + elif item['is'] in ('more', '>'): return self.query_keys[item['attribute']] > item['value'] elif item['is'] == 'in': return self.query_keys[item['attribute']].In(*item['value']) From 6c8251bdae54fe80cdf6d7ab2bf2a46348e5bb9c Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Tue, 3 May 2022 19:42:45 +0200 Subject: [PATCH 18/27] Add some first tests. --- tests/unit/plugins/modules/test_api.py | 66 ++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/unit/plugins/modules/test_api.py b/tests/unit/plugins/modules/test_api.py index f3dd1143..21e30d88 100644 --- a/tests/unit/plugins/modules/test_api.py +++ b/tests/unit/plugins/modules/test_api.py @@ -197,3 +197,69 @@ def test_api_query_and_WHERE_no_cond(self): result = exc.exception.args[0] self.assertEqual(result['changed'], False) + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api) + def test_api_extended_query(self): + with self.assertRaises(AnsibleExitJson) as exc: + module_args = self.config_module_args.copy() + module_args['extended_query'] = { + 'attributes': ['.id', 'name'], + } + set_module_args(module_args) + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api) + def test_api_extended_query_missing_key(self): + with self.assertRaises(AnsibleExitJson) as exc: + module_args = self.config_module_args.copy() + module_args['extended_query'] = { + 'attributes': ['.id', 'other'], + } + set_module_args(module_args) + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.select_where) + def test_api_extended_query_and_WHERE(self): + with self.assertRaises(AnsibleExitJson) as exc: + module_args = self.config_module_args.copy() + module_args['extended_query'] = { + 'attributes': ['.id', 'name'], + 'where': [ + { + 'attribute': 'name', + 'is': '==', + 'value': 'dummy_bridge_A2', + }, + ], + } + set_module_args(module_args) + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.select_where) + def test_api_extended_query_and_WHERE_no_cond(self): + with self.assertRaises(AnsibleExitJson) as exc: + module_args = self.config_module_args.copy() + module_args['extended_query'] = { + 'attributes': ['.id', 'name'], + 'where': [ + { + 'attribute': 'name', + 'is': 'not', + 'value': 'dummy_bridge_A2', + }, + ], + } + set_module_args(module_args) + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) From 418252efa011facab6cd1ce4bb5071b43a418ba0 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Tue, 3 May 2022 19:49:06 +0200 Subject: [PATCH 19/27] Do not add fake message when there is no search result. --- plugins/modules/api.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index 5988b3c6..8d0f40f9 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -542,10 +542,6 @@ def api_extended_query(self): select = self.api_path.select(*self.extended_query['attributes']) for row in select: self.result['message'].append(row) - if len(self.result['message']) < 1: - msg = "no results for '%s 'query' %s" % (' '.join(self.path), - self.module.params['extended_query']) - self.result['message'].append(msg) self.return_result(False) except LibRouterosError as e: self.errors(e) From 0f525d3c68b9257d6385a6ecdcb935d0bcdf2519 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Tue, 3 May 2022 19:56:12 +0200 Subject: [PATCH 20/27] Improve tests. --- tests/unit/plugins/modules/fake_api.py | 4 +- tests/unit/plugins/modules/test_api.py | 56 ++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/tests/unit/plugins/modules/fake_api.py b/tests/unit/plugins/modules/fake_api.py index 6b5805be..94b9e0c9 100644 --- a/tests/unit/plugins/modules/fake_api.py +++ b/tests/unit/plugins/modules/fake_api.py @@ -90,7 +90,7 @@ def select(self, *args): if result: return result else: - return ["no results for 'interface bridge 'query' %s" % ' '.join(args)] + return [] @classmethod def select_where(cls, api, path): @@ -106,7 +106,7 @@ def select(self, *args): return self def where(self, *args): - return ["*A1"] + return [{".id": "*A1", "name": "dummy_bridge_A1"}] class Key(object): diff --git a/tests/unit/plugins/modules/test_api.py b/tests/unit/plugins/modules/test_api.py index 21e30d88..01b987cb 100644 --- a/tests/unit/plugins/modules/test_api.py +++ b/tests/unit/plugins/modules/test_api.py @@ -164,6 +164,11 @@ def test_api_query(self): result = exc.exception.args[0] self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], [ + {'.id': '*A1', 'name': 'dummy_bridge_A1'}, + {'.id': '*A2', 'name': 'dummy_bridge_A2'}, + {'.id': '*A3', 'name': 'dummy_bridge_A3'}, + ]) @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api) def test_api_query_missing_key(self): @@ -175,6 +180,7 @@ def test_api_query_missing_key(self): result = exc.exception.args[0] self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], ["no results for 'interface bridge 'query' .id other"]) @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.select_where) def test_api_query_and_WHERE(self): @@ -186,6 +192,9 @@ def test_api_query_and_WHERE(self): result = exc.exception.args[0] self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], [ + {'.id': '*A1', 'name': 'dummy_bridge_A1'}, + ]) @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.select_where) def test_api_query_and_WHERE_no_cond(self): @@ -197,6 +206,9 @@ def test_api_query_and_WHERE_no_cond(self): result = exc.exception.args[0] self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], [ + {'.id': '*A1', 'name': 'dummy_bridge_A1'}, + ]) @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api) def test_api_extended_query(self): @@ -210,6 +222,11 @@ def test_api_extended_query(self): result = exc.exception.args[0] self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], [ + {'.id': '*A1', 'name': 'dummy_bridge_A1'}, + {'.id': '*A2', 'name': 'dummy_bridge_A2'}, + {'.id': '*A3', 'name': 'dummy_bridge_A3'}, + ]) @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api) def test_api_extended_query_missing_key(self): @@ -223,6 +240,7 @@ def test_api_extended_query_missing_key(self): result = exc.exception.args[0] self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], []) @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.select_where) def test_api_extended_query_and_WHERE(self): @@ -243,6 +261,9 @@ def test_api_extended_query_and_WHERE(self): result = exc.exception.args[0] self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], [ + {'.id': '*A1', 'name': 'dummy_bridge_A1'}, + ]) @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.select_where) def test_api_extended_query_and_WHERE_no_cond(self): @@ -263,3 +284,38 @@ def test_api_extended_query_and_WHERE_no_cond(self): result = exc.exception.args[0] self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], [ + {'.id': '*A1', 'name': 'dummy_bridge_A1'}, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.select_where) + def test_api_extended_query_and_WHERE_or(self): + with self.assertRaises(AnsibleExitJson) as exc: + module_args = self.config_module_args.copy() + module_args['extended_query'] = { + 'attributes': ['.id', 'name'], + 'where': [ + { + 'or': [ + { + 'attribute': 'name', + 'is': 'in', + 'value': [1, 2], + }, + { + 'attribute': 'name', + 'is': '!=', + 'value': 5, + }, + ], + }, + ], + } + set_module_args(module_args) + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], [ + {'.id': '*A1', 'name': 'dummy_bridge_A1'}, + ]) From 24035b38dfb01aea7a02d6493d640c270f270801 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 15 May 2022 23:03:13 +0200 Subject: [PATCH 21/27] Fix tests. --- tests/unit/plugins/modules/fake_api.py | 9 +++++++++ tests/unit/plugins/modules/test_api.py | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/unit/plugins/modules/fake_api.py b/tests/unit/plugins/modules/fake_api.py index 94b9e0c9..cef65867 100644 --- a/tests/unit/plugins/modules/fake_api.py +++ b/tests/unit/plugins/modules/fake_api.py @@ -116,3 +116,12 @@ def __init__(self, name): def str_return(self): return str(self.name) + + +class Or(object): + def __init__(self, *args): + self.args = args + self.str_return() + + def str_return(self): + return repr(self.args) diff --git a/tests/unit/plugins/modules/test_api.py b/tests/unit/plugins/modules/test_api.py index 01b987cb..94982ac8 100644 --- a/tests/unit/plugins/modules/test_api.py +++ b/tests/unit/plugins/modules/test_api.py @@ -21,7 +21,7 @@ import pytest from ansible_collections.community.routeros.tests.unit.compat.mock import patch, MagicMock -from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import FakeLibRouterosError, Key, fake_ros_api +from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import FakeLibRouterosError, Key, Or, fake_ros_api from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase from ansible_collections.community.routeros.plugins.modules import api @@ -37,6 +37,7 @@ def setUp(self): self.patch_create_api = patch('ansible_collections.community.routeros.plugins.modules.api.create_api', MagicMock(new=fake_ros_api)) self.patch_create_api.start() self.module.Key = MagicMock(new=Key) + self.module.Or = MagicMock(new=Or) self.config_module_args = {"username": "admin", "password": "pаss", "hostname": "127.0.0.1", From 8b1470aa601962d41373494b06b997028c56ff0b Mon Sep 17 00:00:00 2001 From: Nikolay Dachev Date: Sun, 22 May 2022 16:27:57 -0400 Subject: [PATCH 22/27] update extened query docs and ros api module examples --- plugins/modules/api.py | 134 ++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 90 deletions(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index 8d0f40f9..92fa3a6e 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -62,7 +62,8 @@ type: str extended_query: description: - - TODO + - Extended query given path for selected query attributes from RouterOS aip. + - Extended query allow conjunctive input, in such case 'no results' will be returned. type: dict suboptions: attributes: @@ -135,118 +136,76 @@ EXAMPLES = ''' --- -- name: Use RouterOS API - hosts: localhost - gather_facts: no - vars: - hostname: "ros_api_hostname/ip" - username: "admin" - password: "secret_password" - - path: "ip address" - - nic: "ether2" - ip1: "1.1.1.1/32" - ip2: "2.2.2.2/32" - ip3: "3.3.3.3/32" - tasks: - - name: Get "{{ path }} print" + - name: Get example - ip address print community.routeros.api: hostname: "{{ hostname }}" password: "{{ password }}" username: "{{ username }}" - path: "{{ path }}" - register: print_path - - - name: Dump "{{ path }} print" output - ansible.builtin.debug: - msg: '{{ print_path }}' + path: "ip address" - - name: Add ip address "{{ ip1 }}" and "{{ ip2 }}" + - name: Add example - ip address community.routeros.api: hostname: "{{ hostname }}" password: "{{ password }}" username: "{{ username }}" - path: "{{ path }}" - add: "{{ item }}" - loop: - - "address={{ ip1 }} interface={{ nic }}" - - "address={{ ip2 }} interface={{ nic }}" - register: addout - - - name: Dump "Add ip address" output - ".id" for new added items - ansible.builtin.debug: - msg: '{{ addout }}' - - - name: Query for ".id" in "{{ path }} WHERE address == {{ ip2 }}" + path: "ip address" + add: "address=192.168.255.10/24 interface=ether2" + + - name: Query example - ".id, address" in "ip address WHERE address == 192.168.255.10/24" community.routeros.api: hostname: "{{ hostname }}" password: "{{ password }}" username: "{{ username }}" - path: "{{ path }}" + path: "ip address" query: ".id address WHERE address == {{ ip2 }}" - register: queryout - - name: Dump "Query for" output and set fact with ".id" for "{{ ip2 }}" - ansible.builtin.debug: - msg: '{{ queryout }}' - - - name: Store query_id for later usage - ansible.builtin.set_fact: - query_id: "{{ queryout['msg'][0]['.id'] }}" - - - name: Update ".id = {{ query_id }}" taken with custom fact "fquery_id" + - name: Extended query example - ".id,address,network" where address is not 192.168.255.10/24 or is 10.20.36.20/24 community.routeros.api: hostname: "{{ hostname }}" password: "{{ password }}" username: "{{ username }}" - path: "{{ path }}" - update: >- - .id={{ query_id }} - address={{ ip3 }} - comment={{ 'A comment with spaces' | community.routeros.quote_argument_value }} - register: updateout - - - name: Dump "Update" output - ansible.builtin.debug: - msg: '{{ updateout }}' - - - name: Remove ips - stage 1 - query ".id" for "{{ ip2 }}" and "{{ ip3 }}" + path: "ip address" + extended_query: + attributes: + - network + - address + - .id + where: + - attribute: "network" + is: "==" + value: "192.168.255.0" + - or: + - attribute: "address" + is: "!=" + value: "192.168.255.10/24" + - attribute: "address" + is: "eq" + value: "10.20.36.20/24" + - attribute: "network" + is: "in" + value: + - "10.20.36.0" + - "192.168.255.0" + + - name: Update example - ether2 ip addres with ".id = *14" community.routeros.api: hostname: "{{ hostname }}" password: "{{ password }}" username: "{{ username }}" - path: "{{ path }}" - query: ".id address WHERE address == {{ item }}" - register: id_to_remove - loop: - - "{{ ip2 }}" - - "{{ ip3 }}" - - - name: Set fact for ".id" from "Remove ips - stage 1 - query" - ansible.builtin.set_fact: - to_be_remove: "{{ to_be_remove |default([]) + [item['msg'][0]['.id']] }}" - loop: "{{ id_to_remove.results }}" - - - name: Dump "Remove ips - stage 1 - query" output - ansible.builtin.debug: - msg: '{{ to_be_remove }}' - - # Remove "{{ rmips }}" with ".id" by "to_be_remove" from query - - name: Remove ips - stage 2 - remove "{{ ip2 }}" and "{{ ip3 }}" by '.id' + path: "ip address" + update: >- + .id=*14 + address=192.168.255.20/24 + comment={{ 'Update 192.168.255.10/24 with .id=*14 to 192.168.255.20/24 on ether2' | community.routeros.quote_argument_value }} + + - name: Remove example - ether2 ip 192.168.255.20/24 with ".id = *14" community.routeros.api: hostname: "{{ hostname }}" password: "{{ password }}" username: "{{ username }}" - path: "{{ path }}" - remove: "{{ item }}" - register: remove - loop: "{{ to_be_remove }}" - - - name: Dump "Remove ips - stage 2 - remove" output - ansible.builtin.debug: - msg: '{{ remove }}' + path: "ip address" + remove: "*14" - name: Arbitrary command example "/system identity print" community.routeros.api: @@ -255,11 +214,6 @@ username: "{{ username }}" path: "system identity" cmd: "print" - register: cmdout - - - name: Dump "Arbitrary command example" output - ansible.builtin.debug: - msg: "{{ cmdout }}" ''' RETURN = ''' From 4859144f4cf80b172c13cafbd58da3b216addcb9 Mon Sep 17 00:00:00 2001 From: Nikolay Dachev Date: Sun, 22 May 2022 16:42:54 -0400 Subject: [PATCH 23/27] fix pep plugins/modules/api.py:154:1: W293: blank line contains whitespace --- plugins/modules/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index 92fa3a6e..ae038959 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -151,7 +151,7 @@ username: "{{ username }}" path: "ip address" add: "address=192.168.255.10/24 interface=ether2" - + - name: Query example - ".id, address" in "ip address WHERE address == 192.168.255.10/24" community.routeros.api: hostname: "{{ hostname }}" From f26bc269cf6c58b35e41c16ad8a42082633b8f45 Mon Sep 17 00:00:00 2001 From: Nikolay Dachev Date: Sun, 22 May 2022 16:48:30 -0400 Subject: [PATCH 24/27] fix extended query example intend --- plugins/modules/api.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index ae038959..a1d15f42 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -167,26 +167,26 @@ username: "{{ username }}" path: "ip address" extended_query: - attributes: - - network - - address - - .id - where: - - attribute: "network" - is: "==" - value: "192.168.255.0" - - or: - - attribute: "address" - is: "!=" - value: "192.168.255.10/24" - - attribute: "address" - is: "eq" - value: "10.20.36.20/24" - - attribute: "network" - is: "in" - value: - - "10.20.36.0" - - "192.168.255.0" + attributes: + - network + - address + - .id + where: + - attribute: "network" + is: "==" + value: "192.168.255.0" + - or: + - attribute: "address" + is: "!=" + value: "192.168.255.10/24" + - attribute: "address" + is: "eq" + value: "10.20.36.20/24" + - attribute: "network" + is: "in" + value: + - "10.20.36.0" + - "192.168.255.0" - name: Update example - ether2 ip addres with ".id = *14" community.routeros.api: From 660d446c3322efc8ee0cf659614653beac0f2c51 Mon Sep 17 00:00:00 2001 From: Nikolay Dachev Date: Mon, 23 May 2022 09:33:58 +0300 Subject: [PATCH 25/27] Update plugins/modules/api.py Co-authored-by: Felix Fontein --- plugins/modules/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index a1d15f42..39d1f44e 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -62,7 +62,7 @@ type: str extended_query: description: - - Extended query given path for selected query attributes from RouterOS aip. + - Extended query given path for selected query attributes from RouterOS API. - Extended query allow conjunctive input, in such case 'no results' will be returned. type: dict suboptions: From a98e096bfa220890c88e7d1272e2e5cfa1446f6d Mon Sep 17 00:00:00 2001 From: Nikolay Dachev Date: Mon, 23 May 2022 09:34:08 +0300 Subject: [PATCH 26/27] Update plugins/modules/api.py Co-authored-by: Felix Fontein --- plugins/modules/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index 39d1f44e..b44555fc 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -63,7 +63,7 @@ extended_query: description: - Extended query given path for selected query attributes from RouterOS API. - - Extended query allow conjunctive input, in such case 'no results' will be returned. + - Extended query allow conjunctive input. If there is no matching entry, an empty list will be returned. type: dict suboptions: attributes: From d35d1fbd245f3a5bcc57a26227ef30d33bb81aae Mon Sep 17 00:00:00 2001 From: Nikolay Dachev Date: Mon, 23 May 2022 02:57:37 -0400 Subject: [PATCH 27/27] fix example docs --- plugins/modules/api.py | 176 +++++++++++++++++++++++------------------ 1 file changed, 97 insertions(+), 79 deletions(-) diff --git a/plugins/modules/api.py b/plugins/modules/api.py index a1d15f42..82d7c66f 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -135,85 +135,103 @@ ''' EXAMPLES = ''' ---- - tasks: - - name: Get example - ip address print - community.routeros.api: - hostname: "{{ hostname }}" - password: "{{ password }}" - username: "{{ username }}" - path: "ip address" - - - name: Add example - ip address - community.routeros.api: - hostname: "{{ hostname }}" - password: "{{ password }}" - username: "{{ username }}" - path: "ip address" - add: "address=192.168.255.10/24 interface=ether2" - - - name: Query example - ".id, address" in "ip address WHERE address == 192.168.255.10/24" - community.routeros.api: - hostname: "{{ hostname }}" - password: "{{ password }}" - username: "{{ username }}" - path: "ip address" - query: ".id address WHERE address == {{ ip2 }}" - - - name: Extended query example - ".id,address,network" where address is not 192.168.255.10/24 or is 10.20.36.20/24 - community.routeros.api: - hostname: "{{ hostname }}" - password: "{{ password }}" - username: "{{ username }}" - path: "ip address" - extended_query: - attributes: - - network - - address - - .id - where: - - attribute: "network" - is: "==" - value: "192.168.255.0" - - or: - - attribute: "address" - is: "!=" - value: "192.168.255.10/24" - - attribute: "address" - is: "eq" - value: "10.20.36.20/24" - - attribute: "network" - is: "in" - value: - - "10.20.36.0" - - "192.168.255.0" - - - name: Update example - ether2 ip addres with ".id = *14" - community.routeros.api: - hostname: "{{ hostname }}" - password: "{{ password }}" - username: "{{ username }}" - path: "ip address" - update: >- - .id=*14 - address=192.168.255.20/24 - comment={{ 'Update 192.168.255.10/24 with .id=*14 to 192.168.255.20/24 on ether2' | community.routeros.quote_argument_value }} - - - name: Remove example - ether2 ip 192.168.255.20/24 with ".id = *14" - community.routeros.api: - hostname: "{{ hostname }}" - password: "{{ password }}" - username: "{{ username }}" - path: "ip address" - remove: "*14" - - - name: Arbitrary command example "/system identity print" - community.routeros.api: - hostname: "{{ hostname }}" - password: "{{ password }}" - username: "{{ username }}" - path: "system identity" - cmd: "print" +- name: Get example - ip address print + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "ip address" + register: ipaddrd_printout + +- name: Dump "Get example" output + ansible.builtin.debug: + msg: '{{ ipaddrd_printout }}' + +- name: Add example - ip address + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "ip address" + add: "address=192.168.255.10/24 interface=ether2" + +- name: Query example - ".id, address" in "ip address WHERE address == 192.168.255.10/24" + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "ip address" + query: ".id address WHERE address == {{ ip2 }}" + register: queryout + +- name: Dump "Query example" output + ansible.builtin.debug: + msg: '{{ queryout }}' + +- name: Extended query example - ".id,address,network" where address is not 192.168.255.10/24 or is 10.20.36.20/24 + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "ip address" + extended_query: + attributes: + - network + - address + - .id + where: + - attribute: "network" + is: "==" + value: "192.168.255.0" + - or: + - attribute: "address" + is: "!=" + value: "192.168.255.10/24" + - attribute: "address" + is: "eq" + value: "10.20.36.20/24" + - attribute: "network" + is: "in" + value: + - "10.20.36.0" + - "192.168.255.0" + register: extended_queryout + +- name: Dump "Extended query example" output + ansible.builtin.debug: + msg: '{{ extended_queryout }}' + +- name: Update example - ether2 ip addres with ".id = *14" + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "ip address" + update: >- + .id=*14 + address=192.168.255.20/24 + comment={{ 'Update 192.168.255.10/24 to 192.168.255.20/24 on ether2' | community.routeros.quote_argument_value }} + +- name: Remove example - ether2 ip 192.168.255.20/24 with ".id = *14" + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "ip address" + remove: "*14" + +- name: Arbitrary command example "/system identity print" + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "system identity" + cmd: "print" + register: arbitraryout + +- name: Dump "Arbitrary command example" output + ansible.builtin.debug: + msg: '{{ arbitraryout }}' ''' RETURN = '''