pyinfra 2.9.2__py2.py3-none-any.whl → 3.0b1__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 +261 -255
- pyinfra/api/arguments_typed.py +77 -0
- pyinfra/api/command.py +66 -53
- pyinfra/api/config.py +27 -22
- pyinfra/api/connect.py +1 -1
- pyinfra/api/connectors.py +2 -24
- pyinfra/api/deploy.py +21 -52
- pyinfra/api/exceptions.py +33 -8
- pyinfra/api/facts.py +77 -113
- pyinfra/api/host.py +150 -82
- pyinfra/api/inventory.py +17 -25
- pyinfra/api/operation.py +232 -198
- pyinfra/api/operations.py +102 -148
- pyinfra/api/state.py +137 -79
- pyinfra/api/util.py +55 -70
- pyinfra/connectors/base.py +150 -0
- pyinfra/connectors/chroot.py +160 -169
- pyinfra/connectors/docker.py +227 -237
- pyinfra/connectors/dockerssh.py +231 -253
- pyinfra/connectors/local.py +195 -207
- pyinfra/connectors/ssh.py +528 -615
- pyinfra/connectors/ssh_util.py +114 -0
- pyinfra/connectors/sshuserclient/client.py +5 -3
- pyinfra/connectors/terraform.py +86 -65
- pyinfra/connectors/util.py +212 -137
- pyinfra/connectors/vagrant.py +55 -48
- pyinfra/context.py +3 -2
- pyinfra/facts/docker.py +1 -0
- pyinfra/facts/files.py +45 -32
- pyinfra/facts/git.py +3 -1
- pyinfra/facts/gpg.py +1 -1
- pyinfra/facts/hardware.py +4 -2
- pyinfra/facts/iptables.py +5 -3
- pyinfra/facts/mysql.py +1 -0
- pyinfra/facts/postgres.py +168 -0
- pyinfra/facts/postgresql.py +5 -161
- pyinfra/facts/selinux.py +3 -1
- pyinfra/facts/server.py +77 -30
- pyinfra/facts/systemd.py +29 -12
- pyinfra/facts/sysvinit.py +10 -10
- pyinfra/facts/util/packaging.py +4 -2
- pyinfra/local.py +4 -5
- pyinfra/operations/apk.py +3 -3
- pyinfra/operations/apt.py +25 -47
- pyinfra/operations/brew.py +7 -14
- pyinfra/operations/bsdinit.py +4 -4
- pyinfra/operations/cargo.py +1 -1
- pyinfra/operations/choco.py +1 -1
- pyinfra/operations/dnf.py +4 -4
- pyinfra/operations/files.py +108 -321
- pyinfra/operations/gem.py +1 -1
- pyinfra/operations/git.py +6 -37
- pyinfra/operations/iptables.py +2 -10
- pyinfra/operations/launchd.py +1 -1
- pyinfra/operations/lxd.py +1 -9
- pyinfra/operations/mysql.py +5 -28
- pyinfra/operations/npm.py +1 -1
- pyinfra/operations/openrc.py +1 -1
- pyinfra/operations/pacman.py +3 -3
- pyinfra/operations/pip.py +14 -15
- pyinfra/operations/pkg.py +1 -1
- pyinfra/operations/pkgin.py +3 -3
- pyinfra/operations/postgres.py +347 -0
- pyinfra/operations/postgresql.py +17 -380
- pyinfra/operations/python.py +2 -17
- pyinfra/operations/selinux.py +5 -28
- pyinfra/operations/server.py +59 -84
- pyinfra/operations/snap.py +1 -3
- pyinfra/operations/ssh.py +8 -23
- pyinfra/operations/systemd.py +7 -7
- pyinfra/operations/sysvinit.py +3 -12
- pyinfra/operations/upstart.py +4 -4
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/files.py +2 -2
- pyinfra/operations/util/packaging.py +6 -24
- pyinfra/operations/util/service.py +18 -37
- pyinfra/operations/vzctl.py +2 -2
- pyinfra/operations/xbps.py +3 -3
- pyinfra/operations/yum.py +4 -4
- pyinfra/operations/zypper.py +4 -4
- {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/METADATA +19 -22
- pyinfra-3.0b1.dist-info/RECORD +163 -0
- pyinfra-3.0b1.dist-info/entry_points.txt +11 -0
- pyinfra_cli/__main__.py +2 -0
- pyinfra_cli/commands.py +7 -2
- pyinfra_cli/exceptions.py +83 -42
- pyinfra_cli/inventory.py +19 -4
- pyinfra_cli/log.py +17 -3
- pyinfra_cli/main.py +133 -90
- pyinfra_cli/prints.py +93 -129
- pyinfra_cli/util.py +60 -29
- tests/test_api/test_api.py +2 -0
- tests/test_api/test_api_arguments.py +13 -13
- tests/test_api/test_api_deploys.py +28 -29
- tests/test_api/test_api_facts.py +60 -98
- tests/test_api/test_api_operations.py +100 -200
- tests/test_cli/test_cli.py +18 -49
- tests/test_cli/test_cli_deploy.py +11 -37
- tests/test_cli/test_cli_exceptions.py +50 -19
- tests/test_cli/util.py +1 -1
- tests/test_connectors/test_chroot.py +6 -6
- tests/test_connectors/test_docker.py +4 -4
- tests/test_connectors/test_dockerssh.py +38 -50
- tests/test_connectors/test_local.py +11 -12
- tests/test_connectors/test_ssh.py +66 -107
- tests/test_connectors/test_terraform.py +9 -15
- tests/test_connectors/test_util.py +24 -46
- tests/test_connectors/test_vagrant.py +4 -4
- pyinfra/api/operation.pyi +0 -117
- pyinfra/connectors/ansible.py +0 -171
- pyinfra/connectors/mech.py +0 -186
- pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
- pyinfra/connectors/winrm.py +0 -320
- 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 -551
- pyinfra-2.9.2.dist-info/RECORD +0 -170
- pyinfra-2.9.2.dist-info/entry_points.txt +0 -14
- tests/test_connectors/test_ansible.py +0 -64
- tests/test_connectors/test_mech.py +0 -126
- tests/test_connectors/test_winrm.py +0 -76
- {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/LICENSE.md +0 -0
- {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/WHEEL +0 -0
- {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/top_level.txt +0 -0
|
@@ -3,90 +3,68 @@
|
|
|
3
3
|
from unittest import TestCase
|
|
4
4
|
|
|
5
5
|
from pyinfra.api import Config, State
|
|
6
|
-
from pyinfra.connectors.util import
|
|
7
|
-
make_unix_command,
|
|
8
|
-
make_unix_command_for_host,
|
|
9
|
-
split_combined_output,
|
|
10
|
-
)
|
|
6
|
+
from pyinfra.connectors.util import make_unix_command, make_unix_command_for_host
|
|
11
7
|
|
|
12
8
|
from ..util import make_inventory
|
|
13
9
|
|
|
14
10
|
|
|
15
|
-
class TestConnectorUtil(TestCase):
|
|
16
|
-
def test_split_combined_output_works(self):
|
|
17
|
-
results = split_combined_output(
|
|
18
|
-
[
|
|
19
|
-
("stdout", "stdout1"),
|
|
20
|
-
("stdout", "stdout2"),
|
|
21
|
-
("stderr", "stderr1"),
|
|
22
|
-
("stdout", "stdout3"),
|
|
23
|
-
],
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
assert results == (["stdout1", "stdout2", "stdout3"], ["stderr1"])
|
|
27
|
-
|
|
28
|
-
def test_split_combined_output_raises(self):
|
|
29
|
-
with self.assertRaises(ValueError):
|
|
30
|
-
split_combined_output(["nope", ""])
|
|
31
|
-
|
|
32
|
-
|
|
33
11
|
class TestMakeUnixCommandConnectorUtil(TestCase):
|
|
34
12
|
def test_command(self):
|
|
35
13
|
command = make_unix_command("echo Šablony")
|
|
36
14
|
assert command.get_raw_value() == "sh -c 'echo Šablony'"
|
|
37
15
|
|
|
38
16
|
def test_doas_command(self):
|
|
39
|
-
command = make_unix_command("uptime",
|
|
17
|
+
command = make_unix_command("uptime", _doas=True)
|
|
40
18
|
assert command.get_raw_value() == "doas -n sh -c uptime"
|
|
41
19
|
|
|
42
20
|
def test_doas_user_command(self):
|
|
43
|
-
command = make_unix_command("uptime",
|
|
21
|
+
command = make_unix_command("uptime", _doas=True, _doas_user="pyinfra")
|
|
44
22
|
assert command.get_raw_value() == "doas -n -u pyinfra sh -c uptime"
|
|
45
23
|
|
|
46
24
|
def test_sudo_command(self):
|
|
47
|
-
command = make_unix_command("uptime",
|
|
25
|
+
command = make_unix_command("uptime", _sudo=True)
|
|
48
26
|
assert command.get_raw_value() == "sudo -H -n sh -c uptime"
|
|
49
27
|
|
|
50
28
|
def test_sudo_multi_arg_command(self):
|
|
51
|
-
command = make_unix_command("echo hi",
|
|
29
|
+
command = make_unix_command("echo hi", _sudo=True, _preserve_sudo_env=True)
|
|
52
30
|
assert command.get_raw_value() == "sudo -H -n -E sh -c 'echo hi'"
|
|
53
31
|
|
|
54
32
|
def test_sudo_preserve_env_command(self):
|
|
55
|
-
command = make_unix_command("uptime",
|
|
33
|
+
command = make_unix_command("uptime", _sudo=True, _preserve_sudo_env=True)
|
|
56
34
|
assert command.get_raw_value() == "sudo -H -n -E sh -c uptime"
|
|
57
35
|
|
|
58
36
|
def test_use_sudo_login_command(self):
|
|
59
|
-
command = make_unix_command("uptime",
|
|
37
|
+
command = make_unix_command("uptime", _sudo=True, _use_sudo_login=True)
|
|
60
38
|
assert command.get_raw_value() == "sudo -H -n -i sh -c uptime"
|
|
61
39
|
|
|
62
40
|
def test_sudo_user_command(self):
|
|
63
|
-
command = make_unix_command("uptime",
|
|
41
|
+
command = make_unix_command("uptime", _sudo=True, _sudo_user="pyinfra")
|
|
64
42
|
assert command.get_raw_value() == "sudo -H -n -u pyinfra sh -c uptime"
|
|
65
43
|
|
|
66
44
|
def test_su_command(self):
|
|
67
|
-
command = make_unix_command("uptime",
|
|
45
|
+
command = make_unix_command("uptime", _su_user="pyinfra")
|
|
68
46
|
assert command.get_raw_value() == "su pyinfra -c 'sh -c uptime'"
|
|
69
47
|
|
|
70
48
|
def test_su_multi_arg_command(self):
|
|
71
|
-
command = make_unix_command("echo hi",
|
|
49
|
+
command = make_unix_command("echo hi", _su_user="pyinfra")
|
|
72
50
|
assert command.get_raw_value() == "su pyinfra -c 'sh -c '\"'\"'echo hi'\"'\"''"
|
|
73
51
|
|
|
74
52
|
def test_use_su_login_command(self):
|
|
75
|
-
command = make_unix_command("uptime",
|
|
53
|
+
command = make_unix_command("uptime", _su_user="pyinfra", _use_su_login=True)
|
|
76
54
|
assert command.get_raw_value() == "su -l pyinfra -c 'sh -c uptime'"
|
|
77
55
|
|
|
78
56
|
def test_preserve_su_env_command(self):
|
|
79
|
-
command = make_unix_command("uptime",
|
|
57
|
+
command = make_unix_command("uptime", _su_user="pyinfra", _preserve_su_env=True)
|
|
80
58
|
assert command.get_raw_value() == "su -m pyinfra -c 'sh -c uptime'"
|
|
81
59
|
|
|
82
60
|
def test_su_shell_command(self):
|
|
83
|
-
command = make_unix_command("uptime",
|
|
61
|
+
command = make_unix_command("uptime", _su_user="pyinfra", _su_shell="bash")
|
|
84
62
|
assert command.get_raw_value() == "su -s `which bash` pyinfra -c 'sh -c uptime'"
|
|
85
63
|
|
|
86
64
|
def test_command_env(self):
|
|
87
65
|
command = make_unix_command(
|
|
88
66
|
"uptime",
|
|
89
|
-
|
|
67
|
+
_env={
|
|
90
68
|
"key": "value",
|
|
91
69
|
"anotherkey": "anothervalue",
|
|
92
70
|
},
|
|
@@ -97,23 +75,23 @@ class TestMakeUnixCommandConnectorUtil(TestCase):
|
|
|
97
75
|
]
|
|
98
76
|
|
|
99
77
|
def test_command_chdir(self):
|
|
100
|
-
command = make_unix_command("uptime",
|
|
78
|
+
command = make_unix_command("uptime", _chdir="/opt/somedir")
|
|
101
79
|
assert command.get_raw_value() == "sh -c 'cd /opt/somedir && uptime'"
|
|
102
80
|
|
|
103
81
|
def test_custom_shell_command(self):
|
|
104
|
-
command = make_unix_command("uptime",
|
|
82
|
+
command = make_unix_command("uptime", _shell_executable="bash")
|
|
105
83
|
assert command.get_raw_value() == "bash -c uptime"
|
|
106
84
|
|
|
107
85
|
def test_mixed_command(self):
|
|
108
86
|
command = make_unix_command(
|
|
109
87
|
"echo hi",
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
88
|
+
_chdir="/opt/somedir",
|
|
89
|
+
_env={"key": "value"},
|
|
90
|
+
_sudo=True,
|
|
91
|
+
_sudo_user="root",
|
|
92
|
+
_preserve_sudo_env=True,
|
|
93
|
+
_su_user="pyinfra",
|
|
94
|
+
_shell_executable="bash",
|
|
117
95
|
)
|
|
118
96
|
assert command.get_raw_value() == (
|
|
119
97
|
"sudo -H -n -E -u root " # sudo bit
|
|
@@ -125,7 +103,7 @@ class TestMakeUnixCommandConnectorUtil(TestCase):
|
|
|
125
103
|
def test_command_exists_su_config_only(self):
|
|
126
104
|
"""
|
|
127
105
|
This tests covers a bug that appeared when `make_unix_command` is called
|
|
128
|
-
with `
|
|
106
|
+
with `_su_user=False` (default) but `SU_USER` set on the config object,
|
|
129
107
|
resulting in an empty command output.
|
|
130
108
|
"""
|
|
131
109
|
state = State(make_inventory(), Config(SU_USER=True))
|
|
@@ -3,7 +3,7 @@ from unittest import TestCase
|
|
|
3
3
|
from unittest.mock import mock_open, patch
|
|
4
4
|
|
|
5
5
|
from pyinfra.api.exceptions import InventoryError
|
|
6
|
-
from pyinfra.connectors.vagrant import
|
|
6
|
+
from pyinfra.connectors.vagrant import VagrantInventoryConnector, get_vagrant_options
|
|
7
7
|
|
|
8
8
|
FAKE_VAGRANT_OPTIONS = {
|
|
9
9
|
"groups": {
|
|
@@ -69,7 +69,7 @@ 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 = make_names_data()
|
|
72
|
+
data = list(VagrantInventoryConnector.make_names_data())
|
|
73
73
|
|
|
74
74
|
assert data == [
|
|
75
75
|
(
|
|
@@ -103,7 +103,7 @@ class TestVagrantConnector(TestCase):
|
|
|
103
103
|
]
|
|
104
104
|
|
|
105
105
|
def test_make_names_data_with_limit(self):
|
|
106
|
-
data = make_names_data(
|
|
106
|
+
data = list(VagrantInventoryConnector.make_names_data(name=("ubuntu16",)))
|
|
107
107
|
|
|
108
108
|
assert data == [
|
|
109
109
|
(
|
|
@@ -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
|
-
make_names_data(
|
|
123
|
+
list(VagrantInventoryConnector.make_names_data(name="nope"))
|
pyinfra/api/operation.pyi
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
from typing import (
|
|
2
|
-
Callable,
|
|
3
|
-
Dict,
|
|
4
|
-
Generator,
|
|
5
|
-
Generic,
|
|
6
|
-
Iterable,
|
|
7
|
-
List,
|
|
8
|
-
Mapping,
|
|
9
|
-
Protocol,
|
|
10
|
-
Tuple,
|
|
11
|
-
overload,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
from typing_extensions import ParamSpec
|
|
15
|
-
|
|
16
|
-
from pyinfra.api.command import (
|
|
17
|
-
FileDownloadCommand,
|
|
18
|
-
FileUploadCommand,
|
|
19
|
-
FunctionCommand,
|
|
20
|
-
StringCommand,
|
|
21
|
-
)
|
|
22
|
-
from pyinfra.api.host import Host
|
|
23
|
-
from pyinfra.api.state import State
|
|
24
|
-
|
|
25
|
-
P = ParamSpec("P")
|
|
26
|
-
|
|
27
|
-
Command = str | StringCommand | FileDownloadCommand | FileUploadCommand | FunctionCommand
|
|
28
|
-
|
|
29
|
-
class OperationMeta:
|
|
30
|
-
changed: bool
|
|
31
|
-
commands: List[str] | None
|
|
32
|
-
hash: str | None
|
|
33
|
-
|
|
34
|
-
stdout_lines: List[str]
|
|
35
|
-
stdout: str
|
|
36
|
-
stderr_lines: List[str]
|
|
37
|
-
stderr: str
|
|
38
|
-
|
|
39
|
-
class Operation(Generic[P], Protocol):
|
|
40
|
-
def __call__(
|
|
41
|
-
self,
|
|
42
|
-
_sudo: bool | None = None,
|
|
43
|
-
_sudo_user: str | None = None,
|
|
44
|
-
_use_sudo_login: bool | None = None,
|
|
45
|
-
_use_sudo_password: bool | None = None,
|
|
46
|
-
_preserve_sudo_env: bool | None = None,
|
|
47
|
-
_su_user: str | None = None,
|
|
48
|
-
_use_su_login: bool | None = None,
|
|
49
|
-
_preserve_su_env: bool | None = None,
|
|
50
|
-
_su_shell: str | None = None,
|
|
51
|
-
_doas: bool | None = None,
|
|
52
|
-
_doas_user: str | None = None,
|
|
53
|
-
_shell_executable: str | None = None,
|
|
54
|
-
_chdir: str | None = None,
|
|
55
|
-
_env: Mapping[str, str] | None = None,
|
|
56
|
-
_success_exit_codes: Iterable[int] | None = None,
|
|
57
|
-
_timeout: int | None = None,
|
|
58
|
-
_get_pty: bool | None = None,
|
|
59
|
-
_stdin: str | List[str] | Tuple[str, ...] | None = None,
|
|
60
|
-
name: str | None = None,
|
|
61
|
-
_ignore_errors: bool | None = None,
|
|
62
|
-
_continue_on_error: bool | None = None,
|
|
63
|
-
_precondition: str | None = None,
|
|
64
|
-
_postcondition: str | None = None,
|
|
65
|
-
_on_success: Callable[[State, Host, str], None] | None = None,
|
|
66
|
-
_on_error: Callable[[State, Host, str], None] | None = None,
|
|
67
|
-
_parallel: int | None = None,
|
|
68
|
-
_run_once: bool | None = None,
|
|
69
|
-
_serial: bool | None = None,
|
|
70
|
-
*args: P.args,
|
|
71
|
-
**kwargs: P.kwargs,
|
|
72
|
-
) -> OperationMeta: ...
|
|
73
|
-
|
|
74
|
-
def add_op(
|
|
75
|
-
state: State,
|
|
76
|
-
op_func: Operation[P],
|
|
77
|
-
_sudo: bool | None = None,
|
|
78
|
-
_sudo_user: str | None = None,
|
|
79
|
-
_use_sudo_login: bool | None = None,
|
|
80
|
-
_use_sudo_password: bool | None = None,
|
|
81
|
-
_preserve_sudo_env: bool | None = None,
|
|
82
|
-
_su_user: str | None = None,
|
|
83
|
-
_use_su_login: bool | None = None,
|
|
84
|
-
_preserve_su_env: bool | None = None,
|
|
85
|
-
_su_shell: str | None = None,
|
|
86
|
-
_doas: bool | None = None,
|
|
87
|
-
_doas_user: str | None = None,
|
|
88
|
-
_shell_executable: str | None = None,
|
|
89
|
-
_chdir: str | None = None,
|
|
90
|
-
_env: Mapping[str, str] | None = None,
|
|
91
|
-
_success_exit_codes: Iterable[int] | None = None,
|
|
92
|
-
_timeout: int | None = None,
|
|
93
|
-
_get_pty: bool | None = None,
|
|
94
|
-
_stdin: str | List[str] | Tuple[str, ...] | None = None,
|
|
95
|
-
name: str | None = None,
|
|
96
|
-
_ignore_errors: bool | None = None,
|
|
97
|
-
_continue_on_error: bool | None = None,
|
|
98
|
-
_precondition: str | None = None,
|
|
99
|
-
_postcondition: str | None = None,
|
|
100
|
-
_on_success: Callable[[State, Host, str], None] | None = None,
|
|
101
|
-
_on_error: Callable[[State, Host, str], None] | None = None,
|
|
102
|
-
_parallel: int | None = None,
|
|
103
|
-
_run_once: bool | None = None,
|
|
104
|
-
_serial: bool | None = None,
|
|
105
|
-
host: Iterable[Host] | Host | None = None,
|
|
106
|
-
*args: P.args,
|
|
107
|
-
**kwargs: P.kwargs,
|
|
108
|
-
) -> Dict[Host, OperationMeta]: ...
|
|
109
|
-
@overload
|
|
110
|
-
def operation(func: Callable[P, Generator[Command, None, None]]) -> Operation[P]: ...
|
|
111
|
-
@overload
|
|
112
|
-
def operation(
|
|
113
|
-
pipeline_facts=None,
|
|
114
|
-
is_idempotent: bool = True,
|
|
115
|
-
idempotent_notice=None,
|
|
116
|
-
frame_offset=1,
|
|
117
|
-
) -> Callable[[Callable[P, Generator[Command, None, None]]], Operation[P]]: ...
|
pyinfra/connectors/ansible.py
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
**Note**: this connector is a work in progress! While it parses the list of
|
|
3
|
-
hosts OK, it doesn't handle nested groups properly yet.
|
|
4
|
-
|
|
5
|
-
The `@ansible` connector can be used to parse Ansible inventory files.
|
|
6
|
-
|
|
7
|
-
.. code:: python
|
|
8
|
-
|
|
9
|
-
# Load an Ansible inventory relative to the current directory
|
|
10
|
-
pyinfra @ansible/path/to/inventory
|
|
11
|
-
|
|
12
|
-
# Load using an absolute path
|
|
13
|
-
pyinfra @ansible//absolute/path/to/inventory
|
|
14
|
-
"""
|
|
15
|
-
import json
|
|
16
|
-
import re
|
|
17
|
-
from collections import defaultdict
|
|
18
|
-
from configparser import ConfigParser
|
|
19
|
-
from os import path
|
|
20
|
-
from typing import TYPE_CHECKING, Optional
|
|
21
|
-
|
|
22
|
-
from pyinfra import logger
|
|
23
|
-
from pyinfra.api.exceptions import InventoryError
|
|
24
|
-
from pyinfra.api.util import memoize
|
|
25
|
-
|
|
26
|
-
if TYPE_CHECKING:
|
|
27
|
-
from pyinfra.api.host import Host
|
|
28
|
-
|
|
29
|
-
try:
|
|
30
|
-
import yaml
|
|
31
|
-
except ImportError:
|
|
32
|
-
yaml = None # type: ignore
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@memoize
|
|
36
|
-
def show_warning():
|
|
37
|
-
logger.warning("The @ansible connector is in alpha!")
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def make_names_data(inventory_filename: Optional[str] = None):
|
|
41
|
-
show_warning()
|
|
42
|
-
|
|
43
|
-
if not inventory_filename:
|
|
44
|
-
raise InventoryError("No Ansible inventory filename provided!")
|
|
45
|
-
|
|
46
|
-
if not path.exists(inventory_filename):
|
|
47
|
-
raise InventoryError(
|
|
48
|
-
("Could not find Ansible inventory file: {0}").format(inventory_filename),
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
return parse_inventory(inventory_filename)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def parse_inventory(inventory_filename: str):
|
|
55
|
-
# fallback to INI if no extension
|
|
56
|
-
extension = inventory_filename.split(".")[-1] if "." in inventory_filename else "ini"
|
|
57
|
-
|
|
58
|
-
# host:set(groups) mapping
|
|
59
|
-
host_to_groups = {}
|
|
60
|
-
|
|
61
|
-
if extension in ["ini"]:
|
|
62
|
-
host_to_groups = parse_inventory_ini(inventory_filename)
|
|
63
|
-
elif extension in ["json"]:
|
|
64
|
-
with open(inventory_filename, encoding="utf-8") as inventory_file:
|
|
65
|
-
inventory_tree = json.load(inventory_file)
|
|
66
|
-
# close file early
|
|
67
|
-
host_to_groups = parse_inventory_tree(inventory_tree)
|
|
68
|
-
elif extension in ["yaml", "yml"]:
|
|
69
|
-
if yaml is None:
|
|
70
|
-
raise Exception(
|
|
71
|
-
(
|
|
72
|
-
"To parse YAML Ansible inventories requires `pyyaml`. "
|
|
73
|
-
"Install it with `pip install pyyaml`."
|
|
74
|
-
),
|
|
75
|
-
)
|
|
76
|
-
with open(inventory_filename, encoding="utf-8") as inventory_file:
|
|
77
|
-
inventory_tree = yaml.safe_load(inventory_file)
|
|
78
|
-
# close file early
|
|
79
|
-
host_to_groups = parse_inventory_tree(inventory_tree)
|
|
80
|
-
else:
|
|
81
|
-
raise InventoryError(("Ansible inventory file format not supported: {0}").format(extension))
|
|
82
|
-
|
|
83
|
-
return [(host, {}, sorted(list(host_to_groups.get(host, [])))) for host in host_to_groups]
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def parse_inventory_ini(inventory_filename: str):
|
|
87
|
-
config = ConfigParser(
|
|
88
|
-
delimiters=(" "), # we only handle the hostnames for now
|
|
89
|
-
allow_no_value=True, # we don't by default have = values
|
|
90
|
-
interpolation=None, # remove any Python interpolation
|
|
91
|
-
)
|
|
92
|
-
config.read(inventory_filename)
|
|
93
|
-
|
|
94
|
-
host_to_groups = defaultdict(set)
|
|
95
|
-
group_to_hosts = defaultdict(set)
|
|
96
|
-
hosts = []
|
|
97
|
-
|
|
98
|
-
# First pass - load hosts/groups of hosts
|
|
99
|
-
for section in config.sections():
|
|
100
|
-
if ":" in section: # ignore :children and :vars sections this time
|
|
101
|
-
continue
|
|
102
|
-
|
|
103
|
-
options = config.options(section)
|
|
104
|
-
for host in _parse_ansible_hosts(options):
|
|
105
|
-
hosts.append(host)
|
|
106
|
-
host_to_groups[host].add(section)
|
|
107
|
-
group_to_hosts[section].add(host)
|
|
108
|
-
|
|
109
|
-
# Second pass - load any children groups
|
|
110
|
-
for section in config.sections():
|
|
111
|
-
if not section.endswith(":children"): # we only support :children for now
|
|
112
|
-
continue
|
|
113
|
-
|
|
114
|
-
group_name = section.replace(":children", "")
|
|
115
|
-
|
|
116
|
-
options = config.options(section)
|
|
117
|
-
for sub_group_name in options:
|
|
118
|
-
sub_group_hosts = group_to_hosts[sub_group_name]
|
|
119
|
-
for host in sub_group_hosts:
|
|
120
|
-
host_to_groups[host].add(group_name)
|
|
121
|
-
|
|
122
|
-
return host_to_groups
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def _parse_ansible_hosts(hosts):
|
|
126
|
-
for host in hosts:
|
|
127
|
-
expand_match = re.search(r"\[[0-9:]+\]", host)
|
|
128
|
-
if expand_match:
|
|
129
|
-
expand_string = host[expand_match.start() : expand_match.end()]
|
|
130
|
-
bits = expand_string[1:-1].split(":") # remove the [] either side
|
|
131
|
-
|
|
132
|
-
zfill = 0
|
|
133
|
-
if bits[0].startswith("0"):
|
|
134
|
-
zfill = len(bits[0])
|
|
135
|
-
|
|
136
|
-
start, end = int(bits[0]), int(bits[1])
|
|
137
|
-
step = int(bits[2]) if len(bits) > 2 else 1
|
|
138
|
-
|
|
139
|
-
for n in range(start, end + 1, step):
|
|
140
|
-
number_as_string = "{0}".format(n)
|
|
141
|
-
if zfill:
|
|
142
|
-
number_as_string = number_as_string.zfill(zfill)
|
|
143
|
-
|
|
144
|
-
hostname = host.replace(expand_string, number_as_string)
|
|
145
|
-
yield hostname
|
|
146
|
-
else:
|
|
147
|
-
yield host
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def parse_inventory_tree(inventory_tree, host_to_groups=dict(), group_stack=set()):
|
|
151
|
-
for group in inventory_tree:
|
|
152
|
-
# set logic adds tolerance for duplicate group names
|
|
153
|
-
groups = group_stack.union({group})
|
|
154
|
-
|
|
155
|
-
if "hosts" in inventory_tree[group]:
|
|
156
|
-
for host in inventory_tree[group]["hosts"]:
|
|
157
|
-
append_groups_to_host(host, groups, host_to_groups)
|
|
158
|
-
|
|
159
|
-
if "children" in inventory_tree[group]:
|
|
160
|
-
# recursively parse inventory tree
|
|
161
|
-
parse_inventory_tree(inventory_tree[group]["children"], host_to_groups, groups)
|
|
162
|
-
|
|
163
|
-
return host_to_groups
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def append_groups_to_host(host: "Host", groups, host_to_groups):
|
|
167
|
-
if host in host_to_groups:
|
|
168
|
-
# set logic handles de-duplication
|
|
169
|
-
host_to_groups[host] = host_to_groups[host].union(groups)
|
|
170
|
-
else:
|
|
171
|
-
host_to_groups[host] = groups
|
pyinfra/connectors/mech.py
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
The ``@mech`` connector reads the current mech status and generates an inventory
|
|
3
|
-
for any running VMs.
|
|
4
|
-
|
|
5
|
-
.. code:: python
|
|
6
|
-
|
|
7
|
-
# Run on all hosts
|
|
8
|
-
pyinfra @mech ...
|
|
9
|
-
|
|
10
|
-
# Run on a specific VM
|
|
11
|
-
pyinfra @mech/my-vm-name ...
|
|
12
|
-
|
|
13
|
-
# Run on multiple named VMs
|
|
14
|
-
pyinfra @mech/my-vm-name,@mech/another-vm-name ...
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
import json
|
|
18
|
-
from os import path
|
|
19
|
-
from queue import Queue
|
|
20
|
-
from threading import Thread
|
|
21
|
-
|
|
22
|
-
from pyinfra import local, logger
|
|
23
|
-
from pyinfra.api.exceptions import InventoryError
|
|
24
|
-
from pyinfra.api.util import memoize
|
|
25
|
-
from pyinfra.progress import progress_spinner
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _get_mech_ssh_config(queue, progress, target):
|
|
29
|
-
logger.debug("Loading SSH config for %s", target)
|
|
30
|
-
|
|
31
|
-
# Note: We have to work-around the fact that "mech ssh-config somehost"
|
|
32
|
-
# does not return the correct "Host" value. When "mech" fixes this
|
|
33
|
-
# issue we can simply this code.
|
|
34
|
-
lines = local.shell(
|
|
35
|
-
"mech ssh-config {0}".format(target),
|
|
36
|
-
splitlines=True,
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
newlines = []
|
|
40
|
-
for line in lines:
|
|
41
|
-
if line.startswith("Host "):
|
|
42
|
-
newlines.append("Host " + target)
|
|
43
|
-
else:
|
|
44
|
-
newlines.append(line)
|
|
45
|
-
|
|
46
|
-
queue.put(newlines)
|
|
47
|
-
|
|
48
|
-
progress(target)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@memoize
|
|
52
|
-
def get_mech_config(limit=None):
|
|
53
|
-
logger.info("Getting Mech config...")
|
|
54
|
-
|
|
55
|
-
if limit and not isinstance(limit, (list, tuple)):
|
|
56
|
-
limit = [limit]
|
|
57
|
-
|
|
58
|
-
# Note: There is no "--machine-readable" option to 'mech status'
|
|
59
|
-
with progress_spinner({"mech ls"}) as progress:
|
|
60
|
-
output = local.shell(
|
|
61
|
-
"mech ls",
|
|
62
|
-
splitlines=True,
|
|
63
|
-
)
|
|
64
|
-
progress("mech ls")
|
|
65
|
-
|
|
66
|
-
targets = []
|
|
67
|
-
|
|
68
|
-
for line in output:
|
|
69
|
-
|
|
70
|
-
address = ""
|
|
71
|
-
|
|
72
|
-
data = line.split()
|
|
73
|
-
target = data[0]
|
|
74
|
-
|
|
75
|
-
if len(data) == 5:
|
|
76
|
-
address = data[1]
|
|
77
|
-
|
|
78
|
-
# Skip anything not in the limit
|
|
79
|
-
if limit is not None and target not in limit:
|
|
80
|
-
continue
|
|
81
|
-
|
|
82
|
-
# For each vm that has an address, fetch it's SSH config in a thread
|
|
83
|
-
if address != "" and address[0].isdigit():
|
|
84
|
-
targets.append(target)
|
|
85
|
-
|
|
86
|
-
threads = []
|
|
87
|
-
config_queue = Queue()
|
|
88
|
-
|
|
89
|
-
with progress_spinner(targets) as progress:
|
|
90
|
-
for target in targets:
|
|
91
|
-
thread = Thread(
|
|
92
|
-
target=_get_mech_ssh_config,
|
|
93
|
-
args=(config_queue, progress, target),
|
|
94
|
-
)
|
|
95
|
-
threads.append(thread)
|
|
96
|
-
thread.start()
|
|
97
|
-
|
|
98
|
-
for thread in threads:
|
|
99
|
-
thread.join()
|
|
100
|
-
|
|
101
|
-
queue_items = list(config_queue.queue)
|
|
102
|
-
|
|
103
|
-
lines = []
|
|
104
|
-
for output in queue_items:
|
|
105
|
-
lines.extend(output)
|
|
106
|
-
|
|
107
|
-
return lines
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
@memoize
|
|
111
|
-
def get_mech_options():
|
|
112
|
-
if path.exists("@mech.json"):
|
|
113
|
-
with open("@mech.json", "r", encoding="utf-8") as f:
|
|
114
|
-
return json.loads(f.read())
|
|
115
|
-
return {}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def _make_name_data(host):
|
|
119
|
-
mech_options = get_mech_options()
|
|
120
|
-
mech_host = host["Host"]
|
|
121
|
-
|
|
122
|
-
data = {
|
|
123
|
-
"ssh_hostname": host["HostName"],
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
for config_key, data_key in (
|
|
127
|
-
("Port", "ssh_port"),
|
|
128
|
-
("User", "ssh_user"),
|
|
129
|
-
("IdentityFile", "ssh_key"),
|
|
130
|
-
):
|
|
131
|
-
if config_key in host:
|
|
132
|
-
data[data_key] = host[config_key]
|
|
133
|
-
|
|
134
|
-
# Update any configured JSON data
|
|
135
|
-
if mech_host in mech_options.get("data", {}):
|
|
136
|
-
data.update(mech_options["data"][mech_host])
|
|
137
|
-
|
|
138
|
-
# Work out groups
|
|
139
|
-
groups = mech_options.get("groups", {}).get(mech_host, [])
|
|
140
|
-
|
|
141
|
-
if "@mech" not in groups:
|
|
142
|
-
groups.append("@mech")
|
|
143
|
-
|
|
144
|
-
return "@mech/{0}".format(host["Host"]), data, groups
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def make_names_data(limit=None):
|
|
148
|
-
mech_ssh_info = get_mech_config(limit)
|
|
149
|
-
|
|
150
|
-
logger.debug("Got Mech SSH info: \n%s", mech_ssh_info)
|
|
151
|
-
|
|
152
|
-
hosts = []
|
|
153
|
-
current_host = None
|
|
154
|
-
|
|
155
|
-
for line in mech_ssh_info:
|
|
156
|
-
if not line:
|
|
157
|
-
if current_host:
|
|
158
|
-
hosts.append(_make_name_data(current_host))
|
|
159
|
-
|
|
160
|
-
current_host = None
|
|
161
|
-
continue
|
|
162
|
-
|
|
163
|
-
key, value = line.strip().split(" ", 1)
|
|
164
|
-
|
|
165
|
-
if key == "Host":
|
|
166
|
-
if current_host:
|
|
167
|
-
hosts.append(_make_name_data(current_host))
|
|
168
|
-
|
|
169
|
-
# Set the new host
|
|
170
|
-
current_host = {
|
|
171
|
-
key: value,
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
elif current_host:
|
|
175
|
-
current_host[key] = value
|
|
176
|
-
|
|
177
|
-
else:
|
|
178
|
-
logger.debug("Extra Mech SSH key/value (%s=%s)", key, value)
|
|
179
|
-
|
|
180
|
-
if current_host:
|
|
181
|
-
hosts.append(_make_name_data(current_host))
|
|
182
|
-
|
|
183
|
-
if not hosts:
|
|
184
|
-
raise InventoryError("No running Mech instances found!")
|
|
185
|
-
|
|
186
|
-
return hosts
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
|
|
3
|
-
import winrm
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class PyinfraWinrmSession(winrm.Session):
|
|
7
|
-
"""This is our subclassed Session that allows for env setting"""
|
|
8
|
-
|
|
9
|
-
def run_cmd(self, command, args=(), env=None):
|
|
10
|
-
shell_id = self.protocol.open_shell(env_vars=env)
|
|
11
|
-
command_id = self.protocol.run_command(shell_id, command, args)
|
|
12
|
-
rs = winrm.Response(self.protocol.get_command_output(shell_id, command_id))
|
|
13
|
-
self.protocol.cleanup_command(shell_id, command_id)
|
|
14
|
-
self.protocol.close_shell(shell_id)
|
|
15
|
-
return rs
|
|
16
|
-
|
|
17
|
-
def run_ps(self, script, env=None):
|
|
18
|
-
"""base64 encodes a Powershell script and executes the powershell
|
|
19
|
-
encoded script command
|
|
20
|
-
"""
|
|
21
|
-
# must use utf16 little endian on windows
|
|
22
|
-
encoded_ps = base64.b64encode(script.encode("utf_16_le")).decode("ascii")
|
|
23
|
-
rs = self.run_cmd("powershell -encodedcommand {0}".format(encoded_ps), env=env)
|
|
24
|
-
if len(rs.std_err):
|
|
25
|
-
# if there was an error message, clean it it up and make it human
|
|
26
|
-
# readable
|
|
27
|
-
rs.std_err = self._clean_error_msg(rs.std_err)
|
|
28
|
-
return rs
|