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.
Files changed (56) hide show
  1. {cli2-4.2.6/cli2.egg-info → cli2-4.2.8}/PKG-INFO +1 -1
  2. {cli2-4.2.6 → cli2-4.2.8}/cli2/__init__.py +1 -1
  3. {cli2-4.2.6 → cli2-4.2.8}/cli2/ansible/action.py +144 -18
  4. {cli2-4.2.6 → cli2-4.2.8/cli2.egg-info}/PKG-INFO +1 -1
  5. {cli2-4.2.6 → cli2-4.2.8}/setup.py +1 -1
  6. {cli2-4.2.6 → cli2-4.2.8}/tests/test_ansible.py +21 -0
  7. {cli2-4.2.6 → cli2-4.2.8}/MANIFEST.in +0 -0
  8. {cli2-4.2.6 → cli2-4.2.8}/README.rst +0 -0
  9. {cli2-4.2.6 → cli2-4.2.8}/classifiers.txt +0 -0
  10. {cli2-4.2.6 → cli2-4.2.8}/cli2/ansible/__init__.py +0 -0
  11. {cli2-4.2.6 → cli2-4.2.8}/cli2/ansible/playbook.py +0 -0
  12. {cli2-4.2.6 → cli2-4.2.8}/cli2/ansible/variables.py +0 -0
  13. {cli2-4.2.6 → cli2-4.2.8}/cli2/asyncio.py +0 -0
  14. {cli2-4.2.6 → cli2-4.2.8}/cli2/cli.py +0 -0
  15. {cli2-4.2.6 → cli2-4.2.8}/cli2/cli2.py +0 -0
  16. {cli2-4.2.6 → cli2-4.2.8}/cli2/client.py +0 -0
  17. {cli2-4.2.6 → cli2-4.2.8}/cli2/colors.py +0 -0
  18. {cli2-4.2.6 → cli2-4.2.8}/cli2/configuration.py +0 -0
  19. {cli2-4.2.6 → cli2-4.2.8}/cli2/decorators.py +0 -0
  20. {cli2-4.2.6 → cli2-4.2.8}/cli2/display.py +0 -0
  21. {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/__init__.py +0 -0
  22. {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/client.py +0 -0
  23. {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/conf.py +0 -0
  24. {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/example.py +0 -0
  25. {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/example_obj.py +0 -0
  26. {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/nesting.py +0 -0
  27. {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/obj.py +0 -0
  28. {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/obj2.py +0 -0
  29. {cli2-4.2.6 → cli2-4.2.8}/cli2/examples/test.py +0 -0
  30. {cli2-4.2.6 → cli2-4.2.8}/cli2/lock.py +0 -0
  31. {cli2-4.2.6 → cli2-4.2.8}/cli2/log.py +0 -0
  32. {cli2-4.2.6 → cli2-4.2.8}/cli2/node.py +0 -0
  33. {cli2-4.2.6 → cli2-4.2.8}/cli2/sphinx.py +0 -0
  34. {cli2-4.2.6 → cli2-4.2.8}/cli2/table.py +0 -0
  35. {cli2-4.2.6 → cli2-4.2.8}/cli2/test.py +0 -0
  36. {cli2-4.2.6 → cli2-4.2.8}/cli2.egg-info/SOURCES.txt +0 -0
  37. {cli2-4.2.6 → cli2-4.2.8}/cli2.egg-info/dependency_links.txt +0 -0
  38. {cli2-4.2.6 → cli2-4.2.8}/cli2.egg-info/entry_points.txt +0 -0
  39. {cli2-4.2.6 → cli2-4.2.8}/cli2.egg-info/requires.txt +0 -0
  40. {cli2-4.2.6 → cli2-4.2.8}/cli2.egg-info/top_level.txt +0 -0
  41. {cli2-4.2.6 → cli2-4.2.8}/setup.cfg +0 -0
  42. {cli2-4.2.6 → cli2-4.2.8}/tests/test_ansible_variables.py +0 -0
  43. {cli2-4.2.6 → cli2-4.2.8}/tests/test_asyncio.py +0 -0
  44. {cli2-4.2.6 → cli2-4.2.8}/tests/test_cli.py +0 -0
  45. {cli2-4.2.6 → cli2-4.2.8}/tests/test_client.py +0 -0
  46. {cli2-4.2.6 → cli2-4.2.8}/tests/test_command.py +0 -0
  47. {cli2-4.2.6 → cli2-4.2.8}/tests/test_configuration.py +0 -0
  48. {cli2-4.2.6 → cli2-4.2.8}/tests/test_decorators.py +0 -0
  49. {cli2-4.2.6 → cli2-4.2.8}/tests/test_display.py +0 -0
  50. {cli2-4.2.6 → cli2-4.2.8}/tests/test_entry_point.py +0 -0
  51. {cli2-4.2.6 → cli2-4.2.8}/tests/test_group.py +0 -0
  52. {cli2-4.2.6 → cli2-4.2.8}/tests/test_inject.py +0 -0
  53. {cli2-4.2.6 → cli2-4.2.8}/tests/test_lock.py +0 -0
  54. {cli2-4.2.6 → cli2-4.2.8}/tests/test_node.py +0 -0
  55. {cli2-4.2.6 → cli2-4.2.8}/tests/test_restful.py +0 -0
  56. {cli2-4.2.6 → 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.6
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,15 +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
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' not in os.environ:
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 = cli2.diff_data(
194
- self._before_data,
195
- 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(),
196
248
  self._before_label,
197
249
  self._after_label,
198
250
  )
199
- if self.client and self.client.mask:
200
- output = '\n'.join([
201
- line.rstrip() for line in diff if line.strip()
202
- ])
203
- output = re.sub(
204
- f'({"|".join(self.client.mask)}): (.*)',
205
- '\\1: ***MASKED***',
206
- ''.join(output),
207
- )
208
- print(cli2.highlight(output, 'Diff'))
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
- cli2.diff(diff)
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 = copy.deepcopy(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 = copy.deepcopy(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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cli2
3
- Version: 4.2.6
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.6',
47
+ version='4.2.8',
48
48
  setup_requires='setupmeta',
49
49
  packages=['cli2'],
50
50
  install_requires=[
@@ -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