pyinfra 3.0.dev0__py2.py3-none-any.whl → 3.0.2__py2.py3-none-any.whl
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.
- pyinfra/api/__init__.py +3 -0
- pyinfra/api/arguments.py +115 -97
- pyinfra/api/arguments_typed.py +80 -0
- pyinfra/api/command.py +5 -3
- pyinfra/api/config.py +139 -39
- pyinfra/api/connectors.py +5 -2
- pyinfra/api/deploy.py +19 -19
- pyinfra/api/exceptions.py +35 -4
- pyinfra/api/facts.py +62 -86
- pyinfra/api/host.py +102 -15
- pyinfra/api/inventory.py +4 -0
- pyinfra/api/operation.py +188 -120
- pyinfra/api/operations.py +66 -113
- pyinfra/api/state.py +53 -34
- pyinfra/api/util.py +64 -33
- pyinfra/connectors/base.py +65 -20
- pyinfra/connectors/chroot.py +15 -13
- pyinfra/connectors/docker.py +62 -72
- pyinfra/connectors/dockerssh.py +20 -19
- pyinfra/connectors/local.py +32 -22
- pyinfra/connectors/ssh.py +162 -86
- pyinfra/connectors/sshuserclient/client.py +1 -1
- pyinfra/connectors/terraform.py +57 -39
- pyinfra/connectors/util.py +26 -27
- pyinfra/connectors/vagrant.py +27 -26
- pyinfra/context.py +1 -0
- pyinfra/facts/apk.py +7 -2
- pyinfra/facts/apt.py +15 -7
- pyinfra/facts/brew.py +28 -13
- pyinfra/facts/bsdinit.py +9 -6
- pyinfra/facts/cargo.py +6 -3
- pyinfra/facts/choco.py +8 -4
- pyinfra/facts/deb.py +21 -9
- pyinfra/facts/dnf.py +11 -6
- pyinfra/facts/docker.py +30 -5
- pyinfra/facts/files.py +49 -33
- pyinfra/facts/gem.py +7 -2
- pyinfra/facts/git.py +14 -21
- pyinfra/facts/gpg.py +4 -1
- pyinfra/facts/hardware.py +186 -138
- pyinfra/facts/launchd.py +7 -2
- pyinfra/facts/lxd.py +8 -2
- pyinfra/facts/mysql.py +19 -12
- pyinfra/facts/npm.py +3 -1
- pyinfra/facts/openrc.py +8 -2
- pyinfra/facts/pacman.py +13 -5
- pyinfra/facts/pip.py +2 -0
- pyinfra/facts/pkg.py +5 -1
- pyinfra/facts/pkgin.py +7 -2
- pyinfra/facts/postgres.py +170 -0
- pyinfra/facts/postgresql.py +5 -162
- pyinfra/facts/rpm.py +21 -15
- pyinfra/facts/runit.py +70 -0
- pyinfra/facts/selinux.py +12 -4
- pyinfra/facts/server.py +240 -82
- pyinfra/facts/snap.py +8 -2
- pyinfra/facts/systemd.py +37 -13
- pyinfra/facts/sysvinit.py +7 -4
- pyinfra/facts/upstart.py +7 -2
- pyinfra/facts/util/packaging.py +3 -2
- pyinfra/facts/vzctl.py +8 -4
- pyinfra/facts/xbps.py +7 -2
- pyinfra/facts/yum.py +10 -5
- pyinfra/facts/zypper.py +9 -4
- pyinfra/operations/apk.py +5 -3
- pyinfra/operations/apt.py +28 -25
- pyinfra/operations/brew.py +60 -29
- pyinfra/operations/bsdinit.py +6 -4
- pyinfra/operations/cargo.py +3 -1
- pyinfra/operations/choco.py +3 -1
- pyinfra/operations/dnf.py +16 -20
- pyinfra/operations/docker.py +339 -0
- pyinfra/operations/files.py +187 -168
- pyinfra/operations/gem.py +3 -1
- pyinfra/operations/git.py +23 -25
- pyinfra/operations/iptables.py +33 -25
- pyinfra/operations/launchd.py +5 -6
- pyinfra/operations/lxd.py +7 -4
- pyinfra/operations/mysql.py +59 -55
- pyinfra/operations/npm.py +8 -1
- pyinfra/operations/openrc.py +5 -3
- pyinfra/operations/pacman.py +6 -7
- pyinfra/operations/pip.py +19 -12
- pyinfra/operations/pkg.py +3 -1
- pyinfra/operations/pkgin.py +5 -3
- pyinfra/operations/postgres.py +349 -0
- pyinfra/operations/postgresql.py +18 -335
- pyinfra/operations/puppet.py +3 -1
- pyinfra/operations/python.py +8 -19
- pyinfra/operations/runit.py +182 -0
- pyinfra/operations/selinux.py +47 -29
- pyinfra/operations/server.py +138 -67
- pyinfra/operations/snap.py +3 -1
- pyinfra/operations/ssh.py +18 -16
- pyinfra/operations/systemd.py +18 -12
- pyinfra/operations/sysvinit.py +7 -5
- pyinfra/operations/upstart.py +7 -5
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/docker.py +177 -0
- pyinfra/operations/util/files.py +24 -16
- pyinfra/operations/util/packaging.py +54 -38
- pyinfra/operations/util/service.py +39 -47
- pyinfra/operations/vzctl.py +12 -10
- pyinfra/operations/xbps.py +5 -3
- pyinfra/operations/yum.py +15 -19
- pyinfra/operations/zypper.py +9 -10
- pyinfra/version.py +5 -2
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/METADATA +51 -58
- pyinfra-3.0.2.dist-info/RECORD +168 -0
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/WHEEL +1 -1
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/entry_points.txt +0 -3
- pyinfra_cli/__main__.py +4 -3
- pyinfra_cli/commands.py +3 -2
- pyinfra_cli/exceptions.py +75 -43
- pyinfra_cli/inventory.py +52 -31
- pyinfra_cli/log.py +10 -2
- pyinfra_cli/main.py +88 -65
- pyinfra_cli/prints.py +37 -109
- pyinfra_cli/util.py +15 -10
- tests/test_api/test_api.py +2 -0
- tests/test_api/test_api_arguments.py +9 -9
- tests/test_api/test_api_deploys.py +15 -19
- tests/test_api/test_api_facts.py +4 -5
- tests/test_api/test_api_operations.py +18 -20
- tests/test_api/test_api_util.py +41 -2
- tests/test_cli/test_cli.py +14 -50
- tests/test_cli/test_cli_deploy.py +17 -14
- tests/test_cli/test_cli_exceptions.py +50 -19
- tests/test_cli/test_cli_inventory.py +66 -0
- tests/test_cli/util.py +1 -1
- tests/test_connectors/test_dockerssh.py +11 -8
- tests/test_connectors/test_ssh.py +88 -23
- tests/test_connectors/test_sshuserclient.py +1 -1
- tests/test_connectors/test_terraform.py +11 -8
- tests/test_connectors/test_vagrant.py +6 -6
- pyinfra/connectors/ansible.py +0 -175
- pyinfra/connectors/mech.py +0 -189
- pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
- pyinfra/connectors/winrm.py +0 -312
- pyinfra/facts/windows.py +0 -366
- pyinfra/facts/windows_files.py +0 -90
- pyinfra/operations/windows.py +0 -59
- pyinfra/operations/windows_files.py +0 -538
- pyinfra-3.0.dev0.dist-info/RECORD +0 -170
- tests/test_connectors/test_ansible.py +0 -64
- tests/test_connectors/test_mech.py +0 -126
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/LICENSE.md +0 -0
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/top_level.txt +0 -0
|
@@ -9,6 +9,14 @@ from .util import run_cli
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class TestCliDeployState(PatchSSHTestCase):
|
|
12
|
+
def _run_cli(self, hosts, filename):
|
|
13
|
+
return run_cli(
|
|
14
|
+
"-y",
|
|
15
|
+
",".join(hosts),
|
|
16
|
+
path.join("tests", "test_cli", "deploy", filename),
|
|
17
|
+
f'--chdir={path.join("tests", "test_cli", "deploy")}',
|
|
18
|
+
)
|
|
19
|
+
|
|
12
20
|
def _assert_op_data(self, correct_op_name_and_host_names):
|
|
13
21
|
op_order = state.get_op_order()
|
|
14
22
|
|
|
@@ -25,10 +33,14 @@ class TestCliDeployState(PatchSSHTestCase):
|
|
|
25
33
|
assert list(op_meta.names)[0] == correct_op_name
|
|
26
34
|
|
|
27
35
|
for host in state.inventory:
|
|
36
|
+
executed = False
|
|
37
|
+
host_op = state.ops[host].get(op_hash)
|
|
38
|
+
if host_op:
|
|
39
|
+
executed = host_op.operation_meta.executed
|
|
28
40
|
if correct_host_names is True or host.name in correct_host_names:
|
|
29
|
-
|
|
41
|
+
assert executed is True
|
|
30
42
|
else:
|
|
31
|
-
|
|
43
|
+
assert executed is False
|
|
32
44
|
|
|
33
45
|
def test_deploy(self):
|
|
34
46
|
task_file_path = path.join("tasks", "a_task.py")
|
|
@@ -64,6 +76,7 @@ class TestCliDeployState(PatchSSHTestCase):
|
|
|
64
76
|
("Nested order loop 2/1", ("somehost", "anotherhost")),
|
|
65
77
|
("Nested order loop 2/2", ("somehost", "anotherhost")),
|
|
66
78
|
("Final limited operation", ("somehost",)),
|
|
79
|
+
("Second final limited operation", ("anotherhost", "someotherhost")),
|
|
67
80
|
]
|
|
68
81
|
|
|
69
82
|
# Run 3 iterations of the test - each time shuffling the order of the
|
|
@@ -74,12 +87,7 @@ class TestCliDeployState(PatchSSHTestCase):
|
|
|
74
87
|
hosts = ["somehost", "anotherhost", "someotherhost"]
|
|
75
88
|
shuffle(hosts)
|
|
76
89
|
|
|
77
|
-
result =
|
|
78
|
-
"-y",
|
|
79
|
-
",".join(hosts),
|
|
80
|
-
path.join("tests", "deploy", "deploy.py"),
|
|
81
|
-
f'--chdir={path.join("tests", "deploy")}',
|
|
82
|
-
)
|
|
90
|
+
result = self._run_cli(hosts, "deploy.py")
|
|
83
91
|
assert result.exit_code == 0, result.stdout
|
|
84
92
|
|
|
85
93
|
self._assert_op_data(correct_op_name_and_host_names)
|
|
@@ -104,12 +112,7 @@ class TestCliDeployState(PatchSSHTestCase):
|
|
|
104
112
|
hosts = ["somehost", "anotherhost", "someotherhost"]
|
|
105
113
|
shuffle(hosts)
|
|
106
114
|
|
|
107
|
-
result =
|
|
108
|
-
"-y",
|
|
109
|
-
",".join(hosts),
|
|
110
|
-
path.join("tests", "deploy", "deploy_random.py"),
|
|
111
|
-
f'--chdir={path.join("tests", "deploy")}',
|
|
112
|
-
)
|
|
115
|
+
result = self._run_cli(hosts, "deploy_random.py")
|
|
113
116
|
assert result.exit_code == 0, result.stdout
|
|
114
117
|
|
|
115
118
|
self._assert_op_data(correct_op_name_and_host_names)
|
|
@@ -1,34 +1,27 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from os import path
|
|
1
3
|
from unittest import TestCase
|
|
2
4
|
|
|
5
|
+
import pytest
|
|
3
6
|
from click.testing import CliRunner
|
|
4
7
|
|
|
5
|
-
from
|
|
8
|
+
from pyinfra.api import OperationError
|
|
9
|
+
from pyinfra.api.exceptions import ArgumentTypeError
|
|
10
|
+
from pyinfra_cli.exceptions import CliError, UnexpectedExternalError, WrappedError
|
|
6
11
|
from pyinfra_cli.main import cli
|
|
7
12
|
|
|
13
|
+
from .util import run_cli
|
|
14
|
+
|
|
8
15
|
|
|
9
16
|
class TestCliExceptions(TestCase):
|
|
10
17
|
@classmethod
|
|
11
18
|
def setUpClass(cls):
|
|
12
|
-
cls.
|
|
13
|
-
cls.old_cli_show = CliError.show
|
|
14
|
-
|
|
15
|
-
@classmethod
|
|
16
|
-
def tearDownClass(cls):
|
|
17
|
-
CliError.show = cls.old_cli_show
|
|
18
|
-
|
|
19
|
-
def setUp(self):
|
|
20
|
-
self.exception = None
|
|
21
|
-
CliError.show = lambda e: self.capture_cli_error(e)
|
|
22
|
-
|
|
23
|
-
def capture_cli_error(self, e):
|
|
24
|
-
self.exception = e
|
|
25
|
-
self.old_cli_show()
|
|
19
|
+
cls.runner = CliRunner()
|
|
26
20
|
|
|
27
21
|
def assert_cli_exception(self, args, message):
|
|
28
|
-
self.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
assert self.exception.message == message
|
|
22
|
+
result = self.runner.invoke(cli, args, standalone_mode=False)
|
|
23
|
+
self.assertIsInstance(result.exception, CliError)
|
|
24
|
+
assert getattr(result.exception, "message") == message
|
|
32
25
|
|
|
33
26
|
def test_bad_deploy_file(self):
|
|
34
27
|
self.assert_cli_exception(
|
|
@@ -53,3 +46,41 @@ class TestCliExceptions(TestCase):
|
|
|
53
46
|
["my-server.net", "fact", "server.NotAFact"],
|
|
54
47
|
"No such attribute in module pyinfra.facts.server: NotAFact",
|
|
55
48
|
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestCliDeployExceptions(TestCase):
|
|
52
|
+
def _run_cli(self, hosts, filename):
|
|
53
|
+
return run_cli(
|
|
54
|
+
"-y",
|
|
55
|
+
",".join(hosts),
|
|
56
|
+
path.join("tests", "test_cli", "deploy_fails", filename),
|
|
57
|
+
f'--chdir={path.join("tests", "test_cli", "deploy_fails")}',
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def test_invalid_argument_type(self):
|
|
61
|
+
result = self._run_cli(["@local"], "invalid_argument_type.py")
|
|
62
|
+
assert isinstance(result.exception, WrappedError)
|
|
63
|
+
assert isinstance(result.exception.exception, ArgumentTypeError)
|
|
64
|
+
assert (
|
|
65
|
+
result.exception.exception.args[0]
|
|
66
|
+
== "Invalid argument `_sudo`:: None is not an instance of bool"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def test_invalid_operation_arg(self):
|
|
70
|
+
result = self._run_cli(["@local"], "invalid_operation_arg.py")
|
|
71
|
+
assert isinstance(result.exception, UnexpectedExternalError)
|
|
72
|
+
assert isinstance(result.exception.exception, TypeError)
|
|
73
|
+
assert result.exception.filename == "invalid_operation_arg.py"
|
|
74
|
+
assert result.exception.exception.args[0] == "missing a required argument: 'commands'"
|
|
75
|
+
|
|
76
|
+
@pytest.mark.skipif(
|
|
77
|
+
sys.platform.startswith("win"),
|
|
78
|
+
reason="The operation is not compatible with Windows",
|
|
79
|
+
)
|
|
80
|
+
def test_operation_error(self):
|
|
81
|
+
result = self._run_cli(["@local"], "operation_error.py")
|
|
82
|
+
assert isinstance(result.exception, WrappedError)
|
|
83
|
+
assert isinstance(result.exception.exception, OperationError)
|
|
84
|
+
assert (
|
|
85
|
+
result.exception.exception.args[0] == "operation_error.py exists and is not a directory"
|
|
86
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from os import path
|
|
2
|
+
|
|
3
|
+
from pyinfra import inventory
|
|
4
|
+
from pyinfra.context import ctx_inventory, ctx_state
|
|
5
|
+
|
|
6
|
+
from ..paramiko_util import PatchSSHTestCase
|
|
7
|
+
from .util import run_cli
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestCliInventory(PatchSSHTestCase):
|
|
11
|
+
def test_load_deploy_group_data(self):
|
|
12
|
+
ctx_state.reset()
|
|
13
|
+
ctx_inventory.reset()
|
|
14
|
+
|
|
15
|
+
hosts = ["somehost", "anotherhost", "someotherhost"]
|
|
16
|
+
result = run_cli(
|
|
17
|
+
"-y",
|
|
18
|
+
",".join(hosts),
|
|
19
|
+
path.join("tests", "test_cli", "deploy", "deploy.py"),
|
|
20
|
+
f'--chdir={path.join("tests", "test_cli", "deploy")}',
|
|
21
|
+
)
|
|
22
|
+
assert result.exit_code == 0, result.stdout
|
|
23
|
+
|
|
24
|
+
assert inventory.data.get("hello") == "world"
|
|
25
|
+
assert "leftover_data" in inventory.group_data
|
|
26
|
+
assert inventory.group_data["leftover_data"].get("still_parsed") == "never_used"
|
|
27
|
+
assert inventory.group_data["leftover_data"].get("_global_arg") == "gets_parsed"
|
|
28
|
+
|
|
29
|
+
def test_load_group_data(self):
|
|
30
|
+
ctx_state.reset()
|
|
31
|
+
ctx_inventory.reset()
|
|
32
|
+
|
|
33
|
+
hosts = ["somehost", "anotherhost", "someotherhost"]
|
|
34
|
+
result = run_cli(
|
|
35
|
+
"-y",
|
|
36
|
+
",".join(hosts),
|
|
37
|
+
f'--group-data={path.join("tests", "test_cli", "deploy", "group_data")}',
|
|
38
|
+
"exec",
|
|
39
|
+
"uptime",
|
|
40
|
+
)
|
|
41
|
+
assert result.exit_code == 0, result.stdout
|
|
42
|
+
|
|
43
|
+
assert inventory.data.get("hello") == "world"
|
|
44
|
+
assert "leftover_data" in inventory.group_data
|
|
45
|
+
assert inventory.group_data["leftover_data"].get("still_parsed") == "never_used"
|
|
46
|
+
assert inventory.group_data["leftover_data"].get("_global_arg") == "gets_parsed"
|
|
47
|
+
|
|
48
|
+
def test_load_group_data_file(self):
|
|
49
|
+
ctx_state.reset()
|
|
50
|
+
ctx_inventory.reset()
|
|
51
|
+
|
|
52
|
+
hosts = ["somehost", "anotherhost", "someotherhost"]
|
|
53
|
+
filename = path.join("tests", "test_cli", "deploy", "group_data", "leftover_data.py")
|
|
54
|
+
result = run_cli(
|
|
55
|
+
"-y",
|
|
56
|
+
",".join(hosts),
|
|
57
|
+
f"--group-data={filename}",
|
|
58
|
+
"exec",
|
|
59
|
+
"uptime",
|
|
60
|
+
)
|
|
61
|
+
assert result.exit_code == 0, result.stdout
|
|
62
|
+
|
|
63
|
+
assert "hello" not in inventory.data
|
|
64
|
+
assert "leftover_data" in inventory.group_data
|
|
65
|
+
assert inventory.group_data["leftover_data"].get("still_parsed") == "never_used"
|
|
66
|
+
assert inventory.group_data["leftover_data"].get("_global_arg") == "gets_parsed"
|
tests/test_cli/util.py
CHANGED
|
@@ -31,6 +31,9 @@ def fake_ssh_docker_shell(
|
|
|
31
31
|
if str(command).startswith("rm -f"):
|
|
32
32
|
return (True, CommandOutput([]))
|
|
33
33
|
|
|
34
|
+
if "$TMPDIR" in str(command):
|
|
35
|
+
return (True, CommandOutput([]))
|
|
36
|
+
|
|
34
37
|
# This is a bit messy. But it's easier than trying to swap out a mock
|
|
35
38
|
# when it needs to be used...
|
|
36
39
|
if fake_ssh_docker_shell.custom_command:
|
|
@@ -150,10 +153,10 @@ class TestDockerSSHConnector(TestCase):
|
|
|
150
153
|
]
|
|
151
154
|
|
|
152
155
|
inventory = make_inventory(hosts=("@dockerssh/somehost:not-an-image",))
|
|
153
|
-
|
|
154
|
-
state.get_temp_filename = lambda _: "remote_tempfile"
|
|
156
|
+
State(inventory, Config())
|
|
155
157
|
|
|
156
158
|
host = inventory.get_host("@dockerssh/somehost:not-an-image")
|
|
159
|
+
host.get_temp_filename = lambda _: "remote_tempfile"
|
|
157
160
|
host.connect()
|
|
158
161
|
|
|
159
162
|
host.put_file("not-a-file", "not-another-file", print_output=True)
|
|
@@ -167,10 +170,10 @@ class TestDockerSSHConnector(TestCase):
|
|
|
167
170
|
@patch("pyinfra.connectors.ssh.SSHConnector.put_file")
|
|
168
171
|
def test_put_file_error(self, fake_put_file):
|
|
169
172
|
inventory = make_inventory(hosts=("@dockerssh/somehost:not-an-image",))
|
|
170
|
-
|
|
171
|
-
state.get_temp_filename = lambda _: "remote_tempfile"
|
|
173
|
+
State(inventory, Config())
|
|
172
174
|
|
|
173
175
|
host = inventory.get_host("@dockerssh/somehost:not-an-image")
|
|
176
|
+
host.get_temp_filename = lambda _: "remote_tempfile"
|
|
174
177
|
host.connect()
|
|
175
178
|
|
|
176
179
|
# SSH error
|
|
@@ -199,10 +202,10 @@ class TestDockerSSHConnector(TestCase):
|
|
|
199
202
|
]
|
|
200
203
|
|
|
201
204
|
inventory = make_inventory(hosts=("@dockerssh/somehost:not-an-image",))
|
|
202
|
-
|
|
203
|
-
state.get_temp_filename = lambda _: "remote_tempfile"
|
|
205
|
+
State(inventory, Config())
|
|
204
206
|
|
|
205
207
|
host = inventory.get_host("@dockerssh/somehost:not-an-image")
|
|
208
|
+
host.get_temp_filename = lambda _: "remote_tempfile"
|
|
206
209
|
host.connect()
|
|
207
210
|
|
|
208
211
|
host.get_file("not-a-file", "not-another-file", print_output=True)
|
|
@@ -222,10 +225,10 @@ class TestDockerSSHConnector(TestCase):
|
|
|
222
225
|
]
|
|
223
226
|
|
|
224
227
|
inventory = make_inventory(hosts=("@dockerssh/somehost:not-an-image",))
|
|
225
|
-
|
|
226
|
-
state.get_temp_filename = lambda _: "remote_tempfile"
|
|
228
|
+
State(inventory, Config())
|
|
227
229
|
|
|
228
230
|
host = inventory.get_host("@dockerssh/somehost:not-an-image")
|
|
231
|
+
host.get_temp_filename = lambda _: "remote_tempfile"
|
|
229
232
|
host.connect()
|
|
230
233
|
|
|
231
234
|
fake_get_file.return_value = True
|
|
@@ -102,10 +102,10 @@ class TestSSHConnector(TestCase):
|
|
|
102
102
|
pkey=fake_key,
|
|
103
103
|
timeout=10,
|
|
104
104
|
username="vagrant",
|
|
105
|
-
_pyinfra_ssh_forward_agent=
|
|
105
|
+
_pyinfra_ssh_forward_agent=False,
|
|
106
106
|
_pyinfra_ssh_config_file=None,
|
|
107
107
|
_pyinfra_ssh_known_hosts_file=None,
|
|
108
|
-
_pyinfra_ssh_strict_host_key_checking=
|
|
108
|
+
_pyinfra_ssh_strict_host_key_checking="accept-new",
|
|
109
109
|
_pyinfra_ssh_paramiko_connect_kwargs=None,
|
|
110
110
|
)
|
|
111
111
|
|
|
@@ -257,10 +257,10 @@ class TestSSHConnector(TestCase):
|
|
|
257
257
|
pkey=fake_key,
|
|
258
258
|
timeout=10,
|
|
259
259
|
username="vagrant",
|
|
260
|
-
_pyinfra_ssh_forward_agent=
|
|
260
|
+
_pyinfra_ssh_forward_agent=False,
|
|
261
261
|
_pyinfra_ssh_config_file=None,
|
|
262
262
|
_pyinfra_ssh_known_hosts_file=None,
|
|
263
|
-
_pyinfra_ssh_strict_host_key_checking=
|
|
263
|
+
_pyinfra_ssh_strict_host_key_checking="accept-new",
|
|
264
264
|
_pyinfra_ssh_paramiko_connect_kwargs=None,
|
|
265
265
|
)
|
|
266
266
|
|
|
@@ -316,10 +316,10 @@ class TestSSHConnector(TestCase):
|
|
|
316
316
|
pkey=fake_dss_key,
|
|
317
317
|
timeout=10,
|
|
318
318
|
username="vagrant",
|
|
319
|
-
_pyinfra_ssh_forward_agent=
|
|
319
|
+
_pyinfra_ssh_forward_agent=False,
|
|
320
320
|
_pyinfra_ssh_config_file=None,
|
|
321
321
|
_pyinfra_ssh_known_hosts_file=None,
|
|
322
|
-
_pyinfra_ssh_strict_host_key_checking=
|
|
322
|
+
_pyinfra_ssh_strict_host_key_checking="accept-new",
|
|
323
323
|
_pyinfra_ssh_paramiko_connect_kwargs=None,
|
|
324
324
|
)
|
|
325
325
|
|
|
@@ -451,14 +451,24 @@ class TestSSHConnector(TestCase):
|
|
|
451
451
|
|
|
452
452
|
@patch("pyinfra.connectors.util.getpass")
|
|
453
453
|
@patch("pyinfra.connectors.ssh.SSHClient")
|
|
454
|
-
def
|
|
454
|
+
def test_run_shell_command_sudo_password_automatic_prompt(
|
|
455
455
|
self,
|
|
456
456
|
fake_ssh_client,
|
|
457
457
|
fake_getpass,
|
|
458
458
|
):
|
|
459
459
|
fake_ssh = MagicMock()
|
|
460
|
-
|
|
461
|
-
|
|
460
|
+
first_fake_stdout = MagicMock()
|
|
461
|
+
second_fake_stdout = MagicMock()
|
|
462
|
+
third_fake_stdout = MagicMock()
|
|
463
|
+
|
|
464
|
+
first_fake_stdout.__iter__.return_value = ["sudo: a password is required\r"]
|
|
465
|
+
second_fake_stdout.__iter__.return_value = ["/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX"]
|
|
466
|
+
|
|
467
|
+
fake_ssh.exec_command.side_effect = [
|
|
468
|
+
(MagicMock(), first_fake_stdout, MagicMock()), # command w/o sudo password
|
|
469
|
+
(MagicMock(), second_fake_stdout, MagicMock()), # SUDO_ASKPASS_COMMAND
|
|
470
|
+
(MagicMock(), third_fake_stdout, MagicMock()), # command with sudo pw
|
|
471
|
+
]
|
|
462
472
|
|
|
463
473
|
fake_ssh_client.return_value = fake_ssh
|
|
464
474
|
fake_getpass.return_value = "password"
|
|
@@ -469,17 +479,18 @@ class TestSSHConnector(TestCase):
|
|
|
469
479
|
host.connect()
|
|
470
480
|
|
|
471
481
|
command = "echo Šablony"
|
|
472
|
-
|
|
473
|
-
|
|
482
|
+
first_fake_stdout.channel.recv_exit_status.return_value = 1
|
|
483
|
+
second_fake_stdout.channel.recv_exit_status.return_value = 0
|
|
484
|
+
third_fake_stdout.channel.recv_exit_status.return_value = 0
|
|
474
485
|
|
|
475
|
-
out = host.run_shell_command(
|
|
476
|
-
command, _sudo=True, _use_sudo_password=True, print_output=True
|
|
477
|
-
)
|
|
486
|
+
out = host.run_shell_command(command, _sudo=True, print_output=True)
|
|
478
487
|
assert len(out) == 2
|
|
479
488
|
|
|
480
489
|
status, output = out
|
|
481
490
|
assert status is True
|
|
482
491
|
|
|
492
|
+
fake_ssh.exec_command.assert_any_call(("sudo -H -n sh -c 'echo Šablony'"), get_pty=False)
|
|
493
|
+
|
|
483
494
|
fake_ssh.exec_command.assert_called_with(
|
|
484
495
|
(
|
|
485
496
|
"env SUDO_ASKPASS=/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX "
|
|
@@ -491,7 +502,7 @@ class TestSSHConnector(TestCase):
|
|
|
491
502
|
|
|
492
503
|
@patch("pyinfra.connectors.util.getpass")
|
|
493
504
|
@patch("pyinfra.connectors.ssh.SSHClient")
|
|
494
|
-
def
|
|
505
|
+
def test_run_shell_command_sudo_password_automatic_prompt_with_special_chars_in_password(
|
|
495
506
|
self,
|
|
496
507
|
fake_ssh_client,
|
|
497
508
|
fake_getpass,
|
|
@@ -511,7 +522,7 @@ class TestSSHConnector(TestCase):
|
|
|
511
522
|
]
|
|
512
523
|
|
|
513
524
|
fake_ssh_client.return_value = fake_ssh
|
|
514
|
-
fake_getpass.return_value = "
|
|
525
|
+
fake_getpass.return_value = "p@ss'word';"
|
|
515
526
|
|
|
516
527
|
inventory = make_inventory(hosts=("somehost",))
|
|
517
528
|
State(inventory, Config())
|
|
@@ -534,7 +545,7 @@ class TestSSHConnector(TestCase):
|
|
|
534
545
|
fake_ssh.exec_command.assert_called_with(
|
|
535
546
|
(
|
|
536
547
|
"env SUDO_ASKPASS=/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX "
|
|
537
|
-
"PYINFRA_SUDO_PASSWORD=
|
|
548
|
+
"""PYINFRA_SUDO_PASSWORD='p@ss'"'"'word'"'"';' """
|
|
538
549
|
"sudo -H -A -k sh -c 'echo Šablony'"
|
|
539
550
|
),
|
|
540
551
|
get_pty=False,
|
|
@@ -544,13 +555,13 @@ class TestSSHConnector(TestCase):
|
|
|
544
555
|
#
|
|
545
556
|
|
|
546
557
|
@patch("pyinfra.connectors.ssh.SSHClient")
|
|
547
|
-
@patch("pyinfra.connectors.util.
|
|
558
|
+
@patch("pyinfra.connectors.util.getpass")
|
|
548
559
|
def test_run_shell_command_retry_for_sudo_password(
|
|
549
560
|
self,
|
|
550
|
-
|
|
561
|
+
fake_getpass,
|
|
551
562
|
fake_ssh_client,
|
|
552
563
|
):
|
|
553
|
-
|
|
564
|
+
fake_getpass.return_value = "PASSWORD"
|
|
554
565
|
|
|
555
566
|
fake_ssh = MagicMock()
|
|
556
567
|
fake_stdin = MagicMock()
|
|
@@ -570,13 +581,13 @@ class TestSSHConnector(TestCase):
|
|
|
570
581
|
return_values = [1, 0] # return 0 on the second call
|
|
571
582
|
fake_stdout.channel.recv_exit_status.side_effect = lambda: return_values.pop(0)
|
|
572
583
|
|
|
573
|
-
out = host.run_shell_command(command)
|
|
584
|
+
out = host.run_shell_command(command, _sudo=True)
|
|
574
585
|
assert len(out) == 2
|
|
575
586
|
assert out[0] is True
|
|
576
|
-
assert
|
|
587
|
+
assert fake_getpass.called
|
|
577
588
|
fake_ssh.exec_command.assert_called_with(
|
|
578
589
|
"env SUDO_ASKPASS=/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX "
|
|
579
|
-
"PYINFRA_SUDO_PASSWORD=PASSWORD sh -c 'echo hi'",
|
|
590
|
+
"PYINFRA_SUDO_PASSWORD=PASSWORD sudo -H -A -k sh -c 'echo hi'",
|
|
580
591
|
get_pty=False,
|
|
581
592
|
)
|
|
582
593
|
|
|
@@ -1041,3 +1052,57 @@ class TestSSHConnector(TestCase):
|
|
|
1041
1052
|
"not-another-file",
|
|
1042
1053
|
print_output=True,
|
|
1043
1054
|
)
|
|
1055
|
+
|
|
1056
|
+
@patch("pyinfra.connectors.ssh.SSHClient")
|
|
1057
|
+
@patch("pyinfra.connectors.ssh.sleep")
|
|
1058
|
+
def test_ssh_connect_fail_retry(self, fake_sleep, fake_ssh_client):
|
|
1059
|
+
for exception_class in (
|
|
1060
|
+
SSHException,
|
|
1061
|
+
gaierror,
|
|
1062
|
+
socket_error,
|
|
1063
|
+
EOFError,
|
|
1064
|
+
):
|
|
1065
|
+
fake_sleep.reset_mock()
|
|
1066
|
+
fake_ssh_client.reset_mock()
|
|
1067
|
+
|
|
1068
|
+
inventory = make_inventory(
|
|
1069
|
+
hosts=("unresposivehost",), override_data={"ssh_connect_retries": 1}
|
|
1070
|
+
)
|
|
1071
|
+
State(inventory, Config())
|
|
1072
|
+
|
|
1073
|
+
unresposivehost = inventory.get_host("unresposivehost")
|
|
1074
|
+
assert unresposivehost.data.ssh_connect_retries == 1
|
|
1075
|
+
|
|
1076
|
+
fake_ssh_client().connect.side_effect = exception_class()
|
|
1077
|
+
|
|
1078
|
+
with self.assertRaises(ConnectError):
|
|
1079
|
+
unresposivehost.connect(show_errors=False, raise_exceptions=True)
|
|
1080
|
+
|
|
1081
|
+
fake_sleep.assert_called_once()
|
|
1082
|
+
assert fake_ssh_client().connect.call_count == 2
|
|
1083
|
+
|
|
1084
|
+
@patch("pyinfra.connectors.ssh.SSHClient")
|
|
1085
|
+
@patch("pyinfra.connectors.ssh.sleep")
|
|
1086
|
+
def test_ssh_connect_fail_success(self, fake_sleep, fake_ssh_client):
|
|
1087
|
+
for exception_class in (
|
|
1088
|
+
SSHException,
|
|
1089
|
+
gaierror,
|
|
1090
|
+
socket_error,
|
|
1091
|
+
EOFError,
|
|
1092
|
+
):
|
|
1093
|
+
fake_sleep.reset_mock()
|
|
1094
|
+
fake_ssh_client.reset_mock()
|
|
1095
|
+
|
|
1096
|
+
inventory = make_inventory(
|
|
1097
|
+
hosts=("unresposivehost",), override_data={"ssh_connect_retries": 1}
|
|
1098
|
+
)
|
|
1099
|
+
State(inventory, Config())
|
|
1100
|
+
|
|
1101
|
+
unresposivehost = inventory.get_host("unresposivehost")
|
|
1102
|
+
assert unresposivehost.data.ssh_connect_retries == 1
|
|
1103
|
+
|
|
1104
|
+
fake_ssh_client().connect.side_effect = [exception_class(), MagicMock()]
|
|
1105
|
+
|
|
1106
|
+
unresposivehost.connect(show_errors=False, raise_exceptions=True)
|
|
1107
|
+
fake_sleep.assert_called_once()
|
|
1108
|
+
assert fake_ssh_client().connect.call_count == 2
|
|
@@ -7,20 +7,23 @@ from pyinfra.connectors.terraform import TerraformInventoryConnector
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class TestTerraformConnector(TestCase):
|
|
10
|
-
def test_make_names_data_no_output_key(self):
|
|
11
|
-
with self.assertRaises(InventoryError) as context:
|
|
12
|
-
list(TerraformInventoryConnector.make_names_data())
|
|
13
|
-
|
|
14
|
-
assert context.exception.args[0] == "No Terraform output key!"
|
|
15
|
-
|
|
16
10
|
@patch("pyinfra.connectors.terraform.local.shell")
|
|
17
11
|
def test_make_names_data_no_output(self, fake_shell):
|
|
18
|
-
fake_shell.return_value = json.dumps(
|
|
12
|
+
fake_shell.return_value = json.dumps(
|
|
13
|
+
{
|
|
14
|
+
"hello": {
|
|
15
|
+
"world": [],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
)
|
|
19
19
|
|
|
20
20
|
with self.assertRaises(InventoryError) as context:
|
|
21
21
|
list(TerraformInventoryConnector.make_names_data("output_key"))
|
|
22
22
|
|
|
23
|
-
assert
|
|
23
|
+
assert (
|
|
24
|
+
context.exception.args[0]
|
|
25
|
+
== "No Terraform output with key: `output_key`, valid keys:\n - hello.world"
|
|
26
|
+
)
|
|
24
27
|
|
|
25
28
|
@patch("pyinfra.connectors.terraform.local.shell")
|
|
26
29
|
def test_make_names_data_invalid_output(self, fake_shell):
|
|
@@ -69,13 +69,13 @@ class TestVagrantConnector(TestCase):
|
|
|
69
69
|
)
|
|
70
70
|
@patch("pyinfra.connectors.vagrant.path.exists", lambda path: True)
|
|
71
71
|
def test_make_names_data_with_options(self):
|
|
72
|
-
data = VagrantInventoryConnector.make_names_data()
|
|
72
|
+
data = list(VagrantInventoryConnector.make_names_data())
|
|
73
73
|
|
|
74
74
|
assert data == [
|
|
75
75
|
(
|
|
76
76
|
"@vagrant/ubuntu16",
|
|
77
77
|
{
|
|
78
|
-
"ssh_port":
|
|
78
|
+
"ssh_port": 2222,
|
|
79
79
|
"ssh_user": "vagrant",
|
|
80
80
|
"ssh_hostname": "127.0.0.1",
|
|
81
81
|
"ssh_key": "path/to/key",
|
|
@@ -85,7 +85,7 @@ class TestVagrantConnector(TestCase):
|
|
|
85
85
|
(
|
|
86
86
|
"@vagrant/centos7",
|
|
87
87
|
{
|
|
88
|
-
"ssh_port":
|
|
88
|
+
"ssh_port": 2200,
|
|
89
89
|
"ssh_user": "vagrant",
|
|
90
90
|
"ssh_hostname": "127.0.0.1",
|
|
91
91
|
"ssh_key": "path/to/key",
|
|
@@ -103,13 +103,13 @@ class TestVagrantConnector(TestCase):
|
|
|
103
103
|
]
|
|
104
104
|
|
|
105
105
|
def test_make_names_data_with_limit(self):
|
|
106
|
-
data = VagrantInventoryConnector.make_names_data(
|
|
106
|
+
data = list(VagrantInventoryConnector.make_names_data(name=("ubuntu16",)))
|
|
107
107
|
|
|
108
108
|
assert data == [
|
|
109
109
|
(
|
|
110
110
|
"@vagrant/ubuntu16",
|
|
111
111
|
{
|
|
112
|
-
"ssh_port":
|
|
112
|
+
"ssh_port": 2222,
|
|
113
113
|
"ssh_user": "vagrant",
|
|
114
114
|
"ssh_hostname": "127.0.0.1",
|
|
115
115
|
"ssh_key": "path/to/key",
|
|
@@ -120,4 +120,4 @@ class TestVagrantConnector(TestCase):
|
|
|
120
120
|
|
|
121
121
|
def test_make_names_data_no_matches(self):
|
|
122
122
|
with self.assertRaises(InventoryError):
|
|
123
|
-
VagrantInventoryConnector.make_names_data(
|
|
123
|
+
list(VagrantInventoryConnector.make_names_data(name="nope"))
|