mirror of
https://github.com/ansible-collections/community.routeros.git
synced 2025-06-21 09:35:45 +02:00
Add module_utils and filters for quoting and unquoting (#53)
* Move splitting code to own file. * Move list to dictionary code to quoting as well. * Add quoting functionality. * Add quoting filters. * Add integration tests to CI. * Fix bugs, increase coverage. * Make parsing more strict. * Extract function parse_argument_value from split_routeros_command to make proper parsing of WHERE possible. * Adjust expected error message in integration tests. * Simplify code and improve coverage. * Add changelog fragment for WHERE strictness in api module. * Add documenation. * Fix typo. * Add documentation references. * Add example to api module which uses quote_argument_value. * Fix bug, and add tests which prevent this in the future. * Add more escape sequence tests. * Make sure all control characters are quoted.
This commit is contained in:
parent
f9d246cd7a
commit
d73eb1c144
14 changed files with 803 additions and 145 deletions
|
@ -119,6 +119,11 @@ options:
|
|||
- See also I(validate_cert_hostname). Only used when I(tls=true) and I(validate_certs=true).
|
||||
type: path
|
||||
version_added: 1.2.0
|
||||
seealso:
|
||||
- ref: ansible_collections.community.routeros.docsite.api-guide
|
||||
description: How to connect to RouterOS devices with the RouterOS API
|
||||
- ref: ansible_collections.community.routeros.docsite.quoting
|
||||
description: How to quote and unquote commands and arguments
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
@ -190,7 +195,10 @@ EXAMPLES = '''
|
|||
password: "{{ password }}"
|
||||
username: "{{ username }}"
|
||||
path: "{{ path }}"
|
||||
update: ".id={{ query_id }} address={{ ip3 }}"
|
||||
update: >-
|
||||
.id={{ query_id }}
|
||||
address={{ ip3 }}
|
||||
comment={{ 'A comment with spaces' | community.routeros.quote_argument_value }}
|
||||
register: updateout
|
||||
|
||||
- name: Dump "Update" output
|
||||
|
@ -258,8 +266,16 @@ message:
|
|||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.routeros.plugins.module_utils.quoting import (
|
||||
ParseError,
|
||||
convert_list_to_dictionary,
|
||||
parse_argument_value,
|
||||
split_routeros_command,
|
||||
)
|
||||
|
||||
import re
|
||||
import ssl
|
||||
import traceback
|
||||
|
||||
|
@ -274,95 +290,6 @@ except Exception as e:
|
|||
LIB_IMP_ERR = traceback.format_exc()
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
ESCAPE_SEQUENCES = {
|
||||
b'"': b'"',
|
||||
b'\\': b'\\',
|
||||
b'?': b'?',
|
||||
b'$': b'$',
|
||||
b'_': b'_',
|
||||
b'a': b'\a',
|
||||
b'b': b'\b',
|
||||
b'f': b'\xFF',
|
||||
b'n': b'\n',
|
||||
b'r': b'\r',
|
||||
b't': b'\t',
|
||||
b'v': b'\v',
|
||||
}
|
||||
|
||||
ESCAPE_DIGITS = b'0123456789ABCDEF'
|
||||
|
||||
|
||||
def split_routeros(line):
|
||||
line = to_bytes(line)
|
||||
result = []
|
||||
current = []
|
||||
index = 0
|
||||
length = len(line)
|
||||
# States:
|
||||
# 0 = outside param
|
||||
# 1 = param before '='
|
||||
# 2 = param after '=' without quote
|
||||
# 3 = param after '=' with quote
|
||||
state = 0
|
||||
while index < length:
|
||||
ch = line[index:index + 1]
|
||||
index += 1
|
||||
if state == 0 and ch == b' ':
|
||||
pass
|
||||
elif state in (1, 2) and ch == b' ':
|
||||
state = 0
|
||||
result.append(b''.join(current))
|
||||
current = []
|
||||
elif ch == b'=' and state == 1:
|
||||
state = 2
|
||||
current.append(ch)
|
||||
if index + 1 < length and line[index:index + 1] == b'"':
|
||||
state = 3
|
||||
index += 1
|
||||
elif ch == b'"':
|
||||
if state == 3:
|
||||
state = 0
|
||||
result.append(b''.join(current))
|
||||
current = []
|
||||
if index + 1 < length and line[index:index + 1] != b' ':
|
||||
raise ParseError('Ending \'"\' must be followed by space or end of string')
|
||||
else:
|
||||
raise ParseError('\'"\' must follow \'=\'')
|
||||
elif ch == b'\\':
|
||||
if index + 1 == length:
|
||||
raise ParseError('\'\\\' must not be at the end of the line')
|
||||
ch = line[index:index + 1]
|
||||
index += 1
|
||||
if ch in ESCAPE_SEQUENCES:
|
||||
current.append(ch)
|
||||
else:
|
||||
d1 = ESCAPE_DIGITS.find(ch)
|
||||
if d1 < 0:
|
||||
raise ParseError('Invalid escape sequence \'\\{0}\''.format(ch))
|
||||
if index + 1 == length:
|
||||
raise ParseError('Hex escape sequence cut off at end of line')
|
||||
ch2 = line[index:index + 1]
|
||||
d2 = ESCAPE_DIGITS.find(ch2)
|
||||
index += 1
|
||||
if d2 < 0:
|
||||
raise ParseError('Invalid hex escape sequence \'\\{0}{1}\''.format(ch, ch2))
|
||||
result.append(chr(d1 * 16 + d2))
|
||||
else:
|
||||
current.append(ch)
|
||||
if state == 0:
|
||||
state = 1
|
||||
if state in (1, 2):
|
||||
if current:
|
||||
result.append(b''.join(current))
|
||||
elif state == 3:
|
||||
raise ParseError('Unexpected end of string during escaped parameter')
|
||||
return [to_native(part) for part in result]
|
||||
|
||||
|
||||
class ROS_api_module:
|
||||
def __init__(self):
|
||||
module_args = dict(
|
||||
|
@ -410,7 +337,24 @@ class ROS_api_module:
|
|||
self.where = None
|
||||
self.query = self.module.params['query']
|
||||
if self.query:
|
||||
self.query = self.list_remove_empty(self.split_params(self.query))
|
||||
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 '<attribute> <operator> <value>'
|
||||
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:]
|
||||
|
@ -439,26 +383,14 @@ class ROS_api_module:
|
|||
else:
|
||||
self.api_get_all()
|
||||
|
||||
def list_remove_empty(self, check_list):
|
||||
while("" in check_list):
|
||||
check_list.remove("")
|
||||
return check_list
|
||||
|
||||
def list_to_dic(self, ldict):
|
||||
dict = {}
|
||||
for p in ldict:
|
||||
if '=' not in p:
|
||||
self.errors("missing '=' after '%s'" % p)
|
||||
p = p.split('=', 1)
|
||||
if p[1]:
|
||||
dict[p[0]] = p[1]
|
||||
return dict
|
||||
return convert_list_to_dictionary(ldict, skip_empty_values=True, require_assignment=True)
|
||||
|
||||
def split_params(self, params):
|
||||
if not isinstance(params, str):
|
||||
raise AssertionError('Parameters can only be a string, received %s' % type(params))
|
||||
try:
|
||||
return split_routeros(params)
|
||||
return split_routeros_command(params)
|
||||
except ParseError as e:
|
||||
self.module.fail_json(msg=to_native(e))
|
||||
|
||||
|
@ -512,11 +444,6 @@ class ROS_api_module:
|
|||
keys[k] = Key(k)
|
||||
try:
|
||||
if self.where:
|
||||
if len(self.where) < 3:
|
||||
self.errors("invalid syntax for 'WHERE %s'"
|
||||
% ' '.join(self.where))
|
||||
|
||||
where = []
|
||||
if self.where[1] == '==':
|
||||
select = self.api_path.select(*keys).where(keys[self.where[0]] == self.where[2])
|
||||
elif self.where[1] == '!=':
|
||||
|
@ -528,11 +455,10 @@ class ROS_api_module:
|
|||
else:
|
||||
self.errors("'%s' is not operator for 'where'"
|
||||
% self.where[1])
|
||||
for row in select:
|
||||
self.result['message'].append(row)
|
||||
else:
|
||||
for row in self.api_path.select(*keys):
|
||||
self.result['message'].append(row)
|
||||
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))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue