Skip to content

Commit 6da29fe

Browse files
authored
Improving handling of TXT values (#57)
* Add TXT value conversion facilities. * Improve error message handling. * Make WSDL encoder to handle all string types. * Use unicode instead of native strings for TXT entries. * Allow to not use octal encoding for TXT records. * Implement TXT value conversion. * Fix tests for Python 2 (encoding issues). * Fix tests. * Improve formulation. * Add more tests. * Improve unit tests by moving common code into helper. * Improve and test conversion error handling. * Replace normalized/dns -> unquoted/quoted.
1 parent 14e0b3e commit 6da29fe

40 files changed

+1687
-61
lines changed

plugins/doc_fragments/options.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,34 @@ class ModuleDocFragment(object):
2020
type: int
2121
default: 2
2222
'''
23+
24+
TXT_TRANSFORMATION = r'''
25+
options:
26+
txt_transformation:
27+
description:
28+
- Determines how TXT entry values are converted between the API and this module's
29+
input and output.
30+
- The value C(api) means that values are returned from this module as they are returned
31+
from the API, and pushed to the API as they have been passed to this module. For
32+
idempotency checks, the input string will be compared to the strings returned by the
33+
API. The API might automatically transform some values, like splitting long values or
34+
adding quotes, which can cause problems with idempotency.
35+
- The value C(unquoted) automatically transforms values so that you can pass in unquoted
36+
values, and the module will return unquoted values. If you pass in quoted values, they
37+
will be double-quoted.
38+
- The value C(quoted) automatically transforms values so that you must use quoting for values
39+
that contain spaces, characters such as quotation marks and backslashes, and that are
40+
longer than 255 bytes. It also makes sure to return values from the API in a normalized
41+
encoding.
42+
- The default value, C(unquoted), ensures that you can work with values without having
43+
to care about how to correctly quote for DNS. Most users should use one of C(unquoted)
44+
or C(quoted), but not C(api).
45+
- B(Note:) the conversion code assumes UTF-8 encoding for values. If you need another
46+
encoding use I(txt_transformation=api) and handle the encoding yourself.
47+
type: str
48+
choices:
49+
- api
50+
- quoted
51+
- unquoted
52+
default: unquoted
53+
'''
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (c) 2021 Felix Fontein
4+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5+
6+
from __future__ import (absolute_import, division, print_function)
7+
__metaclass__ = type
8+
9+
10+
class DNSConversionError(Exception):
11+
def __init__(self, message):
12+
super(DNSConversionError, self).__init__(message)
13+
self.error_message = message
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (c) 2021 Felix Fontein
4+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5+
6+
from __future__ import (absolute_import, division, print_function)
7+
__metaclass__ = type
8+
9+
10+
from ansible.module_utils.common.text.converters import to_text
11+
from ansible.module_utils.six import raise_from
12+
13+
from ansible_collections.community.dns.plugins.module_utils.record import (
14+
DNSRecord,
15+
)
16+
17+
from ansible_collections.community.dns.plugins.module_utils.conversion.base import (
18+
DNSConversionError,
19+
)
20+
21+
from ansible_collections.community.dns.plugins.module_utils.conversion.txt import (
22+
decode_txt_value,
23+
encode_txt_value,
24+
)
25+
26+
27+
class RecordConverter(object):
28+
def __init__(self, provider_information, option_provider):
29+
"""
30+
Create a record converter.
31+
"""
32+
self._provider_information = provider_information
33+
self._option_provider = option_provider
34+
35+
self._txt_api_handling = self._provider_information.txt_record_handling() # 'decoded', 'encoded', 'encoded-no-octal'
36+
self._txt_transformation = self._option_provider.get_option('txt_transformation') # 'api', 'quoted', 'unquoted'
37+
38+
def _handle_txt_api(self, to_api, record):
39+
"""
40+
Handle TXT records for sending to/from the API.
41+
"""
42+
if self._txt_transformation == 'api':
43+
# Do not touch record values
44+
return
45+
46+
# We assume that records internally use decoded values
47+
if self._txt_api_handling in ('encoded', 'encoded-no-octal'):
48+
if to_api:
49+
record.target = encode_txt_value(record.target, use_octal=self._txt_api_handling == 'encoded')
50+
else:
51+
record.target = decode_txt_value(record.target)
52+
53+
def _handle_txt_user(self, to_user, record):
54+
"""
55+
Handle TXT records for sending to/from the user.
56+
"""
57+
if self._txt_transformation == 'api':
58+
# Do not touch record values
59+
return
60+
61+
# We assume that records internally use decoded values
62+
if self._txt_transformation == 'quoted':
63+
if to_user:
64+
record.target = encode_txt_value(record.target)
65+
else:
66+
record.target = decode_txt_value(record.target)
67+
68+
def process_from_api(self, record):
69+
"""
70+
Process a record object (DNSRecord) after receiving from API.
71+
Modifies the record in-place.
72+
"""
73+
try:
74+
record.target = to_text(record.target)
75+
if record.type == 'TXT':
76+
self._handle_txt_api(False, record)
77+
return record
78+
except DNSConversionError as e:
79+
raise_from(DNSConversionError(u'While processing record from API: {0}'.format(e.error_message)), e)
80+
81+
def process_to_api(self, record):
82+
"""
83+
Process a record object (DNSRecord) for sending to API.
84+
Modifies the record in-place.
85+
"""
86+
try:
87+
if record.type == 'TXT':
88+
self._handle_txt_api(True, record)
89+
return record
90+
except DNSConversionError as e:
91+
raise_from(DNSConversionError(u'While processing record for the API: {0}'.format(e.error_message)), e)
92+
93+
def process_from_user(self, record):
94+
"""
95+
Process a record object (DNSRecord) after receiving from the user.
96+
Modifies the record in-place.
97+
"""
98+
try:
99+
record.target = to_text(record.target)
100+
if record.type == 'TXT':
101+
self._handle_txt_user(False, record)
102+
return record
103+
except DNSConversionError as e:
104+
raise_from(DNSConversionError(u'While processing record from the user: {0}'.format(e.error_message)), e)
105+
106+
def process_to_user(self, record):
107+
"""
108+
Process a record object (DNSRecord) for sending to the user.
109+
Modifies the record in-place.
110+
"""
111+
try:
112+
if record.type == 'TXT':
113+
self._handle_txt_user(True, record)
114+
return record
115+
except DNSConversionError as e:
116+
raise_from(DNSConversionError(u'While processing record for the user: {0}'.format(e.error_message)), e)
117+
118+
def clone_from_api(self, record):
119+
"""
120+
Process a record object (DNSRecord) after receiving from API.
121+
Return a modified clone of the record; the original will not be modified.
122+
"""
123+
record = record.clone()
124+
self.process_from_api(record)
125+
return record
126+
127+
def clone_to_api(self, record):
128+
"""
129+
Process a record object (DNSRecord) for sending to API.
130+
Return a modified clone of the record; the original will not be modified.
131+
"""
132+
record = record.clone()
133+
self.process_to_api(record)
134+
return record
135+
136+
def clone_multiple_from_api(self, records):
137+
"""
138+
Process a list of record object (DNSRecord) after receiving from API.
139+
Return a list of modified clones of the records; the originals will not be modified.
140+
"""
141+
return [self.clone_from_api(record) for record in records]
142+
143+
def clone_multiple_to_api(self, records):
144+
"""
145+
Process a list of record objects (DNSRecord) for sending to API.
146+
Return a list of modified clones of the records; the originals will not be modified.
147+
"""
148+
return [self.clone_to_api(record) for record in records]
149+
150+
def process_multiple_from_api(self, records):
151+
"""
152+
Process a list of record object (DNSRecord) after receiving from API.
153+
Modifies the records in-place.
154+
"""
155+
for record in records:
156+
self.process_from_api(record)
157+
return records
158+
159+
def process_multiple_to_api(self, records):
160+
"""
161+
Process a list of record objects (DNSRecord) for sending to API.
162+
Modifies the records in-place.
163+
"""
164+
for record in records:
165+
self.process_to_api(record)
166+
return records
167+
168+
def process_multiple_from_user(self, records):
169+
"""
170+
Process a list of record object (DNSRecord) after receiving from the user.
171+
Modifies the records in-place.
172+
"""
173+
for record in records:
174+
self.process_from_user(record)
175+
return records
176+
177+
def process_multiple_to_user(self, records):
178+
"""
179+
Process a list of record objects (DNSRecord) for sending to the user.
180+
Modifies the records in-place.
181+
"""
182+
for record in records:
183+
self.process_to_user(record)
184+
return records
185+
186+
def process_value_from_user(self, record_type, value):
187+
"""
188+
Process a record value (string) after receiving from the user.
189+
"""
190+
record = DNSRecord()
191+
record.type = record_type
192+
record.target = value
193+
self.process_from_user(record)
194+
return record.target
195+
196+
def process_values_from_user(self, record_type, values):
197+
"""
198+
Process a list of record values (strings) after receiving from the user.
199+
"""
200+
return [self.process_value_from_user(record_type, value) for value in values]
201+
202+
def process_value_to_user(self, record_type, value):
203+
"""
204+
Process a record value (string) for sending to the user.
205+
"""
206+
record = DNSRecord()
207+
record.type = record_type
208+
record.target = value
209+
self.process_to_user(record)
210+
return record.target
211+
212+
def process_values_to_user(self, record_type, values):
213+
"""
214+
Process a list of record values (strings) for sending to the user.
215+
"""
216+
return [self.process_value_to_user(record_type, value) for value in values]

0 commit comments

Comments
 (0)