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.
Files changed (56) hide show
  1. {cli2-4.2.7/cli2.egg-info → cli2-4.2.8}/PKG-INFO +1 -1
  2. {cli2-4.2.7 → cli2-4.2.8}/cli2/__init__.py +1 -1
  3. {cli2-4.2.7 → cli2-4.2.8}/cli2/ansible/action.py +133 -45
  4. {cli2-4.2.7 → cli2-4.2.8/cli2.egg-info}/PKG-INFO +1 -1
  5. {cli2-4.2.7 → cli2-4.2.8}/setup.py +1 -1
  6. {cli2-4.2.7 → cli2-4.2.8}/tests/test_ansible.py +10 -8
  7. {cli2-4.2.7 → cli2-4.2.8}/MANIFEST.in +0 -0
  8. {cli2-4.2.7 → cli2-4.2.8}/README.rst +0 -0
  9. {cli2-4.2.7 → cli2-4.2.8}/classifiers.txt +0 -0
  10. {cli2-4.2.7 → cli2-4.2.8}/cli2/ansible/__init__.py +0 -0
  11. {cli2-4.2.7 → cli2-4.2.8}/cli2/ansible/playbook.py +0 -0
  12. {cli2-4.2.7 → cli2-4.2.8}/cli2/ansible/variables.py +0 -0
  13. {cli2-4.2.7 → cli2-4.2.8}/cli2/asyncio.py +0 -0
  14. {cli2-4.2.7 → cli2-4.2.8}/cli2/cli.py +0 -0
  15. {cli2-4.2.7 → cli2-4.2.8}/cli2/cli2.py +0 -0
  16. {cli2-4.2.7 → cli2-4.2.8}/cli2/client.py +0 -0
  17. {cli2-4.2.7 → cli2-4.2.8}/cli2/colors.py +0 -0
  18. {cli2-4.2.7 → cli2-4.2.8}/cli2/configuration.py +0 -0
  19. {cli2-4.2.7 → cli2-4.2.8}/cli2/decorators.py +0 -0
  20. {cli2-4.2.7 → cli2-4.2.8}/cli2/display.py +0 -0
  21. {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/__init__.py +0 -0
  22. {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/client.py +0 -0
  23. {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/conf.py +0 -0
  24. {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/example.py +0 -0
  25. {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/example_obj.py +0 -0
  26. {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/nesting.py +0 -0
  27. {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/obj.py +0 -0
  28. {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/obj2.py +0 -0
  29. {cli2-4.2.7 → cli2-4.2.8}/cli2/examples/test.py +0 -0
  30. {cli2-4.2.7 → cli2-4.2.8}/cli2/lock.py +0 -0
  31. {cli2-4.2.7 → cli2-4.2.8}/cli2/log.py +0 -0
  32. {cli2-4.2.7 → cli2-4.2.8}/cli2/node.py +0 -0
  33. {cli2-4.2.7 → cli2-4.2.8}/cli2/sphinx.py +0 -0
  34. {cli2-4.2.7 → cli2-4.2.8}/cli2/table.py +0 -0
  35. {cli2-4.2.7 → cli2-4.2.8}/cli2/test.py +0 -0
  36. {cli2-4.2.7 → cli2-4.2.8}/cli2.egg-info/SOURCES.txt +0 -0
  37. {cli2-4.2.7 → cli2-4.2.8}/cli2.egg-info/dependency_links.txt +0 -0
  38. {cli2-4.2.7 → cli2-4.2.8}/cli2.egg-info/entry_points.txt +0 -0
  39. {cli2-4.2.7 → cli2-4.2.8}/cli2.egg-info/requires.txt +0 -0
  40. {cli2-4.2.7 → cli2-4.2.8}/cli2.egg-info/top_level.txt +0 -0
  41. {cli2-4.2.7 → cli2-4.2.8}/setup.cfg +0 -0
  42. {cli2-4.2.7 → cli2-4.2.8}/tests/test_ansible_variables.py +0 -0
  43. {cli2-4.2.7 → cli2-4.2.8}/tests/test_asyncio.py +0 -0
  44. {cli2-4.2.7 → cli2-4.2.8}/tests/test_cli.py +0 -0
  45. {cli2-4.2.7 → cli2-4.2.8}/tests/test_client.py +0 -0
  46. {cli2-4.2.7 → cli2-4.2.8}/tests/test_command.py +0 -0
  47. {cli2-4.2.7 → cli2-4.2.8}/tests/test_configuration.py +0 -0
  48. {cli2-4.2.7 → cli2-4.2.8}/tests/test_decorators.py +0 -0
  49. {cli2-4.2.7 → cli2-4.2.8}/tests/test_display.py +0 -0
  50. {cli2-4.2.7 → cli2-4.2.8}/tests/test_entry_point.py +0 -0
  51. {cli2-4.2.7 → cli2-4.2.8}/tests/test_group.py +0 -0
  52. {cli2-4.2.7 → cli2-4.2.8}/tests/test_inject.py +0 -0
  53. {cli2-4.2.7 → cli2-4.2.8}/tests/test_lock.py +0 -0
  54. {cli2-4.2.7 → cli2-4.2.8}/tests/test_node.py +0 -0
  55. {cli2-4.2.7 → cli2-4.2.8}/tests/test_restful.py +0 -0
  56. {cli2-4.2.7 → cli2-4.2.8}/tests/test_table.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cli2
3
- Version: 4.2.7
3
+ Version: 4.2.8
4
4
  Summary: image:: https://yourlabs.io/oss/cli2/badges/master/pipeline.svg
5
5
  Home-page: https://yourlabs.io/oss/cli2
6
6
  Author: James Pic
@@ -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
- Experimental: my base class for Ansible actions.
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' not in os.environ:
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
- print('\nResult:')
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 = cli2.diff_data(
207
- self._before_data,
208
- self._after_data,
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
- if self.client and self.client.mask:
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 native(self, data):
226
- def _native(data):
227
- for key, value in data.items():
228
- if isinstance(value, AnsibleUnicode):
229
- data[key] = str(value)
230
- elif isinstance(value, dict):
231
- data[key] = _native(value)
232
- elif isinstance(value, list):
233
- data[key] = [_native(item) for item in value]
234
- return data
235
- return _native(copy.deepcopy(data))
236
-
237
- def masked_result(self):
238
- def _mask(data):
239
- if isinstance(data, dict):
240
- for key, value in data.items():
241
- if key in self.mask:
242
- data[key] = '***MASKED***'
243
- else:
244
- data[key] = _mask(value)
245
- elif isinstance(data, list):
246
- return [_mask(item) for item in data]
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
- return _mask(self.native(self.result))
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 = self.native(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 = self.native(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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cli2
3
- Version: 4.2.7
3
+ Version: 4.2.8
4
4
  Summary: image:: https://yourlabs.io/oss/cli2/badges/master/pipeline.svg
5
5
  Home-page: https://yourlabs.io/oss/cli2
6
6
  Author: James Pic
@@ -44,7 +44,7 @@ from setuptools import setup
44
44
 
45
45
  setup(
46
46
  name='cli2',
47
- version='4.2.7',
47
+ version='4.2.8',
48
48
  setup_requires='setupmeta',
49
49
  packages=['cli2'],
50
50
  install_requires=[
@@ -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
- from cli2.ansible import action
22
- monkeypatch.setattr(action.cli2, 'print', printer)
23
- module = await ActionModule.run_test_async()
24
- assert module.result == dict(x=dict(a='a', b='b'))
25
- printer.assert_called_once_with(
26
- dict(x=dict(a='***MASKED***', b='b'))
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