cli2 4.3.3__tar.gz → 5.0.0__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.3.3/cli2.egg-info → cli2-5.0.0}/PKG-INFO +5 -4
- {cli2-4.3.3 → cli2-5.0.0}/cli2/__init__.py +14 -26
- cli2-5.0.0/cli2/asyncio.py +130 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/display.py +1 -6
- {cli2-4.3.3 → cli2-5.0.0}/cli2/mask.py +3 -1
- {cli2-4.3.3 → cli2-5.0.0/cli2.egg-info}/PKG-INFO +5 -4
- {cli2-4.3.3 → cli2-5.0.0}/cli2.egg-info/SOURCES.txt +0 -6
- {cli2-4.3.3 → cli2-5.0.0}/cli2.egg-info/requires.txt +5 -3
- {cli2-4.3.3 → cli2-5.0.0}/setup.py +3 -5
- {cli2-4.3.3 → cli2-5.0.0}/tests/test_ansible.py +75 -20
- {cli2-4.3.3 → cli2-5.0.0}/tests/test_ansible_variables.py +19 -12
- cli2-5.0.0/tests/test_asyncio.py +68 -0
- {cli2-4.3.3 → cli2-5.0.0}/tests/test_cli.py +1 -1
- {cli2-4.3.3 → cli2-5.0.0}/tests/test_client.py +110 -67
- {cli2-4.3.3 → cli2-5.0.0}/tests/test_mask.py +5 -0
- cli2-4.3.3/cli2/ansible/__init__.py +0 -16
- cli2-4.3.3/cli2/ansible/action.py +0 -400
- cli2-4.3.3/cli2/ansible/playbook.py +0 -215
- cli2-4.3.3/cli2/ansible/variables.py +0 -115
- cli2-4.3.3/cli2/asyncio.py +0 -42
- cli2-4.3.3/cli2/client.py +0 -1868
- cli2-4.3.3/cli2/examples/client.py +0 -61
- cli2-4.3.3/tests/test_asyncio.py +0 -18
- {cli2-4.3.3 → cli2-5.0.0}/MANIFEST.in +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/README.rst +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/classifiers.txt +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/cli.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/cli2.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/colors.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/configuration.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/decorators.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/examples/__init__.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/examples/conf.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/examples/example.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/examples/example_obj.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/examples/nesting.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/examples/obj.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/examples/obj2.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/examples/test.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/lock.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/log.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/node.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/sphinx.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/table.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2/test.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2.egg-info/dependency_links.txt +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2.egg-info/entry_points.txt +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/cli2.egg-info/top_level.txt +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/setup.cfg +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/tests/test_command.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/tests/test_configuration.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/tests/test_decorators.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/tests/test_display.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/tests/test_entry_point.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/tests/test_group.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/tests/test_inject.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/tests/test_lock.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/tests/test_node.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/tests/test_restful.py +0 -0
- {cli2-4.3.3 → cli2-5.0.0}/tests/test_table.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: cli2
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.0
|
|
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
|
|
@@ -13,9 +13,10 @@ Requires-Dist: docstring_parser
|
|
|
13
13
|
Requires-Dist: pyyaml
|
|
14
14
|
Requires-Dist: pygments
|
|
15
15
|
Requires-Dist: structlog
|
|
16
|
-
Provides-Extra:
|
|
17
|
-
Requires-Dist:
|
|
18
|
-
|
|
16
|
+
Provides-Extra: httpx
|
|
17
|
+
Requires-Dist: chttpx; extra == "httpx"
|
|
18
|
+
Provides-Extra: ansible
|
|
19
|
+
Requires-Dist: cansible; extra == "ansible"
|
|
19
20
|
Provides-Extra: test
|
|
20
21
|
Requires-Dist: freezegun; extra == "test"
|
|
21
22
|
Requires-Dist: pytest; extra == "test"
|
|
@@ -9,35 +9,10 @@ from .cli import (
|
|
|
9
9
|
Group,
|
|
10
10
|
EntryPoint,
|
|
11
11
|
)
|
|
12
|
-
from .asyncio import async_resolve, async_run
|
|
12
|
+
from .asyncio import async_resolve, async_run, Queue
|
|
13
13
|
from .colors import colors as c
|
|
14
14
|
|
|
15
15
|
from .configuration import Configuration, cfg
|
|
16
|
-
try:
|
|
17
|
-
from .client import (
|
|
18
|
-
ClientError,
|
|
19
|
-
ResponseError,
|
|
20
|
-
TokenGetError,
|
|
21
|
-
RefusedResponseError,
|
|
22
|
-
RetriesExceededError,
|
|
23
|
-
FieldError,
|
|
24
|
-
FieldValueError,
|
|
25
|
-
FieldExternalizeError,
|
|
26
|
-
Client,
|
|
27
|
-
ClientCommand,
|
|
28
|
-
DateTimeField,
|
|
29
|
-
Field,
|
|
30
|
-
Handler,
|
|
31
|
-
JSONStringField,
|
|
32
|
-
Model,
|
|
33
|
-
ModelCommand,
|
|
34
|
-
Paginator,
|
|
35
|
-
Related,
|
|
36
|
-
)
|
|
37
|
-
except ImportError:
|
|
38
|
-
raise
|
|
39
|
-
# httpx not installed
|
|
40
|
-
pass
|
|
41
16
|
from .display import diff, diff_data, render, print, highlight, yaml_highlight
|
|
42
17
|
from .lock import Lock
|
|
43
18
|
from .log import configure, log
|
|
@@ -55,3 +30,16 @@ def which(cmd):
|
|
|
55
30
|
path = Path(os.getenv('HOME')) / '.local/bin' / cmd
|
|
56
31
|
if path.exists():
|
|
57
32
|
return str(path)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def mutable(obj):
|
|
36
|
+
types = (
|
|
37
|
+
int,
|
|
38
|
+
float,
|
|
39
|
+
str,
|
|
40
|
+
tuple,
|
|
41
|
+
frozenset,
|
|
42
|
+
bool,
|
|
43
|
+
bytes,
|
|
44
|
+
)
|
|
45
|
+
return not isinstance(obj, types)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
from . import display
|
|
4
|
+
from .log import log
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def async_iter(obj):
|
|
8
|
+
""" Check if an object is an async iterable. """
|
|
9
|
+
return inspect.isasyncgen(obj) or hasattr(obj, '__aiter__')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def async_resolve(result, output=False):
|
|
13
|
+
"""
|
|
14
|
+
Recursively resolve awaitables and async iterables.
|
|
15
|
+
|
|
16
|
+
:param result: The awaitable or async iterable to resolve
|
|
17
|
+
:param output: If True, print results as they are resolved. If False,
|
|
18
|
+
collect results.
|
|
19
|
+
|
|
20
|
+
:return: The resolved value(s). If output is True, returns None. If output
|
|
21
|
+
is False, returns a list of resolved values from async iterables.
|
|
22
|
+
"""
|
|
23
|
+
while inspect.iscoroutine(result):
|
|
24
|
+
result = await result
|
|
25
|
+
if async_iter(result):
|
|
26
|
+
results = []
|
|
27
|
+
async for _ in result:
|
|
28
|
+
if output:
|
|
29
|
+
if (
|
|
30
|
+
not inspect.iscoroutine(_)
|
|
31
|
+
and not inspect.isasyncgen(_)
|
|
32
|
+
):
|
|
33
|
+
display.print(_)
|
|
34
|
+
else:
|
|
35
|
+
await async_resolve(_, output=output)
|
|
36
|
+
else:
|
|
37
|
+
results.append(await async_resolve(_))
|
|
38
|
+
return None if output else results
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def async_run(coroutine):
|
|
43
|
+
"""Run an async coroutine in the current event loop or create a new one.
|
|
44
|
+
|
|
45
|
+
If an event loop is already running, creates a task in that loop.
|
|
46
|
+
If no event loop is running, creates a new one and runs the coroutine.
|
|
47
|
+
|
|
48
|
+
:param coroutine: The coroutine to run (return value of an async function)
|
|
49
|
+
:return: The result of the coroutine execution
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
loop = asyncio.get_running_loop()
|
|
53
|
+
except RuntimeError:
|
|
54
|
+
return asyncio.run(coroutine)
|
|
55
|
+
else:
|
|
56
|
+
return loop.create_task(coroutine)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Queue(asyncio.Queue):
|
|
60
|
+
"""
|
|
61
|
+
An async queue with worker pool for concurrent task processing.
|
|
62
|
+
|
|
63
|
+
Extends asyncio.Queue to manage a pool of worker tasks that process items
|
|
64
|
+
from the queue concurrently.
|
|
65
|
+
|
|
66
|
+
.. code-block:: python
|
|
67
|
+
|
|
68
|
+
# will run 2 at the time
|
|
69
|
+
queue = cli2.Queue(num_workers=2)
|
|
70
|
+
# call like asyncio.run
|
|
71
|
+
await queue.run(foo(), bar(), other())
|
|
72
|
+
|
|
73
|
+
.. py:attribute:: num_workers
|
|
74
|
+
|
|
75
|
+
Number of concurrent workers (default: 12)
|
|
76
|
+
|
|
77
|
+
.. py:attribute:: results
|
|
78
|
+
|
|
79
|
+
List of results from completed tasks, order of results not garanteed
|
|
80
|
+
due to concurrency.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, *args, num_workers=12, **kwargs):
|
|
84
|
+
"""Initialize the queue with worker pool.
|
|
85
|
+
|
|
86
|
+
:param num_workers: Number of concurrent workers (default: 12)
|
|
87
|
+
:paarm *args: Positional arguments for asyncio.Queue
|
|
88
|
+
:param **kwargs: Keyword arguments for asyncio.Queue
|
|
89
|
+
"""
|
|
90
|
+
self.num_workers = num_workers or 12
|
|
91
|
+
self.results = []
|
|
92
|
+
super().__init__(*args, **kwargs)
|
|
93
|
+
|
|
94
|
+
async def run(self, *tasks):
|
|
95
|
+
"""
|
|
96
|
+
Run tasks through the worker pool.
|
|
97
|
+
|
|
98
|
+
:param tasks: Coroutines
|
|
99
|
+
"""
|
|
100
|
+
self.results = []
|
|
101
|
+
|
|
102
|
+
for task in tasks:
|
|
103
|
+
await self.put(task)
|
|
104
|
+
|
|
105
|
+
workers = [
|
|
106
|
+
asyncio.create_task(self.worker())
|
|
107
|
+
for i in range(self.num_workers)
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
await self.join()
|
|
111
|
+
for worker in workers:
|
|
112
|
+
worker.cancel()
|
|
113
|
+
|
|
114
|
+
async def worker(self):
|
|
115
|
+
"""Worker task that processes items from the queue.
|
|
116
|
+
|
|
117
|
+
Continuously gets tasks from the queue, executes them, and stores
|
|
118
|
+
results.
|
|
119
|
+
Handles exceptions by logging them.
|
|
120
|
+
"""
|
|
121
|
+
while True:
|
|
122
|
+
task = await self.get()
|
|
123
|
+
try:
|
|
124
|
+
result = await task
|
|
125
|
+
except: # noqa
|
|
126
|
+
log.exception()
|
|
127
|
+
else:
|
|
128
|
+
self.results.append(result)
|
|
129
|
+
finally:
|
|
130
|
+
self.task_done()
|
|
@@ -7,16 +7,11 @@ Generic pretty display utils.
|
|
|
7
7
|
force it with :envvar:`FORCE_COLOR`, ie. gitlab-ci etc
|
|
8
8
|
"""
|
|
9
9
|
import difflib
|
|
10
|
+
import json
|
|
10
11
|
import os
|
|
11
12
|
import sys
|
|
12
13
|
import yaml
|
|
13
14
|
|
|
14
|
-
try:
|
|
15
|
-
import jsonlight as json
|
|
16
|
-
except ImportError:
|
|
17
|
-
import json
|
|
18
|
-
|
|
19
|
-
|
|
20
15
|
_print = print
|
|
21
16
|
|
|
22
17
|
|
|
@@ -109,8 +109,10 @@ class Mask:
|
|
|
109
109
|
data[key] = self._mask(value)
|
|
110
110
|
elif isinstance(data, list):
|
|
111
111
|
return [self._mask(item) for item in data]
|
|
112
|
+
elif isinstance(data, set):
|
|
113
|
+
return {self._mask(item) for item in data}
|
|
112
114
|
elif isinstance(data, str):
|
|
113
|
-
for value in self.values:
|
|
115
|
+
for value in sorted(self.values, key=len, reverse=True):
|
|
114
116
|
data = data.replace(str(value), '***MASKED***')
|
|
115
117
|
return data
|
|
116
118
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: cli2
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.0
|
|
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
|
|
@@ -13,9 +13,10 @@ Requires-Dist: docstring_parser
|
|
|
13
13
|
Requires-Dist: pyyaml
|
|
14
14
|
Requires-Dist: pygments
|
|
15
15
|
Requires-Dist: structlog
|
|
16
|
-
Provides-Extra:
|
|
17
|
-
Requires-Dist:
|
|
18
|
-
|
|
16
|
+
Provides-Extra: httpx
|
|
17
|
+
Requires-Dist: chttpx; extra == "httpx"
|
|
18
|
+
Provides-Extra: ansible
|
|
19
|
+
Requires-Dist: cansible; extra == "ansible"
|
|
19
20
|
Provides-Extra: test
|
|
20
21
|
Requires-Dist: freezegun; extra == "test"
|
|
21
22
|
Requires-Dist: pytest; extra == "test"
|
|
@@ -6,7 +6,6 @@ cli2/__init__.py
|
|
|
6
6
|
cli2/asyncio.py
|
|
7
7
|
cli2/cli.py
|
|
8
8
|
cli2/cli2.py
|
|
9
|
-
cli2/client.py
|
|
10
9
|
cli2/colors.py
|
|
11
10
|
cli2/configuration.py
|
|
12
11
|
cli2/decorators.py
|
|
@@ -24,12 +23,7 @@ cli2.egg-info/dependency_links.txt
|
|
|
24
23
|
cli2.egg-info/entry_points.txt
|
|
25
24
|
cli2.egg-info/requires.txt
|
|
26
25
|
cli2.egg-info/top_level.txt
|
|
27
|
-
cli2/ansible/__init__.py
|
|
28
|
-
cli2/ansible/action.py
|
|
29
|
-
cli2/ansible/playbook.py
|
|
30
|
-
cli2/ansible/variables.py
|
|
31
26
|
cli2/examples/__init__.py
|
|
32
|
-
cli2/examples/client.py
|
|
33
27
|
cli2/examples/conf.py
|
|
34
28
|
cli2/examples/example.py
|
|
35
29
|
cli2/examples/example_obj.py
|
|
@@ -44,7 +44,7 @@ from setuptools import setup
|
|
|
44
44
|
|
|
45
45
|
setup(
|
|
46
46
|
name='cli2',
|
|
47
|
-
version='
|
|
47
|
+
version='5.0.0',
|
|
48
48
|
setup_requires='setupmeta',
|
|
49
49
|
packages=['cli2'],
|
|
50
50
|
install_requires=[
|
|
@@ -54,10 +54,8 @@ setup(
|
|
|
54
54
|
'structlog',
|
|
55
55
|
],
|
|
56
56
|
extras_require=dict(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
'truststore',
|
|
60
|
-
],
|
|
57
|
+
httpx=['chttpx'],
|
|
58
|
+
ansible=['cansible'],
|
|
61
59
|
test=[
|
|
62
60
|
'freezegun',
|
|
63
61
|
'pytest',
|
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
import cli2
|
|
2
|
+
import chttpx
|
|
2
3
|
import httpx
|
|
3
4
|
import mock
|
|
4
5
|
import pytest
|
|
5
6
|
import textwrap
|
|
6
7
|
import yaml
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class ActionModule(ansible.ActionBase):
|
|
12
|
-
mask_keys = ['a']
|
|
13
|
-
|
|
14
|
-
async def run_async(self):
|
|
15
|
-
self.result['x'] = dict(a='a', b='b', c='c', d='foo a rrr')
|
|
9
|
+
import cansible
|
|
16
10
|
|
|
17
11
|
|
|
18
12
|
@pytest.mark.asyncio
|
|
19
13
|
async def test_mask(monkeypatch):
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
class ActionModule(cansible.ActionBase):
|
|
15
|
+
masked_keys = ['a']
|
|
16
|
+
print = mock.Mock()
|
|
17
|
+
|
|
18
|
+
async def run_async(self):
|
|
19
|
+
self.result['x'] = dict(a='a', b='b', c='c', d='foo a rrr')
|
|
20
|
+
|
|
22
21
|
module = await ActionModule.run_test_async(facts=dict(mask_keys=['b']))
|
|
23
22
|
# result is untouched
|
|
24
23
|
assert module.result == {'x':
|
|
@@ -26,15 +25,27 @@ async def test_mask(monkeypatch):
|
|
|
26
25
|
}
|
|
27
26
|
# output has proper masking
|
|
28
27
|
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
|
-
|
|
28
|
+
module.print.assert_called_once_with(expected, mask=False)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_subprocess_remote(playbook):
|
|
32
|
+
playbook.task_add(
|
|
33
|
+
'yourlabs.test.password_get',
|
|
34
|
+
no_log=True,
|
|
35
|
+
)
|
|
36
|
+
result = playbook()
|
|
37
|
+
expected = "foo:***MASKED***:bar\n\nansible_facts:\n\n mask_values:\n\n - \'***MASKED***\'\n\nsecret: \'***MASKED***\'\n\nstdout: foo:***MASKED***:bar" # noqa
|
|
38
|
+
assert expected in result['stdout']
|
|
39
|
+
|
|
40
|
+
|
|
30
41
|
|
|
31
42
|
|
|
32
43
|
@pytest.mark.asyncio
|
|
33
44
|
async def test_response_error(httpx_mock):
|
|
34
|
-
class Client(
|
|
45
|
+
class Client(chttpx.Client):
|
|
35
46
|
mask_keys = ['secret']
|
|
36
47
|
|
|
37
|
-
class Action(
|
|
48
|
+
class Action(cansible.ActionBase):
|
|
38
49
|
async def client_factory(self):
|
|
39
50
|
return Client(base_url='http://foo')
|
|
40
51
|
|
|
@@ -59,9 +70,9 @@ async def test_response_error(httpx_mock):
|
|
|
59
70
|
|
|
60
71
|
@pytest.mark.asyncio
|
|
61
72
|
async def test_option():
|
|
62
|
-
class Action(
|
|
63
|
-
fact =
|
|
64
|
-
arg =
|
|
73
|
+
class Action(cansible.ActionBase):
|
|
74
|
+
fact = cansible.Option(fact='fact', default='default fact')
|
|
75
|
+
arg = cansible.Option(arg='arg', fact='arg_fact')
|
|
65
76
|
|
|
66
77
|
async def run_async(self):
|
|
67
78
|
self.result['arg'] = self.arg
|
|
@@ -90,8 +101,8 @@ async def test_option():
|
|
|
90
101
|
error="Missing arg `arg` or fact `arg_fact`",
|
|
91
102
|
)
|
|
92
103
|
|
|
93
|
-
class Action(
|
|
94
|
-
name =
|
|
104
|
+
class Action(cansible.ActionBase):
|
|
105
|
+
name = cansible.Option('name')
|
|
95
106
|
|
|
96
107
|
async def run_async(self):
|
|
97
108
|
self.result['name'] = self.name
|
|
@@ -108,8 +119,8 @@ async def test_diff(monkeypatch):
|
|
|
108
119
|
_print = mock.Mock()
|
|
109
120
|
monkeypatch.setattr(cli2.display, '_print', _print)
|
|
110
121
|
|
|
111
|
-
class Action(
|
|
112
|
-
name =
|
|
122
|
+
class Action(cansible.ActionBase):
|
|
123
|
+
name = cansible.Option('name', 'object_name', 'Test object')
|
|
113
124
|
|
|
114
125
|
async def run_async(self):
|
|
115
126
|
self.before_set(dict(foo=1))
|
|
@@ -119,6 +130,41 @@ async def test_diff(monkeypatch):
|
|
|
119
130
|
_print.assert_called_once_with(expected)
|
|
120
131
|
|
|
121
132
|
|
|
133
|
+
@pytest.mark.asyncio
|
|
134
|
+
async def test_fact_set():
|
|
135
|
+
class Action(cansible.ActionBase):
|
|
136
|
+
foo = cansible.Option(fact='foo')
|
|
137
|
+
|
|
138
|
+
async def run_async(self):
|
|
139
|
+
self.foo = 'bar'
|
|
140
|
+
assert self.foo == 'bar'
|
|
141
|
+
|
|
142
|
+
action = await Action.run_test_async(facts=dict(foo='bar'))
|
|
143
|
+
assert 'ansible_facts' not in action.result
|
|
144
|
+
|
|
145
|
+
action = await Action.run_test_async()
|
|
146
|
+
assert action.result['ansible_facts'] == dict(foo='bar')
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@pytest.mark.asyncio
|
|
150
|
+
async def test_fact_set_mutable():
|
|
151
|
+
class Action(cansible.ActionBase):
|
|
152
|
+
async def run_async(self):
|
|
153
|
+
assert self.mask_values is self.mask.values
|
|
154
|
+
self.mask_values.add('foo')
|
|
155
|
+
|
|
156
|
+
action = await Action.run_test_async()
|
|
157
|
+
assert 'mask_values' in action.facts_values
|
|
158
|
+
assert action.result['ansible_facts'] == dict(mask_values=['foo'])
|
|
159
|
+
|
|
160
|
+
action = await Action.run_test_async(facts=dict(mask_values=['foo']))
|
|
161
|
+
assert 'ansible_facts' not in action.result
|
|
162
|
+
|
|
163
|
+
action = await Action.run_test_async(facts=dict(mask_values=['bar']))
|
|
164
|
+
result = action.result['ansible_facts']['mask_values']
|
|
165
|
+
assert sorted(result) == ['bar', 'foo']
|
|
166
|
+
|
|
167
|
+
|
|
122
168
|
def test_playbook_render(playbook):
|
|
123
169
|
assert playbook.name == 'test_playbook_render'
|
|
124
170
|
playbook.task_add(
|
|
@@ -165,3 +211,12 @@ def test_playbook_exec(playbook):
|
|
|
165
211
|
result = playbook()
|
|
166
212
|
assert result['changed'] == 0
|
|
167
213
|
assert result['ok'] == 2
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_masking_ansible_story(playbook):
|
|
217
|
+
with open('tests/test_ansible_masking.yml', 'r') as f:
|
|
218
|
+
data = yaml.safe_load(f.read())
|
|
219
|
+
playbook.tasks += data[0]['tasks']
|
|
220
|
+
result = playbook()
|
|
221
|
+
expected = "hello ***MASKED*** ***MASKED*** bye\n\nansible_facts:\n\n mask_values:\n\n - \'***MASKED***\'\n\n - \'***MASKED***\'\n\ncmd: echo hello ***MASKED*** ***MASKED*** bye\n\nstdout: hello ***MASKED*** ***MASKED*** bye" # noqa
|
|
222
|
+
assert expected in result['stdout']
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import cansible
|
|
2
2
|
import pytest
|
|
3
3
|
import os
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def test_story():
|
|
7
|
-
variables =
|
|
7
|
+
variables = cansible.Variables(
|
|
8
8
|
root_path=os.path.dirname(__file__),
|
|
9
9
|
pass_path=os.path.dirname(__file__) + '/vault_pass',
|
|
10
10
|
)
|
|
@@ -13,27 +13,34 @@ def test_story():
|
|
|
13
13
|
assert variables['variables_vault.yml'] == dict(bar='foo')
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def
|
|
17
|
-
variables =
|
|
18
|
-
with pytest.raises(
|
|
16
|
+
def test_unresolvable_path_error():
|
|
17
|
+
variables = cansible.Variables()
|
|
18
|
+
with pytest.raises(cansible.UnresolvablePathError) as exc:
|
|
19
19
|
variables['variables.yml']
|
|
20
20
|
assert exc.value.args == (
|
|
21
21
|
'variables.yml must be absolute if root_path not set',
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
-
with pytest.raises(Exception) as exc:
|
|
25
|
-
variables['/variables_vault.yml']
|
|
26
|
-
assert exc.value.args == ('/variables_vault.yml does not exist',)
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
def test_path_not_found_error():
|
|
26
|
+
variables = cansible.Variables()
|
|
27
|
+
with pytest.raises(cansible.PathNotFoundError) as exc:
|
|
28
|
+
variables['/nonexistent.yml']
|
|
29
|
+
assert exc.value.args == ('/nonexistent.yml does not exist',)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_vault_password_required_error():
|
|
33
|
+
variables = cansible.Variables(root_path=os.path.dirname(__file__))
|
|
34
|
+
with pytest.raises(cansible.VaultPasswordFileRequiredError) as exc:
|
|
30
35
|
variables['variables_vault.yml']
|
|
31
36
|
assert exc.value.args == ('Vault password required in pass_path',)
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
|
|
39
|
+
def test_vault_password_file_not_found_error():
|
|
40
|
+
variables = cansible.Variables(
|
|
34
41
|
root_path=os.path.dirname(__file__),
|
|
35
42
|
pass_path='/does/not/exist',
|
|
36
43
|
)
|
|
37
|
-
with pytest.raises(
|
|
44
|
+
with pytest.raises(cansible.VaultPasswordFileNotFoundError) as exc:
|
|
38
45
|
variables['variables_vault.yml']
|
|
39
46
|
assert exc.value.args == ('/does/not/exist does not exist',)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import cli2
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest import mock
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_async_run_noloop():
|
|
8
|
+
async def task():
|
|
9
|
+
await asyncio.sleep(.01)
|
|
10
|
+
|
|
11
|
+
cli2.async_run(task())
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.asyncio
|
|
15
|
+
async def test_async_run_loop():
|
|
16
|
+
async def task():
|
|
17
|
+
await asyncio.sleep(.01)
|
|
18
|
+
|
|
19
|
+
cli2.async_run(task())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.asyncio
|
|
23
|
+
async def test_queue_basic():
|
|
24
|
+
async def task(i):
|
|
25
|
+
return i
|
|
26
|
+
|
|
27
|
+
queue = cli2.Queue()
|
|
28
|
+
await queue.run(task(1), task(2), task(3))
|
|
29
|
+
|
|
30
|
+
assert sorted(queue.results) == [1, 2, 3]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.asyncio
|
|
34
|
+
async def test_queue_concurrency():
|
|
35
|
+
start = asyncio.get_event_loop().time()
|
|
36
|
+
|
|
37
|
+
async def worker(i):
|
|
38
|
+
await asyncio.sleep(0.1)
|
|
39
|
+
return i
|
|
40
|
+
|
|
41
|
+
queue = cli2.Queue(num_workers=3)
|
|
42
|
+
await queue.run(worker(1), worker(2), worker(3))
|
|
43
|
+
|
|
44
|
+
duration = asyncio.get_event_loop().time() - start
|
|
45
|
+
assert duration < 0.15, 'Should run in parallel'
|
|
46
|
+
assert sorted(queue.results) == [1, 2, 3]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.asyncio
|
|
50
|
+
async def test_queue_error_handling(monkeypatch):
|
|
51
|
+
import cli2.asyncio
|
|
52
|
+
logger = mock.Mock()
|
|
53
|
+
monkeypatch.setattr(cli2.asyncio, 'log', logger)
|
|
54
|
+
|
|
55
|
+
async def worker(i):
|
|
56
|
+
if i == 2:
|
|
57
|
+
raise ValueError('test error')
|
|
58
|
+
await asyncio.sleep(0.01)
|
|
59
|
+
return i
|
|
60
|
+
|
|
61
|
+
queue = cli2.Queue()
|
|
62
|
+
await queue.run(worker(1), worker(2), worker(3))
|
|
63
|
+
|
|
64
|
+
# Error should not stop other tasks
|
|
65
|
+
assert queue.results == [1, 3]
|
|
66
|
+
|
|
67
|
+
# Exceptions should be printed though, not swallowed
|
|
68
|
+
logger.exception.assert_called_once()
|