cli2 4.0.4__tar.gz → 4.0.6__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.0.4/cli2.egg-info → cli2-4.0.6}/PKG-INFO +1 -1
- {cli2-4.0.4 → cli2-4.0.6}/cli2/__init__.py +12 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/ansible/__init__.py +1 -0
- cli2-4.0.6/cli2/ansible/variables.py +115 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/cli.py +2 -0
- {cli2-4.0.4 → cli2-4.0.6/cli2.egg-info}/PKG-INFO +1 -1
- {cli2-4.0.4 → cli2-4.0.6}/cli2.egg-info/SOURCES.txt +2 -0
- {cli2-4.0.4 → cli2-4.0.6}/setup.py +1 -1
- cli2-4.0.6/tests/test_ansible_variables.py +39 -0
- {cli2-4.0.4 → cli2-4.0.6}/tests/test_group.py +14 -2
- {cli2-4.0.4 → cli2-4.0.6}/MANIFEST.in +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/README.rst +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/classifiers.txt +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/ansible/action.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/ansible/playbook.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/ansible/pytest.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/asyncio.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/cli2.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/client.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/colors.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/configuration.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/decorators.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/display.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/examples/__init__.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/examples/client.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/examples/conf.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/examples/example.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/examples/example_obj.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/examples/nesting.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/examples/obj.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/examples/obj2.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/examples/test.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/lock.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/log.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/node.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/sphinx.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/table.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2/test.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2.egg-info/dependency_links.txt +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2.egg-info/entry_points.txt +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2.egg-info/requires.txt +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/cli2.egg-info/top_level.txt +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/setup.cfg +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/tests/test_ansible.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/tests/test_cli.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/tests/test_client.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/tests/test_command.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/tests/test_configuration.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/tests/test_decorators.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/tests/test_display.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/tests/test_entry_point.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/tests/test_inject.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/tests/test_lock.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/tests/test_node.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/tests/test_restful.py +0 -0
- {cli2-4.0.4 → cli2-4.0.6}/tests/test_table.py +0 -0
|
@@ -40,3 +40,15 @@ from .display import diff, diff_data, render, print, highlight
|
|
|
40
40
|
from .lock import Lock
|
|
41
41
|
from .log import configure, log, get_logger
|
|
42
42
|
from .table import Table
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def which(cmd):
|
|
46
|
+
""" Wrapper around shutil.which, and also check for ~/.local/bin. """
|
|
47
|
+
import shutil
|
|
48
|
+
path = shutil.which(cmd)
|
|
49
|
+
if path:
|
|
50
|
+
return path
|
|
51
|
+
|
|
52
|
+
path = Path(os.getenv('HOME')) / '.local/bin' / cmd
|
|
53
|
+
if path.exists():
|
|
54
|
+
return str(path)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ansible variables file reader with vault support
|
|
3
|
+
|
|
4
|
+
Why not use the Ansible Python API? We don't have a lot to do here, and the CLI
|
|
5
|
+
are less likely to be subject to changes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import cli2
|
|
9
|
+
import functools
|
|
10
|
+
import subprocess
|
|
11
|
+
import yaml
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Vault(yaml.YAMLObject):
|
|
16
|
+
yaml_tag = '!vault'
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def from_yaml(cls, loader, node):
|
|
20
|
+
"""
|
|
21
|
+
Convert a representation node to a Python object.
|
|
22
|
+
"""
|
|
23
|
+
return subprocess.check_output(
|
|
24
|
+
f'echo \'{node.value}\''
|
|
25
|
+
f' | {cls.ansible_vault}'
|
|
26
|
+
f' decrypt --vault-password-file {cls.pass_path}',
|
|
27
|
+
shell=True,
|
|
28
|
+
).decode().strip()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Variables(dict):
|
|
32
|
+
"""
|
|
33
|
+
Ansible variables reader.
|
|
34
|
+
|
|
35
|
+
In general, it should be instanciated with :py:attr:`root_path` and
|
|
36
|
+
:py:attr:`pass_path` to fully function correctly.
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
|
|
40
|
+
.. code-block:: python
|
|
41
|
+
|
|
42
|
+
import cli2.ansible
|
|
43
|
+
variables = cli2.ansible.Variables(
|
|
44
|
+
root_path=Path(__file__).parent,
|
|
45
|
+
pass_path='~/.vault_password',
|
|
46
|
+
)
|
|
47
|
+
print(variables['playbooks/vars/example.yml'])
|
|
48
|
+
|
|
49
|
+
Every file read is cached in the variables object.
|
|
50
|
+
|
|
51
|
+
.. py:attribute:: root_path
|
|
52
|
+
|
|
53
|
+
Unless you feed this with only absolute path, you'll need a root_path
|
|
54
|
+
so that relative paths can be resolved. This should be your collection
|
|
55
|
+
root.
|
|
56
|
+
|
|
57
|
+
.. py:attribute:: pass_path
|
|
58
|
+
|
|
59
|
+
Unless you don't use ansible-vault, you'll need to give the pass to the
|
|
60
|
+
vault password here.
|
|
61
|
+
"""
|
|
62
|
+
def __init__(self, root_path=None, pass_path=None):
|
|
63
|
+
self.root_path = Path(root_path) if root_path else None
|
|
64
|
+
self.pass_path = Path(pass_path) if pass_path else None
|
|
65
|
+
|
|
66
|
+
def __getitem__(self, key):
|
|
67
|
+
if key not in self:
|
|
68
|
+
self.read(key)
|
|
69
|
+
return super().__getitem__(key)
|
|
70
|
+
|
|
71
|
+
@functools.cached_property
|
|
72
|
+
def ansible_vault(self):
|
|
73
|
+
return cli2.which('ansible-vault')
|
|
74
|
+
|
|
75
|
+
def read(self, path):
|
|
76
|
+
"""
|
|
77
|
+
Read an ansible YAML variable file.
|
|
78
|
+
|
|
79
|
+
:param path: Absolute path or path relative to :py:attr:`root_path`
|
|
80
|
+
"""
|
|
81
|
+
key = path
|
|
82
|
+
path = Path(path)
|
|
83
|
+
|
|
84
|
+
if path.is_absolute():
|
|
85
|
+
path = path
|
|
86
|
+
elif self.root_path:
|
|
87
|
+
path = self.root_path / path
|
|
88
|
+
else:
|
|
89
|
+
raise Exception(f'{path} must be absolute if root_path not set')
|
|
90
|
+
|
|
91
|
+
if not path.exists():
|
|
92
|
+
raise Exception(f'{path} does not exist')
|
|
93
|
+
|
|
94
|
+
with path.open('r') as f:
|
|
95
|
+
content = f.read()
|
|
96
|
+
|
|
97
|
+
if content.strip().startswith('$ANSIBLE_VAULT'):
|
|
98
|
+
if not self.pass_path:
|
|
99
|
+
raise Exception('Vault password required in pass_path')
|
|
100
|
+
if not self.pass_path.exists():
|
|
101
|
+
raise Exception(f'{self.pass_path} does not exist')
|
|
102
|
+
args = [
|
|
103
|
+
self.ansible_vault,
|
|
104
|
+
'view',
|
|
105
|
+
'--vault-password-file',
|
|
106
|
+
str(self.pass_path),
|
|
107
|
+
str(path),
|
|
108
|
+
]
|
|
109
|
+
content = subprocess.check_output(args)
|
|
110
|
+
|
|
111
|
+
# todo: find a thread safe way to use our YAMLObject
|
|
112
|
+
Vault.ansible_vault = self.ansible_vault
|
|
113
|
+
Vault.pass_path = self.pass_path
|
|
114
|
+
self[key] = yaml.load(content, Loader=yaml.FullLoader)
|
|
115
|
+
return self[key]
|
|
@@ -27,6 +27,7 @@ cli2/ansible/__init__.py
|
|
|
27
27
|
cli2/ansible/action.py
|
|
28
28
|
cli2/ansible/playbook.py
|
|
29
29
|
cli2/ansible/pytest.py
|
|
30
|
+
cli2/ansible/variables.py
|
|
30
31
|
cli2/examples/__init__.py
|
|
31
32
|
cli2/examples/client.py
|
|
32
33
|
cli2/examples/conf.py
|
|
@@ -37,6 +38,7 @@ cli2/examples/obj.py
|
|
|
37
38
|
cli2/examples/obj2.py
|
|
38
39
|
cli2/examples/test.py
|
|
39
40
|
tests/test_ansible.py
|
|
41
|
+
tests/test_ansible_variables.py
|
|
40
42
|
tests/test_cli.py
|
|
41
43
|
tests/test_client.py
|
|
42
44
|
tests/test_command.py
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from cli2 import ansible
|
|
2
|
+
import pytest
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_story():
|
|
7
|
+
variables = ansible.Variables(
|
|
8
|
+
root_path=os.path.dirname(__file__),
|
|
9
|
+
pass_path=os.path.dirname(__file__) + '/vault_pass',
|
|
10
|
+
)
|
|
11
|
+
variables.read('variables.yml')
|
|
12
|
+
assert variables['variables.yml'] == dict(foo='bar', vaulted='foobar')
|
|
13
|
+
assert variables['variables_vault.yml'] == dict(bar='foo')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_exceptions():
|
|
17
|
+
variables = ansible.Variables()
|
|
18
|
+
with pytest.raises(Exception) as exc:
|
|
19
|
+
variables['variables.yml']
|
|
20
|
+
assert exc.value.args == (
|
|
21
|
+
'variables.yml must be absolute if root_path not set',
|
|
22
|
+
)
|
|
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
|
+
|
|
28
|
+
variables = ansible.Variables(root_path=os.path.dirname(__file__))
|
|
29
|
+
with pytest.raises(Exception) as exc:
|
|
30
|
+
variables['variables_vault.yml']
|
|
31
|
+
assert exc.value.args == ('Vault password required in pass_path',)
|
|
32
|
+
|
|
33
|
+
variables = ansible.Variables(
|
|
34
|
+
root_path=os.path.dirname(__file__),
|
|
35
|
+
pass_path='/does/not/exist',
|
|
36
|
+
)
|
|
37
|
+
with pytest.raises(Exception) as exc:
|
|
38
|
+
variables['variables_vault.yml']
|
|
39
|
+
assert exc.value.args == ('/does/not/exist does not exist',)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import cli2
|
|
2
|
+
import functools
|
|
2
3
|
import cli2.test
|
|
3
4
|
|
|
4
5
|
|
|
@@ -188,9 +189,12 @@ def test_overrides():
|
|
|
188
189
|
|
|
189
190
|
|
|
190
191
|
def test_load():
|
|
191
|
-
|
|
192
|
+
class BarMetaclass(type):
|
|
193
|
+
@property
|
|
194
|
+
def fails3(self):
|
|
195
|
+
raise Exception('fails')
|
|
192
196
|
|
|
193
|
-
class Bar:
|
|
197
|
+
class Bar(metaclass=BarMetaclass):
|
|
194
198
|
@cli2.cmd
|
|
195
199
|
def test(self):
|
|
196
200
|
pass
|
|
@@ -212,6 +216,14 @@ def test_load():
|
|
|
212
216
|
def exclude(self):
|
|
213
217
|
pass
|
|
214
218
|
|
|
219
|
+
@property
|
|
220
|
+
def fails(self):
|
|
221
|
+
raise Exception('Loading should not eval properties')
|
|
222
|
+
|
|
223
|
+
@functools.cached_property
|
|
224
|
+
def fails2(self):
|
|
225
|
+
raise Exception('Loading should not eval properties')
|
|
226
|
+
|
|
215
227
|
group = cli2.Group()
|
|
216
228
|
group.load(Foo)
|
|
217
229
|
assert list(group.keys()) == ['help', 'test', 'classmeth', 'test2']
|
|
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
|