cli2 4.2.7__tar.gz → 4.2.9__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.9}/PKG-INFO +1 -1
- {cli2-4.2.7 → cli2-4.2.9}/cli2/__init__.py +1 -1
- {cli2-4.2.7 → cli2-4.2.9}/cli2/ansible/action.py +148 -47
- {cli2-4.2.7 → cli2-4.2.9/cli2.egg-info}/PKG-INFO +1 -1
- {cli2-4.2.7 → cli2-4.2.9}/setup.py +1 -1
- {cli2-4.2.7 → cli2-4.2.9}/tests/test_ansible.py +10 -8
- {cli2-4.2.7 → cli2-4.2.9}/MANIFEST.in +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/README.rst +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/classifiers.txt +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/ansible/__init__.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/ansible/playbook.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/ansible/variables.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/asyncio.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/cli.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/cli2.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/client.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/colors.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/configuration.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/decorators.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/display.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/__init__.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/client.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/conf.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/example.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/example_obj.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/nesting.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/obj.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/obj2.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/test.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/lock.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/log.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/node.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/sphinx.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/table.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2/test.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2.egg-info/SOURCES.txt +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2.egg-info/dependency_links.txt +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2.egg-info/entry_points.txt +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2.egg-info/requires.txt +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/cli2.egg-info/top_level.txt +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/setup.cfg +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/tests/test_ansible_variables.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/tests/test_asyncio.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/tests/test_cli.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/tests/test_client.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/tests/test_command.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/tests/test_configuration.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/tests/test_decorators.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/tests/test_display.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/tests/test_entry_point.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/tests/test_group.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/tests/test_inject.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/tests/test_lock.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/tests/test_node.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/tests/test_restful.py +0 -0
- {cli2-4.2.7 → cli2-4.2.9}/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,23 @@ 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(
|
|
190
|
+
self._templar.template(self.task_vars[key])
|
|
191
|
+
)
|
|
192
|
+
if self._task.args.get(key, None):
|
|
193
|
+
self.masked_values.append(
|
|
194
|
+
self._templar.template(self._task.args[key])
|
|
195
|
+
)
|
|
196
|
+
|
|
158
197
|
async def run_wrapped_async(self):
|
|
159
198
|
self.verbosity = self.task_vars.get('ansible_verbosity', 0)
|
|
160
199
|
|
|
161
|
-
if 'LOG_LEVEL' not in os.environ and 'DEBUG'
|
|
200
|
+
if 'LOG_LEVEL' not in os.environ and os.getenv('DEBUG'):
|
|
162
201
|
if self.verbosity == 1:
|
|
163
202
|
os.environ['LOG_LEVEL'] = 'INFO'
|
|
164
203
|
elif self.verbosity >= 2:
|
|
@@ -170,6 +209,7 @@ class ActionBase(ActionBase):
|
|
|
170
209
|
self.client = await self.client_factory()
|
|
171
210
|
except NotImplementedError:
|
|
172
211
|
self.client = None
|
|
212
|
+
self.collect_masked_values()
|
|
173
213
|
await self.run_async()
|
|
174
214
|
except Exception as exc:
|
|
175
215
|
self.result['failed'] = True
|
|
@@ -196,56 +236,72 @@ class ActionBase(ActionBase):
|
|
|
196
236
|
self.exc = exc
|
|
197
237
|
finally:
|
|
198
238
|
if self.mask and self.verbosity:
|
|
199
|
-
|
|
200
|
-
cli2.print(self.masked_result())
|
|
239
|
+
self.print_yaml(self.result)
|
|
201
240
|
|
|
202
241
|
if (
|
|
203
242
|
self._before_data != UNSET_DEFAULT
|
|
204
243
|
and self._after_data != UNSET_DEFAULT
|
|
205
244
|
):
|
|
206
|
-
diff =
|
|
207
|
-
|
|
208
|
-
|
|
245
|
+
diff = difflib.unified_diff(
|
|
246
|
+
to_nice_yaml(
|
|
247
|
+
self.mask_data(self._before_data)
|
|
248
|
+
).splitlines(),
|
|
249
|
+
to_nice_yaml(
|
|
250
|
+
self.mask_data(self._after_data)
|
|
251
|
+
).splitlines(),
|
|
209
252
|
self._before_label,
|
|
210
253
|
self._after_label,
|
|
211
254
|
)
|
|
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)
|
|
255
|
+
cli2.diff(diff)
|
|
224
256
|
|
|
225
|
-
def
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
257
|
+
def print_yaml(self, data):
|
|
258
|
+
"""
|
|
259
|
+
Render data as masked yaml.
|
|
260
|
+
"""
|
|
261
|
+
data = self.mask_data(data)
|
|
262
|
+
yaml = to_nice_yaml(data)
|
|
263
|
+
rendered = cli2.yaml_highlight(yaml)
|
|
264
|
+
self.print(rendered)
|
|
265
|
+
|
|
266
|
+
def print(self, data):
|
|
267
|
+
# this serves for mocking
|
|
268
|
+
print(data)
|
|
269
|
+
|
|
270
|
+
def mask_data(self, data):
|
|
271
|
+
""""
|
|
272
|
+
Do our best to mask sensitive values in the data param recursively.
|
|
273
|
+
|
|
274
|
+
- when data is a dict: it is recursively iterated on, any value that in
|
|
275
|
+
is :py:attr:`masked_keys` will have it's value replaced with
|
|
276
|
+
``***MASKED***``, also, the value is added to
|
|
277
|
+
:py:attr:`masked_values`.
|
|
278
|
+
- when data is a string, each :py:attr:`masked_values` will be replaced
|
|
279
|
+
with ``***MASKED***``, so we're actually able to mask sensitive
|
|
280
|
+
information from stdout outputs and the likes.
|
|
281
|
+
- when data is a list, each item is passed to :py:meth:`mask_data()`.
|
|
282
|
+
|
|
283
|
+
Note that the :envvar:`DEBUG` environment variable will prevent any
|
|
284
|
+
masking at all.
|
|
285
|
+
"""
|
|
286
|
+
if os.getenv('DEBUG'):
|
|
247
287
|
return data
|
|
248
|
-
|
|
288
|
+
|
|
289
|
+
return self._mask(copy.deepcopy(data))
|
|
290
|
+
|
|
291
|
+
def _mask(self, data):
|
|
292
|
+
if isinstance(data, dict):
|
|
293
|
+
for key, value in data.items():
|
|
294
|
+
if key in self.masked_keys:
|
|
295
|
+
self.masked_values.append(self._templar.template(value))
|
|
296
|
+
data[key] = '***MASKED***'
|
|
297
|
+
else:
|
|
298
|
+
data[key] = self.mask_data(value)
|
|
299
|
+
elif isinstance(data, list):
|
|
300
|
+
return [self.mask_data(item) for item in data]
|
|
301
|
+
elif isinstance(data, str):
|
|
302
|
+
for value in self.masked_values:
|
|
303
|
+
data = data.replace(str(value), '***MASKED***')
|
|
304
|
+
return data
|
|
249
305
|
|
|
250
306
|
async def run_async(self):
|
|
251
307
|
"""
|
|
@@ -277,14 +333,24 @@ class ActionBase(ActionBase):
|
|
|
277
333
|
:param fail: Allow this test to fail without exception
|
|
278
334
|
"""
|
|
279
335
|
from unittest import mock
|
|
280
|
-
|
|
336
|
+
from ansible.template import Templar
|
|
337
|
+
from ansible.parsing.dataloader import DataLoader
|
|
338
|
+
loader = DataLoader()
|
|
339
|
+
obj = cls(
|
|
340
|
+
task=mock.Mock(),
|
|
341
|
+
connection=mock.Mock(),
|
|
342
|
+
play_context=mock.Mock(),
|
|
343
|
+
loader=loader,
|
|
344
|
+
templar=Templar(loader, variables=facts),
|
|
345
|
+
shared_loader_obj=mock.Mock(),
|
|
346
|
+
)
|
|
281
347
|
obj.tmp = None
|
|
282
348
|
obj.result = dict()
|
|
283
|
-
obj._task = mock.Mock()
|
|
284
349
|
obj._task.args = args or {}
|
|
285
350
|
obj.task_vars = facts or {}
|
|
286
351
|
obj.task_vars.setdefault('ansible_verbosity', 2)
|
|
287
352
|
obj.exc = False
|
|
353
|
+
obj.masked_values = []
|
|
288
354
|
if client:
|
|
289
355
|
async def _factory():
|
|
290
356
|
return client
|
|
@@ -315,7 +381,7 @@ class ActionBase(ActionBase):
|
|
|
315
381
|
:param data: Dictionnary of data
|
|
316
382
|
:param label: Label to show in diff
|
|
317
383
|
"""
|
|
318
|
-
self._before_data =
|
|
384
|
+
self._before_data = data
|
|
319
385
|
self._before_label = label
|
|
320
386
|
|
|
321
387
|
def after_set(self, data, label='after'):
|
|
@@ -325,5 +391,40 @@ class ActionBase(ActionBase):
|
|
|
325
391
|
:param data: Dictionnary of data
|
|
326
392
|
:param label: Label to show in diff
|
|
327
393
|
"""
|
|
328
|
-
self._after_data =
|
|
394
|
+
self._after_data = data
|
|
329
395
|
self._after_label = label
|
|
396
|
+
|
|
397
|
+
def subprocess_remote(self, cmd, **kwargs):
|
|
398
|
+
"""
|
|
399
|
+
Execute a shell command on the remote in a masked context
|
|
400
|
+
|
|
401
|
+
:param cmd: Command to run
|
|
402
|
+
:param kwargs: Other shell args, such as creates etc
|
|
403
|
+
"""
|
|
404
|
+
new_task = self._task.copy()
|
|
405
|
+
new_task.args = dict(_raw_params=cmd, **kwargs)
|
|
406
|
+
if self.verbosity:
|
|
407
|
+
display.display(
|
|
408
|
+
f'<{self.task_vars["inventory_hostname"]}> + '
|
|
409
|
+
+ self.mask_data(cmd),
|
|
410
|
+
color='blue',
|
|
411
|
+
)
|
|
412
|
+
shell_action = self._shared_loader_obj.action_loader.get(
|
|
413
|
+
'ansible.builtin.shell',
|
|
414
|
+
task=new_task,
|
|
415
|
+
connection=self._connection,
|
|
416
|
+
play_context=self._play_context,
|
|
417
|
+
loader=self._loader,
|
|
418
|
+
templar=self._templar,
|
|
419
|
+
shared_loader_obj=self._shared_loader_obj,
|
|
420
|
+
)
|
|
421
|
+
result = shell_action.run(task_vars=self.task_vars.copy())
|
|
422
|
+
|
|
423
|
+
if self.verbosity:
|
|
424
|
+
if 'stderr_lines' in result:
|
|
425
|
+
print(self.mask_data(result['stderr']))
|
|
426
|
+
if 'stdout_lines' in result:
|
|
427
|
+
print(self.mask_data(result['stdout']))
|
|
428
|
+
|
|
429
|
+
result.pop('invocation')
|
|
430
|
+
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
|