cli2 4.2.7__tar.gz → 4.2.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cli2-4.2.7/cli2.egg-info → cli2-4.2.8}/PKG-INFO +1 -1
- {cli2-4.2.7 → cli2-4.2.8}/cli2/__init__.py +1 -1
- {cli2-4.2.7 → cli2-4.2.8}/cli2/ansible/action.py +133 -45
- {cli2-4.2.7 → cli2-4.2.8/cli2.egg-info}/PKG-INFO +1 -1
- {cli2-4.2.7 → cli2-4.2.8}/setup.py +1 -1
- {cli2-4.2.7 → cli2-4.2.8}/tests/test_ansible.py +10 -8
- {cli2-4.2.7 → cli2-4.2.8}/MANIFEST.in +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/README.rst +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/classifiers.txt +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/ansible/__init__.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/ansible/playbook.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/ansible/variables.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/asyncio.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/cli.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/cli2.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/client.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/colors.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/configuration.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/decorators.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/display.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/__init__.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/client.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/conf.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/example.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/example_obj.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/nesting.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/obj.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/obj2.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/test.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/lock.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/log.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/node.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/sphinx.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/table.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2/test.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2.egg-info/SOURCES.txt +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2.egg-info/dependency_links.txt +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2.egg-info/entry_points.txt +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2.egg-info/requires.txt +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/cli2.egg-info/top_level.txt +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/setup.cfg +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/tests/test_ansible_variables.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/tests/test_asyncio.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/tests/test_cli.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/tests/test_client.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/tests/test_command.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/tests/test_configuration.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/tests/test_decorators.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/tests/test_display.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/tests/test_entry_point.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/tests/test_group.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/tests/test_inject.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/tests/test_lock.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/tests/test_node.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/tests/test_restful.py +0 -0
- {cli2-4.2.7 → cli2-4.2.8}/tests/test_table.py +0 -0
|
@@ -38,7 +38,7 @@ except ImportError:
|
|
|
38
38
|
raise
|
|
39
39
|
# httpx not installed
|
|
40
40
|
pass
|
|
41
|
-
from .display import diff, diff_data, render, print, highlight
|
|
41
|
+
from .display import diff, diff_data, render, print, highlight, yaml_highlight
|
|
42
42
|
from .lock import Lock
|
|
43
43
|
from .log import configure, log
|
|
44
44
|
from .table import Table
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Base class for Ansible Actions.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import cli2
|
|
7
7
|
import copy
|
|
8
|
+
import difflib
|
|
8
9
|
import os
|
|
9
10
|
import re
|
|
10
11
|
import traceback
|
|
11
12
|
|
|
12
|
-
from ansible.parsing.yaml.objects import AnsibleUnicode
|
|
13
13
|
from ansible.plugins.action import ActionBase
|
|
14
|
+
from ansible.plugins.action import display
|
|
15
|
+
from ansible.plugins.filter.core import to_nice_yaml
|
|
14
16
|
|
|
15
17
|
# colors:
|
|
16
18
|
# black
|
|
@@ -133,8 +135,32 @@ class ActionBase(ActionBase):
|
|
|
133
135
|
List of result keys to mask, if set, this module will print the result
|
|
134
136
|
with masked values, so you can set no_log: True for the task and still
|
|
135
137
|
see most of the result.
|
|
138
|
+
|
|
139
|
+
.. py:attribute:: masked_keys
|
|
140
|
+
|
|
141
|
+
Property which returns the list of keys in :py:attr:`mask` + those in
|
|
142
|
+
the `mask` fact + those in the client if any.
|
|
143
|
+
|
|
144
|
+
.. py:attribute:: masked_values
|
|
145
|
+
|
|
146
|
+
First, the plugin will iterate over args and facts to learn all values
|
|
147
|
+
which are in :py:attr:`masked_keys`.
|
|
148
|
+
|
|
149
|
+
This is then both used and provisioned by :py:meth:`mask_data()`.
|
|
136
150
|
"""
|
|
137
151
|
mask = None
|
|
152
|
+
masked = Option(fact='mask', default=[])
|
|
153
|
+
masked_values = None
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def masked_keys(self):
|
|
157
|
+
result = []
|
|
158
|
+
if self.mask:
|
|
159
|
+
result += self.mask
|
|
160
|
+
result += self.masked
|
|
161
|
+
if self.client and self.client.mask:
|
|
162
|
+
result += self.client.mask
|
|
163
|
+
return result
|
|
138
164
|
|
|
139
165
|
def get(self, arg_name=None, fact_name=None, default=UNSET_DEFAULT):
|
|
140
166
|
if arg_name and arg_name in self._task.args:
|
|
@@ -155,10 +181,19 @@ class ActionBase(ActionBase):
|
|
|
155
181
|
asyncio.run(self.run_wrapped_async())
|
|
156
182
|
return self.result
|
|
157
183
|
|
|
184
|
+
def collect_masked_values(self):
|
|
185
|
+
# collect all values that have to be masked
|
|
186
|
+
self.masked_values = []
|
|
187
|
+
for key in self.masked_keys:
|
|
188
|
+
if self.task_vars.get(key, None):
|
|
189
|
+
self.masked_values.append(self.task_vars[key])
|
|
190
|
+
if self._task.args.get(key, None):
|
|
191
|
+
self.masked_values.append(self._task.args[key])
|
|
192
|
+
|
|
158
193
|
async def run_wrapped_async(self):
|
|
159
194
|
self.verbosity = self.task_vars.get('ansible_verbosity', 0)
|
|
160
195
|
|
|
161
|
-
if 'LOG_LEVEL' not in os.environ and 'DEBUG'
|
|
196
|
+
if 'LOG_LEVEL' not in os.environ and os.getenv('DEBUG'):
|
|
162
197
|
if self.verbosity == 1:
|
|
163
198
|
os.environ['LOG_LEVEL'] = 'INFO'
|
|
164
199
|
elif self.verbosity >= 2:
|
|
@@ -170,6 +205,7 @@ class ActionBase(ActionBase):
|
|
|
170
205
|
self.client = await self.client_factory()
|
|
171
206
|
except NotImplementedError:
|
|
172
207
|
self.client = None
|
|
208
|
+
self.collect_masked_values()
|
|
173
209
|
await self.run_async()
|
|
174
210
|
except Exception as exc:
|
|
175
211
|
self.result['failed'] = True
|
|
@@ -196,56 +232,72 @@ class ActionBase(ActionBase):
|
|
|
196
232
|
self.exc = exc
|
|
197
233
|
finally:
|
|
198
234
|
if self.mask and self.verbosity:
|
|
199
|
-
|
|
200
|
-
cli2.print(self.masked_result())
|
|
235
|
+
self.print_yaml(self.result)
|
|
201
236
|
|
|
202
237
|
if (
|
|
203
238
|
self._before_data != UNSET_DEFAULT
|
|
204
239
|
and self._after_data != UNSET_DEFAULT
|
|
205
240
|
):
|
|
206
|
-
diff =
|
|
207
|
-
|
|
208
|
-
|
|
241
|
+
diff = difflib.unified_diff(
|
|
242
|
+
to_nice_yaml(
|
|
243
|
+
self.mask_data(self._before_data)
|
|
244
|
+
).splitlines(),
|
|
245
|
+
to_nice_yaml(
|
|
246
|
+
self.mask_data(self._after_data)
|
|
247
|
+
).splitlines(),
|
|
209
248
|
self._before_label,
|
|
210
249
|
self._after_label,
|
|
211
250
|
)
|
|
212
|
-
|
|
213
|
-
output = '\n'.join([
|
|
214
|
-
line.rstrip() for line in diff if line.strip()
|
|
215
|
-
])
|
|
216
|
-
output = re.sub(
|
|
217
|
-
f'({"|".join(self.client.mask)}): (.*)',
|
|
218
|
-
'\\1: ***MASKED***',
|
|
219
|
-
''.join(output),
|
|
220
|
-
)
|
|
221
|
-
print(cli2.highlight(output, 'Diff'))
|
|
222
|
-
else:
|
|
223
|
-
cli2.diff(diff)
|
|
251
|
+
cli2.diff(diff)
|
|
224
252
|
|
|
225
|
-
def
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
253
|
+
def print_yaml(self, data):
|
|
254
|
+
"""
|
|
255
|
+
Render data as masked yaml.
|
|
256
|
+
"""
|
|
257
|
+
data = self.mask_data(data)
|
|
258
|
+
yaml = to_nice_yaml(data)
|
|
259
|
+
rendered = cli2.yaml_highlight(yaml)
|
|
260
|
+
self.print(rendered)
|
|
261
|
+
|
|
262
|
+
def print(self, data):
|
|
263
|
+
# this serves for mocking
|
|
264
|
+
print(data)
|
|
265
|
+
|
|
266
|
+
def mask_data(self, data):
|
|
267
|
+
""""
|
|
268
|
+
Do our best to mask sensitive values in the data param recursively.
|
|
269
|
+
|
|
270
|
+
- when data is a dict: it is recursively iterated on, any value that in
|
|
271
|
+
is :py:attr:`masked_keys` will have it's value replaced with
|
|
272
|
+
``***MASKED***``, also, the value is added to
|
|
273
|
+
:py:attr:`masked_values`.
|
|
274
|
+
- when data is a string, each :py:attr:`masked_values` will be replaced
|
|
275
|
+
with ``***MASKED***``, so we're actually able to mask sensitive
|
|
276
|
+
information from stdout outputs and the likes.
|
|
277
|
+
- when data is a list, each item is passed to :py:meth:`mask_data()`.
|
|
278
|
+
|
|
279
|
+
Note that the :envvar:`DEBUG` environment variable will prevent any
|
|
280
|
+
masking at all.
|
|
281
|
+
"""
|
|
282
|
+
if os.getenv('DEBUG'):
|
|
247
283
|
return data
|
|
248
|
-
|
|
284
|
+
|
|
285
|
+
return self._mask(copy.deepcopy(data))
|
|
286
|
+
|
|
287
|
+
def _mask(self, data):
|
|
288
|
+
if isinstance(data, dict):
|
|
289
|
+
for key, value in data.items():
|
|
290
|
+
if key in self.masked_keys:
|
|
291
|
+
self.masked_values.append(value)
|
|
292
|
+
data[key] = '***MASKED***'
|
|
293
|
+
else:
|
|
294
|
+
data[key] = self.mask_data(value)
|
|
295
|
+
elif isinstance(data, list):
|
|
296
|
+
return [self.mask_data(item) for item in data]
|
|
297
|
+
elif isinstance(data, str):
|
|
298
|
+
for value in self.masked_values:
|
|
299
|
+
data = data.replace(str(value), '***MASKED***')
|
|
300
|
+
return data
|
|
249
301
|
|
|
250
302
|
async def run_async(self):
|
|
251
303
|
"""
|
|
@@ -285,6 +337,7 @@ class ActionBase(ActionBase):
|
|
|
285
337
|
obj.task_vars = facts or {}
|
|
286
338
|
obj.task_vars.setdefault('ansible_verbosity', 2)
|
|
287
339
|
obj.exc = False
|
|
340
|
+
obj.masked_values = []
|
|
288
341
|
if client:
|
|
289
342
|
async def _factory():
|
|
290
343
|
return client
|
|
@@ -315,7 +368,7 @@ class ActionBase(ActionBase):
|
|
|
315
368
|
:param data: Dictionnary of data
|
|
316
369
|
:param label: Label to show in diff
|
|
317
370
|
"""
|
|
318
|
-
self._before_data =
|
|
371
|
+
self._before_data = data
|
|
319
372
|
self._before_label = label
|
|
320
373
|
|
|
321
374
|
def after_set(self, data, label='after'):
|
|
@@ -325,5 +378,40 @@ class ActionBase(ActionBase):
|
|
|
325
378
|
:param data: Dictionnary of data
|
|
326
379
|
:param label: Label to show in diff
|
|
327
380
|
"""
|
|
328
|
-
self._after_data =
|
|
381
|
+
self._after_data = data
|
|
329
382
|
self._after_label = label
|
|
383
|
+
|
|
384
|
+
def subprocess_remote(self, cmd, **kwargs):
|
|
385
|
+
"""
|
|
386
|
+
Execute a shell command on the remote in a masked context
|
|
387
|
+
|
|
388
|
+
:param cmd: Command to run
|
|
389
|
+
:param kwargs: Other shell args, such as creates etc
|
|
390
|
+
"""
|
|
391
|
+
new_task = self._task.copy()
|
|
392
|
+
new_task.args = dict(_raw_params=cmd, **kwargs)
|
|
393
|
+
if self.verbosity:
|
|
394
|
+
display.display(
|
|
395
|
+
f'<{self.task_vars["inventory_hostname"]}> + '
|
|
396
|
+
+ self.mask_data(cmd),
|
|
397
|
+
color='blue',
|
|
398
|
+
)
|
|
399
|
+
shell_action = self._shared_loader_obj.action_loader.get(
|
|
400
|
+
'ansible.builtin.shell',
|
|
401
|
+
task=new_task,
|
|
402
|
+
connection=self._connection,
|
|
403
|
+
play_context=self._play_context,
|
|
404
|
+
loader=self._loader,
|
|
405
|
+
templar=self._templar,
|
|
406
|
+
shared_loader_obj=self._shared_loader_obj,
|
|
407
|
+
)
|
|
408
|
+
result = shell_action.run(task_vars=self.task_vars.copy())
|
|
409
|
+
|
|
410
|
+
if self.verbosity:
|
|
411
|
+
if 'stderr_lines' in result:
|
|
412
|
+
print(self.mask_data(result['stderr']))
|
|
413
|
+
if 'stdout_lines' in result:
|
|
414
|
+
print(self.mask_data(result['stdout']))
|
|
415
|
+
|
|
416
|
+
result.pop('invocation')
|
|
417
|
+
return result
|
|
@@ -12,19 +12,21 @@ class ActionModule(ansible.ActionBase):
|
|
|
12
12
|
mask = ['a']
|
|
13
13
|
|
|
14
14
|
async def run_async(self):
|
|
15
|
-
self.result['x'] = dict(a='a', b='b')
|
|
15
|
+
self.result['x'] = dict(a='a', b='b', c='c', d='foo a rrr')
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
@pytest.mark.asyncio
|
|
19
19
|
async def test_mask(monkeypatch):
|
|
20
20
|
printer = mock.Mock()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
assert module.result ==
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
monkeypatch.setattr(ActionModule, 'print', printer)
|
|
22
|
+
module = await ActionModule.run_test_async(facts=dict(mask=['b']))
|
|
23
|
+
# result is untouched
|
|
24
|
+
assert module.result == {'x':
|
|
25
|
+
{'a': 'a', 'b': 'b', 'c': 'c', 'd': 'foo a rrr'}
|
|
26
|
+
}
|
|
27
|
+
# output has proper masking
|
|
28
|
+
expected = "\x1b[94mx\x1b[39;49;00m:\x1b[37m\x1b[39;49;00m\n\x1b[37m \x1b[39;49;00m\x1b[94ma\x1b[39;49;00m:\x1b[37m \x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33m***MASKED***\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n\x1b[37m \x1b[39;49;00m\x1b[94mb\x1b[39;49;00m:\x1b[37m \x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[33m***MASKED***\x1b[39;49;00m\x1b[33m'\x1b[39;49;00m\x1b[37m\x1b[39;49;00m\n\x1b[37m \x1b[39;49;00m\x1b[94mc\x1b[39;49;00m:\x1b[37m \x1b[39;49;00mc\x1b[37m\x1b[39;49;00m\n\x1b[37m \x1b[39;49;00m\x1b[94md\x1b[39;49;00m:\x1b[37m \x1b[39;49;00mfoo ***MASKED*** rrr\x1b[37m\x1b[39;49;00m\n" # noqa
|
|
29
|
+
printer.assert_called_once_with(expected)
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
@pytest.mark.asyncio
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|