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.
Files changed (56) hide show
  1. {cli2-4.2.7/cli2.egg-info → cli2-4.2.9}/PKG-INFO +1 -1
  2. {cli2-4.2.7 → cli2-4.2.9}/cli2/__init__.py +1 -1
  3. {cli2-4.2.7 → cli2-4.2.9}/cli2/ansible/action.py +148 -47
  4. {cli2-4.2.7 → cli2-4.2.9/cli2.egg-info}/PKG-INFO +1 -1
  5. {cli2-4.2.7 → cli2-4.2.9}/setup.py +1 -1
  6. {cli2-4.2.7 → cli2-4.2.9}/tests/test_ansible.py +10 -8
  7. {cli2-4.2.7 → cli2-4.2.9}/MANIFEST.in +0 -0
  8. {cli2-4.2.7 → cli2-4.2.9}/README.rst +0 -0
  9. {cli2-4.2.7 → cli2-4.2.9}/classifiers.txt +0 -0
  10. {cli2-4.2.7 → cli2-4.2.9}/cli2/ansible/__init__.py +0 -0
  11. {cli2-4.2.7 → cli2-4.2.9}/cli2/ansible/playbook.py +0 -0
  12. {cli2-4.2.7 → cli2-4.2.9}/cli2/ansible/variables.py +0 -0
  13. {cli2-4.2.7 → cli2-4.2.9}/cli2/asyncio.py +0 -0
  14. {cli2-4.2.7 → cli2-4.2.9}/cli2/cli.py +0 -0
  15. {cli2-4.2.7 → cli2-4.2.9}/cli2/cli2.py +0 -0
  16. {cli2-4.2.7 → cli2-4.2.9}/cli2/client.py +0 -0
  17. {cli2-4.2.7 → cli2-4.2.9}/cli2/colors.py +0 -0
  18. {cli2-4.2.7 → cli2-4.2.9}/cli2/configuration.py +0 -0
  19. {cli2-4.2.7 → cli2-4.2.9}/cli2/decorators.py +0 -0
  20. {cli2-4.2.7 → cli2-4.2.9}/cli2/display.py +0 -0
  21. {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/__init__.py +0 -0
  22. {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/client.py +0 -0
  23. {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/conf.py +0 -0
  24. {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/example.py +0 -0
  25. {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/example_obj.py +0 -0
  26. {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/nesting.py +0 -0
  27. {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/obj.py +0 -0
  28. {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/obj2.py +0 -0
  29. {cli2-4.2.7 → cli2-4.2.9}/cli2/examples/test.py +0 -0
  30. {cli2-4.2.7 → cli2-4.2.9}/cli2/lock.py +0 -0
  31. {cli2-4.2.7 → cli2-4.2.9}/cli2/log.py +0 -0
  32. {cli2-4.2.7 → cli2-4.2.9}/cli2/node.py +0 -0
  33. {cli2-4.2.7 → cli2-4.2.9}/cli2/sphinx.py +0 -0
  34. {cli2-4.2.7 → cli2-4.2.9}/cli2/table.py +0 -0
  35. {cli2-4.2.7 → cli2-4.2.9}/cli2/test.py +0 -0
  36. {cli2-4.2.7 → cli2-4.2.9}/cli2.egg-info/SOURCES.txt +0 -0
  37. {cli2-4.2.7 → cli2-4.2.9}/cli2.egg-info/dependency_links.txt +0 -0
  38. {cli2-4.2.7 → cli2-4.2.9}/cli2.egg-info/entry_points.txt +0 -0
  39. {cli2-4.2.7 → cli2-4.2.9}/cli2.egg-info/requires.txt +0 -0
  40. {cli2-4.2.7 → cli2-4.2.9}/cli2.egg-info/top_level.txt +0 -0
  41. {cli2-4.2.7 → cli2-4.2.9}/setup.cfg +0 -0
  42. {cli2-4.2.7 → cli2-4.2.9}/tests/test_ansible_variables.py +0 -0
  43. {cli2-4.2.7 → cli2-4.2.9}/tests/test_asyncio.py +0 -0
  44. {cli2-4.2.7 → cli2-4.2.9}/tests/test_cli.py +0 -0
  45. {cli2-4.2.7 → cli2-4.2.9}/tests/test_client.py +0 -0
  46. {cli2-4.2.7 → cli2-4.2.9}/tests/test_command.py +0 -0
  47. {cli2-4.2.7 → cli2-4.2.9}/tests/test_configuration.py +0 -0
  48. {cli2-4.2.7 → cli2-4.2.9}/tests/test_decorators.py +0 -0
  49. {cli2-4.2.7 → cli2-4.2.9}/tests/test_display.py +0 -0
  50. {cli2-4.2.7 → cli2-4.2.9}/tests/test_entry_point.py +0 -0
  51. {cli2-4.2.7 → cli2-4.2.9}/tests/test_group.py +0 -0
  52. {cli2-4.2.7 → cli2-4.2.9}/tests/test_inject.py +0 -0
  53. {cli2-4.2.7 → cli2-4.2.9}/tests/test_lock.py +0 -0
  54. {cli2-4.2.7 → cli2-4.2.9}/tests/test_node.py +0 -0
  55. {cli2-4.2.7 → cli2-4.2.9}/tests/test_restful.py +0 -0
  56. {cli2-4.2.7 → cli2-4.2.9}/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.9
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,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' not in os.environ:
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
- print('\nResult:')
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 = cli2.diff_data(
207
- self._before_data,
208
- self._after_data,
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
- 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)
255
+ cli2.diff(diff)
224
256
 
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]
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
- return _mask(self.native(self.result))
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
- obj = cls(*[mock.Mock()] * 6)
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 = self.native(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 = self.native(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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cli2
3
- Version: 4.2.7
3
+ Version: 4.2.9
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.9',
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