cli2 4.2.6__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.6/cli2.egg-info → cli2-4.2.8}/PKG-INFO +1 -1
- {cli2-4.2.6 → cli2-4.2.8}/cli2/__init__.py +1 -1
- {cli2-4.2.6 → cli2-4.2.8}/cli2/ansible/action.py +144 -18
- {cli2-4.2.6 → cli2-4.2.8/cli2.egg-info}/PKG-INFO +1 -1
- {cli2-4.2.6 → cli2-4.2.8}/setup.py +1 -1
- {cli2-4.2.6 → cli2-4.2.8}/tests/test_ansible.py +21 -0
- {cli2-4.2.6 → cli2-4.2.8}/MANIFEST.in +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/README.rst +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/classifiers.txt +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/ansible/__init__.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/ansible/playbook.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/ansible/variables.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/asyncio.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/cli.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/cli2.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/client.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/colors.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/configuration.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/decorators.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/display.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/__init__.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/client.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/conf.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/example.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/example_obj.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/nesting.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/obj.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/obj2.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/test.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/lock.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/log.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/node.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/sphinx.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/table.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2/test.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2.egg-info/SOURCES.txt +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2.egg-info/dependency_links.txt +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2.egg-info/entry_points.txt +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2.egg-info/requires.txt +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/cli2.egg-info/top_level.txt +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/setup.cfg +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/tests/test_ansible_variables.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/tests/test_asyncio.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/tests/test_cli.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/tests/test_client.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/tests/test_command.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/tests/test_configuration.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/tests/test_decorators.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/tests/test_display.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/tests/test_entry_point.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/tests/test_group.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/tests/test_inject.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/tests/test_lock.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/tests/test_node.py +0 -0
- {cli2-4.2.6 → cli2-4.2.8}/tests/test_restful.py +0 -0
- {cli2-4.2.6 → 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,15 +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
13
|
from ansible.plugins.action import ActionBase
|
|
14
|
+
from ansible.plugins.action import display
|
|
15
|
+
from ansible.plugins.filter.core import to_nice_yaml
|
|
13
16
|
|
|
14
17
|
# colors:
|
|
15
18
|
# black
|
|
@@ -126,7 +129,39 @@ class ActionBase(ActionBase):
|
|
|
126
129
|
|
|
127
130
|
The client object generated by :py:meth:`client_factory` if you
|
|
128
131
|
implement it.
|
|
132
|
+
|
|
133
|
+
.. py:attribute:: mask
|
|
134
|
+
|
|
135
|
+
List of result keys to mask, if set, this module will print the result
|
|
136
|
+
with masked values, so you can set no_log: True for the task and still
|
|
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()`.
|
|
129
150
|
"""
|
|
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
|
|
164
|
+
|
|
130
165
|
def get(self, arg_name=None, fact_name=None, default=UNSET_DEFAULT):
|
|
131
166
|
if arg_name and arg_name in self._task.args:
|
|
132
167
|
return self._task.args[arg_name]
|
|
@@ -146,10 +181,19 @@ class ActionBase(ActionBase):
|
|
|
146
181
|
asyncio.run(self.run_wrapped_async())
|
|
147
182
|
return self.result
|
|
148
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
|
+
|
|
149
193
|
async def run_wrapped_async(self):
|
|
150
194
|
self.verbosity = self.task_vars.get('ansible_verbosity', 0)
|
|
151
195
|
|
|
152
|
-
if 'LOG_LEVEL' not in os.environ and 'DEBUG'
|
|
196
|
+
if 'LOG_LEVEL' not in os.environ and os.getenv('DEBUG'):
|
|
153
197
|
if self.verbosity == 1:
|
|
154
198
|
os.environ['LOG_LEVEL'] = 'INFO'
|
|
155
199
|
elif self.verbosity >= 2:
|
|
@@ -161,6 +205,7 @@ class ActionBase(ActionBase):
|
|
|
161
205
|
self.client = await self.client_factory()
|
|
162
206
|
except NotImplementedError:
|
|
163
207
|
self.client = None
|
|
208
|
+
self.collect_masked_values()
|
|
164
209
|
await self.run_async()
|
|
165
210
|
except Exception as exc:
|
|
166
211
|
self.result['failed'] = True
|
|
@@ -186,28 +231,73 @@ class ActionBase(ActionBase):
|
|
|
186
231
|
# for pytest to raise
|
|
187
232
|
self.exc = exc
|
|
188
233
|
finally:
|
|
234
|
+
if self.mask and self.verbosity:
|
|
235
|
+
self.print_yaml(self.result)
|
|
236
|
+
|
|
189
237
|
if (
|
|
190
238
|
self._before_data != UNSET_DEFAULT
|
|
191
239
|
and self._after_data != UNSET_DEFAULT
|
|
192
240
|
):
|
|
193
|
-
diff =
|
|
194
|
-
|
|
195
|
-
|
|
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(),
|
|
196
248
|
self._before_label,
|
|
197
249
|
self._after_label,
|
|
198
250
|
)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
251
|
+
cli2.diff(diff)
|
|
252
|
+
|
|
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'):
|
|
283
|
+
return data
|
|
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***'
|
|
209
293
|
else:
|
|
210
|
-
|
|
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
|
|
211
301
|
|
|
212
302
|
async def run_async(self):
|
|
213
303
|
"""
|
|
@@ -247,6 +337,7 @@ class ActionBase(ActionBase):
|
|
|
247
337
|
obj.task_vars = facts or {}
|
|
248
338
|
obj.task_vars.setdefault('ansible_verbosity', 2)
|
|
249
339
|
obj.exc = False
|
|
340
|
+
obj.masked_values = []
|
|
250
341
|
if client:
|
|
251
342
|
async def _factory():
|
|
252
343
|
return client
|
|
@@ -277,7 +368,7 @@ class ActionBase(ActionBase):
|
|
|
277
368
|
:param data: Dictionnary of data
|
|
278
369
|
:param label: Label to show in diff
|
|
279
370
|
"""
|
|
280
|
-
self._before_data =
|
|
371
|
+
self._before_data = data
|
|
281
372
|
self._before_label = label
|
|
282
373
|
|
|
283
374
|
def after_set(self, data, label='after'):
|
|
@@ -287,5 +378,40 @@ class ActionBase(ActionBase):
|
|
|
287
378
|
:param data: Dictionnary of data
|
|
288
379
|
:param label: Label to show in diff
|
|
289
380
|
"""
|
|
290
|
-
self._after_data =
|
|
381
|
+
self._after_data = data
|
|
291
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
|
|
@@ -8,6 +8,27 @@ import yaml
|
|
|
8
8
|
from cli2 import ansible
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
class ActionModule(ansible.ActionBase):
|
|
12
|
+
mask = ['a']
|
|
13
|
+
|
|
14
|
+
async def run_async(self):
|
|
15
|
+
self.result['x'] = dict(a='a', b='b', c='c', d='foo a rrr')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.mark.asyncio
|
|
19
|
+
async def test_mask(monkeypatch):
|
|
20
|
+
printer = mock.Mock()
|
|
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)
|
|
30
|
+
|
|
31
|
+
|
|
11
32
|
@pytest.mark.asyncio
|
|
12
33
|
async def test_response_error(httpx_mock):
|
|
13
34
|
class Client(cli2.Client):
|
|
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
|