pyinfra 3.0.1__py2.py3-none-any.whl → 3.1__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/host.py +5 -2
- pyinfra/api/operation.py +12 -10
- pyinfra/api/util.py +4 -2
- pyinfra/connectors/ssh.py +1 -1
- pyinfra/connectors/terraform.py +30 -20
- pyinfra/facts/apt.py +1 -1
- pyinfra/facts/files.py +8 -1
- pyinfra/facts/flatpak.py +70 -0
- pyinfra/facts/systemd.py +4 -1
- pyinfra/facts/zfs.py +57 -0
- pyinfra/operations/docker.py +4 -4
- pyinfra/operations/files.py +9 -2
- pyinfra/operations/flatpak.py +79 -0
- pyinfra/operations/mysql.py +5 -5
- pyinfra/operations/postgres.py +3 -3
- pyinfra/operations/server.py +9 -9
- pyinfra/operations/zfs.py +175 -0
- {pyinfra-3.0.1.dist-info → pyinfra-3.1.dist-info}/METADATA +1 -1
- {pyinfra-3.0.1.dist-info → pyinfra-3.1.dist-info}/RECORD +29 -25
- pyinfra_cli/inventory.py +34 -2
- pyinfra_cli/main.py +8 -8
- pyinfra_cli/prints.py +17 -1
- tests/test_api/test_api_inventory.py +21 -0
- tests/test_cli/test_cli_deploy.py +7 -2
- tests/test_connectors/test_terraform.py +6 -6
- {pyinfra-3.0.1.dist-info → pyinfra-3.1.dist-info}/LICENSE.md +0 -0
- {pyinfra-3.0.1.dist-info → pyinfra-3.1.dist-info}/WHEEL +0 -0
- {pyinfra-3.0.1.dist-info → pyinfra-3.1.dist-info}/entry_points.txt +0 -0
- {pyinfra-3.0.1.dist-info → pyinfra-3.1.dist-info}/top_level.txt +0 -0
pyinfra/api/host.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from contextlib import contextmanager
|
|
4
|
+
from copy import copy
|
|
4
5
|
from typing import (
|
|
5
6
|
TYPE_CHECKING,
|
|
6
7
|
Any,
|
|
@@ -41,7 +42,6 @@ def extract_callable_datas(
|
|
|
41
42
|
# the data is stored on the state temporarily.
|
|
42
43
|
if callable(data):
|
|
43
44
|
data = data()
|
|
44
|
-
|
|
45
45
|
yield data
|
|
46
46
|
|
|
47
47
|
|
|
@@ -66,7 +66,10 @@ class HostData:
|
|
|
66
66
|
def __getattr__(self, key: str):
|
|
67
67
|
for data in extract_callable_datas(self.datas):
|
|
68
68
|
try:
|
|
69
|
-
|
|
69
|
+
# Take a shallow copy of the object here, we don't want modifications
|
|
70
|
+
# to host.data.<X> to stick, instead setting host.data.<Y> = is the
|
|
71
|
+
# correct way to achieve this (see __setattr__).
|
|
72
|
+
return copy(data[key])
|
|
70
73
|
except KeyError:
|
|
71
74
|
pass
|
|
72
75
|
|
pyinfra/api/operation.py
CHANGED
|
@@ -60,7 +60,7 @@ class OperationMeta:
|
|
|
60
60
|
if self._commands is not None:
|
|
61
61
|
return (
|
|
62
62
|
"OperationMeta(executed=True, "
|
|
63
|
-
f"success={self.did_succeed}, hash={self._hash}, commands={len(self._commands)})"
|
|
63
|
+
f"success={self.did_succeed()}, hash={self._hash}, commands={len(self._commands)})"
|
|
64
64
|
)
|
|
65
65
|
return (
|
|
66
66
|
"OperationMeta(executed=False, "
|
|
@@ -87,6 +87,12 @@ class OperationMeta:
|
|
|
87
87
|
if not self.is_complete():
|
|
88
88
|
raise RuntimeError("Cannot evaluate operation result before execution")
|
|
89
89
|
|
|
90
|
+
@property
|
|
91
|
+
def executed(self) -> bool:
|
|
92
|
+
if self._commands is None:
|
|
93
|
+
return False
|
|
94
|
+
return len(self._commands) > 0
|
|
95
|
+
|
|
90
96
|
@property
|
|
91
97
|
def will_change(self) -> bool:
|
|
92
98
|
if self._maybe_is_change is not None:
|
|
@@ -100,16 +106,12 @@ class OperationMeta:
|
|
|
100
106
|
self._maybe_is_change = False
|
|
101
107
|
return False
|
|
102
108
|
|
|
103
|
-
def
|
|
109
|
+
def did_change(self) -> bool:
|
|
110
|
+
self._raise_if_not_complete()
|
|
104
111
|
return bool(self._success and len(self._commands or []) > 0)
|
|
105
112
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return context.host.when(self._did_change)
|
|
109
|
-
|
|
110
|
-
@property
|
|
111
|
-
def did_not_change(self):
|
|
112
|
-
return context.host.when(lambda: not self._did_change())
|
|
113
|
+
def did_not_change(self) -> bool:
|
|
114
|
+
return not self.did_change()
|
|
113
115
|
|
|
114
116
|
def did_succeed(self, _raise_if_not_complete=True) -> bool:
|
|
115
117
|
if _raise_if_not_complete:
|
|
@@ -124,7 +126,7 @@ class OperationMeta:
|
|
|
124
126
|
@property
|
|
125
127
|
def changed(self) -> bool:
|
|
126
128
|
if self.is_complete():
|
|
127
|
-
return self.
|
|
129
|
+
return self.did_change()
|
|
128
130
|
return self.will_change
|
|
129
131
|
|
|
130
132
|
@property
|
pyinfra/api/util.py
CHANGED
|
@@ -139,12 +139,13 @@ def get_operation_order_from_stack(state: "State"):
|
|
|
139
139
|
return line_numbers
|
|
140
140
|
|
|
141
141
|
|
|
142
|
-
def get_template(filename_or_io: str | IO):
|
|
142
|
+
def get_template(filename_or_io: str | IO, jinja_env_kwargs: dict[str, Any] | None = None):
|
|
143
143
|
"""
|
|
144
144
|
Gets a jinja2 ``Template`` object for the input filename or string, with caching
|
|
145
145
|
based on the filename of the template, or the SHA1 of the input string.
|
|
146
146
|
"""
|
|
147
|
-
|
|
147
|
+
if jinja_env_kwargs is None:
|
|
148
|
+
jinja_env_kwargs = {}
|
|
148
149
|
file_data = get_file_io(filename_or_io, mode="r")
|
|
149
150
|
cache_key = file_data.cache_key
|
|
150
151
|
|
|
@@ -158,6 +159,7 @@ def get_template(filename_or_io: str | IO):
|
|
|
158
159
|
undefined=StrictUndefined,
|
|
159
160
|
keep_trailing_newline=True,
|
|
160
161
|
loader=FileSystemLoader(getcwd()),
|
|
162
|
+
**jinja_env_kwargs,
|
|
161
163
|
).from_string(template_string)
|
|
162
164
|
|
|
163
165
|
if cache_key:
|
pyinfra/connectors/ssh.py
CHANGED
pyinfra/connectors/terraform.py
CHANGED
|
@@ -10,16 +10,15 @@ from .base import BaseConnector
|
|
|
10
10
|
|
|
11
11
|
@memoize
|
|
12
12
|
def show_warning():
|
|
13
|
-
logger.warning("The @terraform connector is in
|
|
13
|
+
logger.warning("The @terraform connector is in beta!")
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def _flatten_dict_gen(d, parent_key, sep):
|
|
17
17
|
for k, v in d.items():
|
|
18
18
|
new_key = parent_key + sep + k if parent_key else k
|
|
19
|
+
yield new_key, v
|
|
19
20
|
if isinstance(v, dict):
|
|
20
21
|
yield from _flatten_dict(v, new_key, sep=sep).items()
|
|
21
|
-
else:
|
|
22
|
-
yield new_key, v
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
def _flatten_dict(d: dict, parent_key: str = "", sep: str = "."):
|
|
@@ -82,7 +81,9 @@ class TerraformInventoryConnector(BaseConnector):
|
|
|
82
81
|
show_warning()
|
|
83
82
|
|
|
84
83
|
if not name:
|
|
85
|
-
|
|
84
|
+
# This is the default which allows one to create a Terraform output
|
|
85
|
+
# "pyinfra" and directly call: pyinfra @terraform ...
|
|
86
|
+
name = "pyinfra_inventory.value"
|
|
86
87
|
|
|
87
88
|
with progress_spinner({"fetch terraform output"}):
|
|
88
89
|
tf_output_raw = local.shell("terraform output -json")
|
|
@@ -96,27 +97,36 @@ class TerraformInventoryConnector(BaseConnector):
|
|
|
96
97
|
keys = "\n".join(f" - {k}" for k in tf_output.keys())
|
|
97
98
|
raise InventoryError(f"No Terraform output with key: `{name}`, valid keys:\n{keys}")
|
|
98
99
|
|
|
99
|
-
if not isinstance(tf_output_value, list):
|
|
100
|
+
if not isinstance(tf_output_value, (list, dict)):
|
|
100
101
|
raise InventoryError(
|
|
101
102
|
"Invalid Terraform output type, should be `list`, got "
|
|
102
103
|
f"`{type(tf_output_value).__name__}`",
|
|
103
104
|
)
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
raise InventoryError(
|
|
110
|
-
"Invalid Terraform list item, missing `name` or `ssh_hostname` keys",
|
|
111
|
-
)
|
|
112
|
-
yield f"@terraform/{name}", ssh_target, ["@terraform"]
|
|
113
|
-
|
|
114
|
-
elif isinstance(ssh_target, str):
|
|
115
|
-
data = {"ssh_hostname": ssh_target}
|
|
116
|
-
yield f"@terraform/{ssh_target}", data, ["@terraform"]
|
|
106
|
+
if isinstance(tf_output_value, list):
|
|
107
|
+
tf_output_value = {
|
|
108
|
+
"all": tf_output_value,
|
|
109
|
+
}
|
|
117
110
|
|
|
118
|
-
|
|
111
|
+
for group_name, hosts in tf_output_value.items():
|
|
112
|
+
if not isinstance(hosts, list):
|
|
119
113
|
raise InventoryError(
|
|
120
|
-
"Invalid Terraform
|
|
121
|
-
f"`{type(
|
|
114
|
+
"Invalid Terraform map value type, all values should be `list`, got "
|
|
115
|
+
f"`{type(hosts).__name__}`",
|
|
122
116
|
)
|
|
117
|
+
for host in hosts:
|
|
118
|
+
if isinstance(host, dict):
|
|
119
|
+
name = host.pop("name", host.get("ssh_hostname"))
|
|
120
|
+
if name is None:
|
|
121
|
+
raise InventoryError(
|
|
122
|
+
"Invalid Terraform list item, missing `name` or `ssh_hostname` keys",
|
|
123
|
+
)
|
|
124
|
+
yield f"@terraform/{name}", host, ["@terraform", group_name]
|
|
125
|
+
elif isinstance(host, str):
|
|
126
|
+
data = {"ssh_hostname": host}
|
|
127
|
+
yield f"@terraform/{host}", data, ["@terraform", group_name]
|
|
128
|
+
else:
|
|
129
|
+
raise InventoryError(
|
|
130
|
+
"Invalid Terraform list item, should be `dict` or `str` got "
|
|
131
|
+
f"`{type(host).__name__}`",
|
|
132
|
+
)
|
pyinfra/facts/apt.py
CHANGED
|
@@ -9,7 +9,7 @@ from .util import make_cat_files_command
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def parse_apt_repo(name):
|
|
12
|
-
regex = r"^(deb(?:-src)?)(?:\s+\[([^\]]+)\])?\s+([^\s]+)\s+([
|
|
12
|
+
regex = r"^(deb(?:-src)?)(?:\s+\[([^\]]+)\])?\s+([^\s]+)\s+([^\s]+)\s+([a-z-\s]*)$"
|
|
13
13
|
|
|
14
14
|
matches = re.match(regex, name)
|
|
15
15
|
|
pyinfra/facts/files.py
CHANGED
|
@@ -5,6 +5,7 @@ The files facts provide information about the filesystem and it's contents on th
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
import re
|
|
8
|
+
import shlex
|
|
8
9
|
import stat
|
|
9
10
|
from datetime import datetime
|
|
10
11
|
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
|
@@ -109,13 +110,19 @@ class File(FactBase[Union[FileDict, Literal[False], None]]):
|
|
|
109
110
|
type = "file"
|
|
110
111
|
|
|
111
112
|
def command(self, path):
|
|
113
|
+
if path.startswith("~/"):
|
|
114
|
+
# Do not quote leading tilde to ensure that it gets properly expanded by the shell
|
|
115
|
+
path = f"~/{shlex.quote(path[2:])}"
|
|
116
|
+
else:
|
|
117
|
+
path = QuoteString(path)
|
|
118
|
+
|
|
112
119
|
return make_formatted_string_command(
|
|
113
120
|
(
|
|
114
121
|
# only stat if the path exists (file or symlink)
|
|
115
122
|
"! (test -e {0} || test -L {0} ) || "
|
|
116
123
|
"( {linux_stat_command} {0} 2> /dev/null || {bsd_stat_command} {0} )"
|
|
117
124
|
),
|
|
118
|
-
|
|
125
|
+
path,
|
|
119
126
|
linux_stat_command=LINUX_STAT_COMMAND,
|
|
120
127
|
bsd_stat_command=BSD_STAT_COMMAND,
|
|
121
128
|
)
|
pyinfra/facts/flatpak.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from pyinfra.api import FactBase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FlatpakBaseFact(FactBase):
|
|
9
|
+
abstract = True
|
|
10
|
+
|
|
11
|
+
def requires_command(self, *args, **kwargs) -> str:
|
|
12
|
+
return "flatpak"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FlatpakPackage(FlatpakBaseFact):
|
|
16
|
+
"""
|
|
17
|
+
Returns information for an installed flatpak package
|
|
18
|
+
|
|
19
|
+
.. code:: python
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
"id": "org.signal.Signal",
|
|
23
|
+
"ref": "app/org.signal.Signal/x86_64/stable",
|
|
24
|
+
"version": "7.12.0"
|
|
25
|
+
}
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
default = dict
|
|
29
|
+
_regexes = {
|
|
30
|
+
"id": "^[ ]+ID:[ ]+(.*)$",
|
|
31
|
+
"ref": r"^[ ]+Ref:[ ]+(.*)$",
|
|
32
|
+
"version": r"^[ ]+Version:[ ]+([\w\d.-]+).*$",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def command(self, package):
|
|
36
|
+
return f"flatpak info {package}"
|
|
37
|
+
|
|
38
|
+
def process(self, output):
|
|
39
|
+
data = {}
|
|
40
|
+
for line in output:
|
|
41
|
+
for regex_name, regex in self._regexes.items():
|
|
42
|
+
matches = re.match(regex, line)
|
|
43
|
+
if matches:
|
|
44
|
+
data[regex_name] = matches.group(1)
|
|
45
|
+
|
|
46
|
+
return data
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class FlatpakPackages(FlatpakBaseFact):
|
|
50
|
+
"""
|
|
51
|
+
Returns a list of installed flatpak packages:
|
|
52
|
+
|
|
53
|
+
.. code:: python
|
|
54
|
+
|
|
55
|
+
[
|
|
56
|
+
"org.gnome.Platform",
|
|
57
|
+
"org.kde.Platform",
|
|
58
|
+
"org.kde.Sdk",
|
|
59
|
+
"org.libreoffice.LibreOffice",
|
|
60
|
+
"org.videolan.VLC"
|
|
61
|
+
]
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
default = list
|
|
65
|
+
|
|
66
|
+
def command(self):
|
|
67
|
+
return "flatpak list --columns=application"
|
|
68
|
+
|
|
69
|
+
def process(self, output):
|
|
70
|
+
return [flatpak for flatpak in output[1:]]
|
pyinfra/facts/systemd.py
CHANGED
|
@@ -32,6 +32,9 @@ def _make_systemctl_cmd(user_mode=False, machine=None, user_name=None):
|
|
|
32
32
|
systemctl_cmd.append("--machine={1}@{0}".format(machine, user_name))
|
|
33
33
|
else:
|
|
34
34
|
systemctl_cmd.append("--machine={0}".format(machine))
|
|
35
|
+
elif user_name is not None:
|
|
36
|
+
# If only the user is given, assume that the connection should be made to the local machine
|
|
37
|
+
systemctl_cmd.append("--machine={0}@.host".format(user_name))
|
|
35
38
|
|
|
36
39
|
return StringCommand(*systemctl_cmd)
|
|
37
40
|
|
|
@@ -58,7 +61,7 @@ class SystemdStatus(FactBase[Dict[str, bool]]):
|
|
|
58
61
|
default = dict
|
|
59
62
|
|
|
60
63
|
state_key = "SubState"
|
|
61
|
-
state_values = ["running", "waiting", "exited"]
|
|
64
|
+
state_values = ["running", "waiting", "exited", "listening"]
|
|
62
65
|
|
|
63
66
|
def command(
|
|
64
67
|
self,
|
pyinfra/facts/zfs.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Manage ZFS filesystems.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pyinfra.api import FactBase, ShortFactBase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _process_zfs_props_table(output):
|
|
9
|
+
datasets: dict = {}
|
|
10
|
+
for line in output:
|
|
11
|
+
dataset, property, value, source = tuple(line.split("\t"))
|
|
12
|
+
if dataset not in datasets:
|
|
13
|
+
datasets[dataset] = {}
|
|
14
|
+
datasets[dataset][property] = value
|
|
15
|
+
return datasets
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Pools(FactBase):
|
|
19
|
+
def command(self):
|
|
20
|
+
return "zpool get -H all"
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def process(output):
|
|
24
|
+
return _process_zfs_props_table(output)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Datasets(FactBase):
|
|
28
|
+
def command(self):
|
|
29
|
+
return "zfs get -H all"
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def process(output):
|
|
33
|
+
return _process_zfs_props_table(output)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Filesystems(ShortFactBase):
|
|
37
|
+
fact = Datasets
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def process_data(data):
|
|
41
|
+
return {name: props for name, props in data.items() if props.get("type") == "filesystem"}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Snapshots(ShortFactBase):
|
|
45
|
+
fact = Datasets
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def process_data(data):
|
|
49
|
+
return {name: props for name, props in data.items() if props.get("type") == "snapshot"}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Volumes(ShortFactBase):
|
|
53
|
+
fact = Datasets
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def process_data(data):
|
|
57
|
+
return {name: props for name, props in data.items() if props.get("type") == "volume"}
|
pyinfra/operations/docker.py
CHANGED
|
@@ -4,7 +4,7 @@ Manager Docker Containers, Volumes and Networks
|
|
|
4
4
|
|
|
5
5
|
from pyinfra import host
|
|
6
6
|
from pyinfra.api import operation
|
|
7
|
-
from pyinfra.facts.docker import
|
|
7
|
+
from pyinfra.facts.docker import DockerContainer, DockerNetwork, DockerVolume
|
|
8
8
|
|
|
9
9
|
from .util.docker import handle_docker
|
|
10
10
|
|
|
@@ -68,7 +68,7 @@ def container(
|
|
|
68
68
|
)
|
|
69
69
|
"""
|
|
70
70
|
|
|
71
|
-
existent_container =
|
|
71
|
+
existent_container = host.get_fact(DockerContainer, object_id=container)
|
|
72
72
|
|
|
73
73
|
if force:
|
|
74
74
|
if existent_container:
|
|
@@ -183,7 +183,7 @@ def volume(volume, driver="", labels=None, present=True):
|
|
|
183
183
|
)
|
|
184
184
|
"""
|
|
185
185
|
|
|
186
|
-
existent_volume =
|
|
186
|
+
existent_volume = host.get_fact(DockerVolume, object_id=volume)
|
|
187
187
|
|
|
188
188
|
if present:
|
|
189
189
|
|
|
@@ -257,7 +257,7 @@ def network(
|
|
|
257
257
|
present=True,
|
|
258
258
|
)
|
|
259
259
|
"""
|
|
260
|
-
existent_network =
|
|
260
|
+
existent_network = host.get_fact(DockerNetwork, object_id=network)
|
|
261
261
|
|
|
262
262
|
if present:
|
|
263
263
|
if existent_network:
|
pyinfra/operations/files.py
CHANGED
|
@@ -915,7 +915,8 @@ def template(
|
|
|
915
915
|
user: str | None = None,
|
|
916
916
|
group: str | None = None,
|
|
917
917
|
mode: str | None = None,
|
|
918
|
-
create_remote_dir=True,
|
|
918
|
+
create_remote_dir: bool = True,
|
|
919
|
+
jinja_env_kwargs: dict[str, Any] | None = None,
|
|
919
920
|
**data,
|
|
920
921
|
):
|
|
921
922
|
'''
|
|
@@ -927,12 +928,18 @@ def template(
|
|
|
927
928
|
+ group: group to own the files
|
|
928
929
|
+ mode: permissions of the files
|
|
929
930
|
+ create_remote_dir: create the remote directory if it doesn't exist
|
|
931
|
+
+ jinja_env_kwargs: keyword arguments to be passed into the jinja Environment()
|
|
930
932
|
|
|
931
933
|
``create_remote_dir``:
|
|
932
934
|
If the remote directory does not exist it will be created using the same
|
|
933
935
|
user & group as passed to ``files.put``. The mode will *not* be copied over,
|
|
934
936
|
if this is required call ``files.directory`` separately.
|
|
935
937
|
|
|
938
|
+
``jinja_env_kwargs``:
|
|
939
|
+
To have more control over how jinja2 renders your template, you can pass
|
|
940
|
+
a dict with arguments that will be passed as keyword args to the jinja2
|
|
941
|
+
`Environment() <https://jinja.palletsprojects.com/en/3.0.x/api/#jinja2.Environment>`_.
|
|
942
|
+
|
|
936
943
|
Notes:
|
|
937
944
|
Common convention is to store templates in a "templates" directory and
|
|
938
945
|
have a filename suffix with '.j2' (for jinja2).
|
|
@@ -1002,7 +1009,7 @@ def template(
|
|
|
1002
1009
|
|
|
1003
1010
|
# Render and make file-like it's output
|
|
1004
1011
|
try:
|
|
1005
|
-
output = get_template(src).render(data)
|
|
1012
|
+
output = get_template(src, jinja_env_kwargs).render(data)
|
|
1006
1013
|
except (TemplateRuntimeError, TemplateSyntaxError, UndefinedError) as e:
|
|
1007
1014
|
trace_frames = [
|
|
1008
1015
|
frame
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Manage flatpak packages. See https://www.flatpak.org/
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pyinfra import host
|
|
8
|
+
from pyinfra.api import operation
|
|
9
|
+
from pyinfra.facts.flatpak import FlatpakPackages
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@operation()
|
|
13
|
+
def packages(
|
|
14
|
+
packages: str | list[str] | None = None,
|
|
15
|
+
present=True,
|
|
16
|
+
):
|
|
17
|
+
"""
|
|
18
|
+
Install/remove a flatpak package
|
|
19
|
+
|
|
20
|
+
+ packages: List of packages
|
|
21
|
+
+ present: whether the package should be installed
|
|
22
|
+
|
|
23
|
+
**Examples:**
|
|
24
|
+
|
|
25
|
+
.. code:: python
|
|
26
|
+
|
|
27
|
+
# Install vlc flatpak
|
|
28
|
+
flatpak.package(
|
|
29
|
+
name="Install vlc",
|
|
30
|
+
packages="org.videolan.VLC",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Install multiple flatpaks
|
|
34
|
+
flatpak.package(
|
|
35
|
+
name="Install vlc and kodi",
|
|
36
|
+
packages=["org.videolan.VLC", "tv.kodi.Kodi"],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Remove vlc
|
|
40
|
+
flatpak.package(
|
|
41
|
+
name="Remove vlc",
|
|
42
|
+
packages="org.videolan.VLC",
|
|
43
|
+
present=False,
|
|
44
|
+
)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
if packages is None:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
if isinstance(packages, str):
|
|
51
|
+
packages = [packages]
|
|
52
|
+
|
|
53
|
+
flatpak_packages = host.get_fact(FlatpakPackages)
|
|
54
|
+
|
|
55
|
+
install_packages = []
|
|
56
|
+
remove_packages = []
|
|
57
|
+
|
|
58
|
+
for package in packages:
|
|
59
|
+
# it's installed
|
|
60
|
+
if package in flatpak_packages:
|
|
61
|
+
if not present:
|
|
62
|
+
# we don't want it
|
|
63
|
+
remove_packages.append(package)
|
|
64
|
+
|
|
65
|
+
# it's not installed
|
|
66
|
+
if package not in flatpak_packages:
|
|
67
|
+
# we want it
|
|
68
|
+
if present:
|
|
69
|
+
install_packages.append(package)
|
|
70
|
+
|
|
71
|
+
# we don't want it
|
|
72
|
+
else:
|
|
73
|
+
host.noop(f"flatpak package {package} is not installed")
|
|
74
|
+
|
|
75
|
+
if install_packages:
|
|
76
|
+
yield " ".join(["flatpak", "install", "--noninteractive"] + install_packages)
|
|
77
|
+
|
|
78
|
+
if remove_packages:
|
|
79
|
+
yield " ".join(["flatpak", "uninstall", "--noninteractive"] + remove_packages)
|
pyinfra/operations/mysql.py
CHANGED
|
@@ -554,7 +554,7 @@ def dump(
|
|
|
554
554
|
)
|
|
555
555
|
"""
|
|
556
556
|
|
|
557
|
-
yield
|
|
557
|
+
yield StringCommand(
|
|
558
558
|
make_mysql_command(
|
|
559
559
|
executable="mysqldump",
|
|
560
560
|
database=database,
|
|
@@ -563,7 +563,8 @@ def dump(
|
|
|
563
563
|
host=mysql_host,
|
|
564
564
|
port=mysql_port,
|
|
565
565
|
),
|
|
566
|
-
|
|
566
|
+
">",
|
|
567
|
+
QuoteString(dest),
|
|
567
568
|
)
|
|
568
569
|
|
|
569
570
|
|
|
@@ -595,7 +596,7 @@ def load(
|
|
|
595
596
|
)
|
|
596
597
|
"""
|
|
597
598
|
|
|
598
|
-
|
|
599
|
+
yield StringCommand(
|
|
599
600
|
make_mysql_command(
|
|
600
601
|
database=database,
|
|
601
602
|
user=mysql_user,
|
|
@@ -605,5 +606,4 @@ def load(
|
|
|
605
606
|
),
|
|
606
607
|
"<",
|
|
607
608
|
QuoteString(src),
|
|
608
|
-
|
|
609
|
-
yield StringCommand(*commands_bits)
|
|
609
|
+
)
|
pyinfra/operations/postgres.py
CHANGED
|
@@ -16,7 +16,7 @@ See example/postgresql.py for detailed example
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
from pyinfra import host
|
|
19
|
-
from pyinfra.api import MaskString, StringCommand, operation
|
|
19
|
+
from pyinfra.api import MaskString, QuoteString, StringCommand, operation
|
|
20
20
|
from pyinfra.facts.postgres import (
|
|
21
21
|
PostgresDatabases,
|
|
22
22
|
PostgresRoles,
|
|
@@ -302,7 +302,7 @@ def dump(
|
|
|
302
302
|
port=psql_port,
|
|
303
303
|
),
|
|
304
304
|
">",
|
|
305
|
-
dest,
|
|
305
|
+
QuoteString(dest),
|
|
306
306
|
)
|
|
307
307
|
|
|
308
308
|
|
|
@@ -345,5 +345,5 @@ def load(
|
|
|
345
345
|
port=psql_port,
|
|
346
346
|
),
|
|
347
347
|
"<",
|
|
348
|
-
src,
|
|
348
|
+
QuoteString(src),
|
|
349
349
|
)
|
pyinfra/operations/server.py
CHANGED
|
@@ -94,7 +94,7 @@ def reboot(delay=10, interval=1, reboot_timeout=300):
|
|
|
94
94
|
|
|
95
95
|
while True:
|
|
96
96
|
host.connect(show_errors=False)
|
|
97
|
-
if host.
|
|
97
|
+
if host.connected:
|
|
98
98
|
break
|
|
99
99
|
|
|
100
100
|
if retries > max_retries:
|
|
@@ -723,11 +723,11 @@ def crontab(
|
|
|
723
723
|
if any(
|
|
724
724
|
(
|
|
725
725
|
special_time != existing_crontab.get("special_time"),
|
|
726
|
-
minute != existing_crontab.get("minute"),
|
|
727
|
-
hour != existing_crontab.get("hour"),
|
|
728
|
-
month != existing_crontab.get("month"),
|
|
729
|
-
day_of_week != existing_crontab.get("day_of_week"),
|
|
730
|
-
day_of_month != existing_crontab.get("day_of_month"),
|
|
726
|
+
try_int(minute) != existing_crontab.get("minute"),
|
|
727
|
+
try_int(hour) != existing_crontab.get("hour"),
|
|
728
|
+
try_int(month) != existing_crontab.get("month"),
|
|
729
|
+
try_int(day_of_week) != existing_crontab.get("day_of_week"),
|
|
730
|
+
try_int(day_of_month) != existing_crontab.get("day_of_month"),
|
|
731
731
|
existing_crontab_command != command,
|
|
732
732
|
),
|
|
733
733
|
):
|
|
@@ -881,11 +881,11 @@ def user_authorized_keys(
|
|
|
881
881
|
|
|
882
882
|
if path.exists(try_path):
|
|
883
883
|
with open(try_path, "r") as f:
|
|
884
|
-
return
|
|
884
|
+
return [key.strip() for key in f.readlines()]
|
|
885
885
|
|
|
886
|
-
return key.strip()
|
|
886
|
+
return [key.strip()]
|
|
887
887
|
|
|
888
|
-
public_keys =
|
|
888
|
+
public_keys = [key for key_or_file in public_keys for key in read_any_pub_key_file(key_or_file)]
|
|
889
889
|
|
|
890
890
|
# Ensure .ssh directory
|
|
891
891
|
# note that this always outputs commands unless the SSH user has access to the
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Manage ZFS filesystems.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pyinfra import host
|
|
6
|
+
from pyinfra.api import operation
|
|
7
|
+
from pyinfra.facts.zfs import Datasets, Snapshots
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@operation()
|
|
11
|
+
def dataset(
|
|
12
|
+
dataset_name,
|
|
13
|
+
present=True,
|
|
14
|
+
recursive=False,
|
|
15
|
+
sparse=None,
|
|
16
|
+
volume_size=None,
|
|
17
|
+
properties={},
|
|
18
|
+
**extra_props,
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Create, destroy or set properties on a ZFS dataset (e.g. filesystem,
|
|
22
|
+
volume, snapshot).
|
|
23
|
+
|
|
24
|
+
+ dataset_name: name of the filesystem to operate on
|
|
25
|
+
+ present: whether the named filesystem should exist
|
|
26
|
+
+ recursive: whether to create parent datasets, or destroy child datasets
|
|
27
|
+
+ sparse: for volumes, whether to create a sparse volume with no allocation
|
|
28
|
+
+ volume_size: the size of the volume
|
|
29
|
+
+ properties: the ZFS properties that should be set on the dataset.
|
|
30
|
+
+ **extra_props: additional props; merged with `properties` for convenience
|
|
31
|
+
|
|
32
|
+
**Examples:**
|
|
33
|
+
|
|
34
|
+
.. code:: python
|
|
35
|
+
|
|
36
|
+
zfs.dataset(
|
|
37
|
+
"tank/srv",
|
|
38
|
+
mountpoint="/srv",
|
|
39
|
+
compression="lz4",
|
|
40
|
+
properties={"com.sun:auto_snapshot": "true"}
|
|
41
|
+
)
|
|
42
|
+
zfs.dataset("tank/vm-disks/db_srv_04", volume_size="32G") # creates a volume
|
|
43
|
+
zfs.dataset("tank/home@old_version", present=False)
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
noop_msg = "{0} is already {1}".format(dataset_name, "present" if present else "absent")
|
|
48
|
+
|
|
49
|
+
properties.update(extra_props)
|
|
50
|
+
|
|
51
|
+
datasets = host.get_fact(Datasets)
|
|
52
|
+
|
|
53
|
+
existing_dataset = datasets.get(dataset_name)
|
|
54
|
+
|
|
55
|
+
if present and not existing_dataset:
|
|
56
|
+
args = ["-o {0}={1}".format(prop, value) for prop, value in properties.items()]
|
|
57
|
+
if recursive:
|
|
58
|
+
args.append("-p")
|
|
59
|
+
if sparse:
|
|
60
|
+
args.append("-s")
|
|
61
|
+
if volume_size:
|
|
62
|
+
args.append("-V {0}".format(volume_size))
|
|
63
|
+
|
|
64
|
+
args.sort() # dicts are unordered, so make sure the test results are deterministic
|
|
65
|
+
|
|
66
|
+
yield "zfs create {0} {1}".format(" ".join(args), dataset_name)
|
|
67
|
+
|
|
68
|
+
elif present and existing_dataset:
|
|
69
|
+
prop_args = [
|
|
70
|
+
"{0}={1}".format(prop, value)
|
|
71
|
+
for prop, value in properties.items() - existing_dataset.items()
|
|
72
|
+
]
|
|
73
|
+
prop_args.sort()
|
|
74
|
+
if prop_args:
|
|
75
|
+
yield "zfs set {0} {1}".format(" ".join(prop_args), dataset_name)
|
|
76
|
+
else:
|
|
77
|
+
host.noop(noop_msg)
|
|
78
|
+
|
|
79
|
+
elif existing_dataset and not present:
|
|
80
|
+
recursive_arg = "-r" if recursive else ""
|
|
81
|
+
yield "zfs destroy {0} {1}".format(recursive_arg, dataset_name)
|
|
82
|
+
|
|
83
|
+
else:
|
|
84
|
+
host.noop(noop_msg)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@operation()
|
|
88
|
+
def snapshot(snapshot_name, present=True, recursive=False, properties={}, **extra_props):
|
|
89
|
+
"""
|
|
90
|
+
Create or destroy a ZFS snapshot, or modify its properties.
|
|
91
|
+
|
|
92
|
+
+ dataset_name: name of the filesystem to operate on
|
|
93
|
+
+ present: whether the named filesystem should exist
|
|
94
|
+
+ recursive: whether to snapshot child datasets
|
|
95
|
+
+ properties: the ZFS properties that should be set on the snapshot.
|
|
96
|
+
+ **extra_props: additional props; merged with `properties` for convenience
|
|
97
|
+
|
|
98
|
+
**Examples:**
|
|
99
|
+
|
|
100
|
+
.. code:: python
|
|
101
|
+
|
|
102
|
+
zfs.snapshot("tank/home@weekly_backup")
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
properties.update(extra_props)
|
|
106
|
+
snapshots = host.get_fact(Snapshots)
|
|
107
|
+
|
|
108
|
+
if snapshot_name in snapshots or not present:
|
|
109
|
+
yield from dataset._inner(snapshot_name, present=present, properties=properties)
|
|
110
|
+
|
|
111
|
+
else:
|
|
112
|
+
args = ["-o {0}={1}".format(prop, value) for prop, value in properties.items()]
|
|
113
|
+
if recursive:
|
|
114
|
+
args.append("-r")
|
|
115
|
+
yield "zfs snap {0} {1}".format(" ".join(args), snapshot_name)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@operation()
|
|
119
|
+
def volume(
|
|
120
|
+
volume_name, size, sparse=False, present=True, recursive=False, properties={}, **extra_props
|
|
121
|
+
):
|
|
122
|
+
"""
|
|
123
|
+
Create or destroy a ZFS volume, or modify its properties.
|
|
124
|
+
|
|
125
|
+
+ volume_name: name of the volume to operate on
|
|
126
|
+
+ size: the size of the volume
|
|
127
|
+
+ sparse: create a sparse volume
|
|
128
|
+
+ present: whether the named volume should exist
|
|
129
|
+
+ recursive: whether to create parent datasets or destroy child datasets
|
|
130
|
+
+ properties: the ZFS properties that should be set on the snapshot.
|
|
131
|
+
+ **extra_props: additional props; merged with `properties` for convenience
|
|
132
|
+
|
|
133
|
+
**Examples:**
|
|
134
|
+
|
|
135
|
+
.. code:: python
|
|
136
|
+
|
|
137
|
+
zfs.volume("tank/vm-disks/db_srv_04", "32G")
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
properties.update(extra_props)
|
|
141
|
+
yield from dataset._inner(
|
|
142
|
+
volume_name,
|
|
143
|
+
volume_size=size,
|
|
144
|
+
present=present,
|
|
145
|
+
sparse=sparse,
|
|
146
|
+
recursive=recursive,
|
|
147
|
+
properties=properties,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@operation()
|
|
152
|
+
def filesystem(fs_name, present=True, recursive=False, properties={}, **extra_props):
|
|
153
|
+
"""
|
|
154
|
+
Create or destroy a ZFS filesystem, or modify its properties.
|
|
155
|
+
|
|
156
|
+
+ fs_name: name of the volume to operate on
|
|
157
|
+
+ present: whether the named volume should exist
|
|
158
|
+
+ recursive: whether to create parent datasets or destroy child datasets
|
|
159
|
+
+ properties: the ZFS properties that should be set on the snapshot.
|
|
160
|
+
+ **extra_props: additional props; merged with `properties` for convenience
|
|
161
|
+
|
|
162
|
+
**Examples:**
|
|
163
|
+
|
|
164
|
+
.. code:: python
|
|
165
|
+
|
|
166
|
+
zfs.filesystem("tank/vm-disks/db_srv_04", "32G")
|
|
167
|
+
|
|
168
|
+
"""
|
|
169
|
+
properties.update(extra_props)
|
|
170
|
+
yield from dataset._inner(
|
|
171
|
+
fs_name,
|
|
172
|
+
present=present,
|
|
173
|
+
recursive=recursive,
|
|
174
|
+
properties=properties,
|
|
175
|
+
)
|
|
@@ -15,21 +15,21 @@ pyinfra/api/connectors.py,sha256=nie7JuLxMSC6gqPjmjuCisQ11R-eAQDtMMWF6YbSQ48,659
|
|
|
15
15
|
pyinfra/api/deploy.py,sha256=xo4F7URUf3xzIChRHZn4zwqs_WTjLjZNC9i9eQjAFk8,2756
|
|
16
16
|
pyinfra/api/exceptions.py,sha256=cCbUp1qN1QO0d9aAvOAbRgYpLi0vUI5j7ZqSjcD1_P8,1861
|
|
17
17
|
pyinfra/api/facts.py,sha256=aMPtkB7vypyXRQDThjwJZzAnEgqjP0wrwyEhRHQf4Js,9449
|
|
18
|
-
pyinfra/api/host.py,sha256=
|
|
18
|
+
pyinfra/api/host.py,sha256=KHDzgCez_rMoW4YkX_2lnbL56tJDwvHHA1eeDCvBtVA,13728
|
|
19
19
|
pyinfra/api/inventory.py,sha256=nPITdNEJ7q71adIqS_OKHsMjD7amUuHEuTl6xzgh1Gk,7734
|
|
20
|
-
pyinfra/api/operation.py,sha256=
|
|
20
|
+
pyinfra/api/operation.py,sha256=Dp7pH9H3EYs7U1ZvquYUbOtWJPO9iIAa4H7GwXdxFxs,15170
|
|
21
21
|
pyinfra/api/operations.py,sha256=jvz9ISfwmQnAQVUKLnbrRdD9QHIAAfypo9l5b3fYG1w,10894
|
|
22
22
|
pyinfra/api/state.py,sha256=3dXRjeZJXnzLcbP9E4aogkRPwIg3_kK1h4Tf4FVZock,12622
|
|
23
|
-
pyinfra/api/util.py,sha256=
|
|
23
|
+
pyinfra/api/util.py,sha256=K4aFjGW7KAz2ZQqfRriRqyHMCQFFrX06WPola3epjaE,12410
|
|
24
24
|
pyinfra/connectors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
25
|
pyinfra/connectors/base.py,sha256=2fASiV-MvpXgcZAFLM_PUwYx5ax6EHai44ri_oEKeSE,3732
|
|
26
26
|
pyinfra/connectors/chroot.py,sha256=Xd72I8T58KIwKOoc0LXCw91AoEIaiHfRLDcDVTHGJ0o,5931
|
|
27
27
|
pyinfra/connectors/docker.py,sha256=2UNHhXS4hpLo7I19ixDeSd7JR8SNo43VgqsaUIZQZJ4,8741
|
|
28
28
|
pyinfra/connectors/dockerssh.py,sha256=VWHY--jqs3yf-RuPUZXav4vLeON9SzoVC9CUyOJo1rg,8919
|
|
29
29
|
pyinfra/connectors/local.py,sha256=vYOBQS_5rf-dVaPeG4dJlLwBHqkxAzLjj3aDEgbAsx8,6900
|
|
30
|
-
pyinfra/connectors/ssh.py,sha256=
|
|
30
|
+
pyinfra/connectors/ssh.py,sha256=F6LeotcbnxPPECGBTAIGbGwMJzEc3MAHGAsigoDkfYQ,21130
|
|
31
31
|
pyinfra/connectors/ssh_util.py,sha256=CN_5AdTA3RpiWCnXTrRBjez1NsN59hITDzQmXIkZvoE,3683
|
|
32
|
-
pyinfra/connectors/terraform.py,sha256=
|
|
32
|
+
pyinfra/connectors/terraform.py,sha256=Tu59cbemll5CfqlIaQtOrLa0HKzl23c64ih0DZXJu1I,4227
|
|
33
33
|
pyinfra/connectors/util.py,sha256=0bvoMsGMD-Tbfaer8NUhWJjBnaNKdmE83PDg48BYjcU,11374
|
|
34
34
|
pyinfra/connectors/vagrant.py,sha256=oEeRglzRmemRXW3vilsp_Xg9qnZMRprRJO9fd_C-f5M,4759
|
|
35
35
|
pyinfra/connectors/sshuserclient/__init__.py,sha256=Qc4RO2wknSWIiNTwOeQ0y2TeiuKHmyWDW2Dz4MOo9CE,44
|
|
@@ -37,7 +37,7 @@ pyinfra/connectors/sshuserclient/client.py,sha256=24KWAAqIaUPQIod-CSeXKkA_WhQnII
|
|
|
37
37
|
pyinfra/connectors/sshuserclient/config.py,sha256=UMwkvTgAIS7__re6Wz_pwH6EU4kO1-uMQ5zuFakH0v4,2721
|
|
38
38
|
pyinfra/facts/__init__.py,sha256=myTXSOZmAqmU88Fyifn035h9Lr6Gj2mlka_jDcXyKGw,347
|
|
39
39
|
pyinfra/facts/apk.py,sha256=q76WdaCNZGKzYia5vMscCsOi4WlnBhcj_9c7Jj-2LqQ,581
|
|
40
|
-
pyinfra/facts/apt.py,sha256=
|
|
40
|
+
pyinfra/facts/apt.py,sha256=t1Us_ABkFRhzZwtAItUNXgvmPUqmzSRHhc20qODBx1g,2172
|
|
41
41
|
pyinfra/facts/brew.py,sha256=qDz89ZlZOiCZv0GLOXOjgFtqq66SLaYXgCncYP2LwDs,2584
|
|
42
42
|
pyinfra/facts/bsdinit.py,sha256=hyESeGu0hPf8HY1D0bIFFFNFpXRdZB2R52aflVQPf9o,577
|
|
43
43
|
pyinfra/facts/cargo.py,sha256=OQF6nOulp2TIaFK1fiAEevsXnL5OMQUL6LkFHidb1yo,605
|
|
@@ -45,7 +45,8 @@ pyinfra/facts/choco.py,sha256=A0VCXnI5H9RocgO1IvaNWRIxnXiIZYEzIDG1F-ydJi4,790
|
|
|
45
45
|
pyinfra/facts/deb.py,sha256=XGyxnow9wjpE8ZKTZDa1_SNChMyMcNgFeTG1ka5uky4,1922
|
|
46
46
|
pyinfra/facts/dnf.py,sha256=9rTBgLHewbk8XCJuikzAYCumfFAzbmmHMchlaXBhdWw,977
|
|
47
47
|
pyinfra/facts/docker.py,sha256=CVSsUEiBaQrNvM1mggoKCXj5DdzwmcbufUY96koqKBw,2250
|
|
48
|
-
pyinfra/facts/files.py,sha256=
|
|
48
|
+
pyinfra/facts/files.py,sha256=qJw5_xnPxS5orFy2Ch4oUjmiFUFGGe71UEXoXdZBSek,11765
|
|
49
|
+
pyinfra/facts/flatpak.py,sha256=lnuFZYGPtPDe35YXTVgn5M0rhgcA5ys3fMI6EmvCnm4,1536
|
|
49
50
|
pyinfra/facts/gem.py,sha256=ktC2hofSwYX0fVcdWleU74ddjW1RPZvKMW-3wYp9lJE,572
|
|
50
51
|
pyinfra/facts/git.py,sha256=6e_2GjDT2oAxdtkHTyeMYQ9N64gZDorLTTVeZhFel18,1276
|
|
51
52
|
pyinfra/facts/gpg.py,sha256=wYKoQl4aHXB1UqqbWCdVhUoa6N6Liz01AmH8fPjxR48,3813
|
|
@@ -67,12 +68,13 @@ pyinfra/facts/runit.py,sha256=iarF_Tql8bkNeHsKGRANRKNyBWwMsflsTNXj1Wz14i8,2021
|
|
|
67
68
|
pyinfra/facts/selinux.py,sha256=zzh7o0SU8ocMRJzcYMuAv6ss93onfojpa1pAN1azgeU,4433
|
|
68
69
|
pyinfra/facts/server.py,sha256=fDXSNNlZghJTGqr9CWRDob-_N-8xxb-KUZlTf5No-M0,20439
|
|
69
70
|
pyinfra/facts/snap.py,sha256=6br9IMIoq88z_RS0FLXxfodIVjUmyPU9eZBa9zO8H1o,2027
|
|
70
|
-
pyinfra/facts/systemd.py,sha256=
|
|
71
|
+
pyinfra/facts/systemd.py,sha256=RS6pdcgpIvWMbQeT93O57EKXQzFzR0tF29lCAJQmaAk,4227
|
|
71
72
|
pyinfra/facts/sysvinit.py,sha256=RniaROHyeZD3jVOa_sISpZV4zx8ae8HkUQrtriLIlWc,1521
|
|
72
73
|
pyinfra/facts/upstart.py,sha256=HYR7vJ6oqtuRhxXQgzGDKYzyKqqVsjT-TtPPWOjBGdA,635
|
|
73
74
|
pyinfra/facts/vzctl.py,sha256=S9aclpDBF3DmBLwMltsd9j3B4QxQ5-1Kb1hybZodEqI,678
|
|
74
75
|
pyinfra/facts/xbps.py,sha256=4gAajBlTAg3bo7vRdx3b2TTi-vvU1y86WZqC0H9nUUk,573
|
|
75
76
|
pyinfra/facts/yum.py,sha256=i42g0FIZg62TZFqFcaUQWNekFFFo4G8vf5wyaKUuh8Q,938
|
|
77
|
+
pyinfra/facts/zfs.py,sha256=MKqh1AEa5Bpa0KDT673e92TiOb8K7YHoEg-Xo424De8,1311
|
|
76
78
|
pyinfra/facts/zypper.py,sha256=sAIZ5SqjsJ1Dc5e3pJrOoR5Gnu9BqZHpDFI8gKLts84,873
|
|
77
79
|
pyinfra/facts/util/__init__.py,sha256=f7HKu8z9_yFC899ajJ3RFiyivioaZeGfOI6nf9GviCs,521
|
|
78
80
|
pyinfra/facts/util/databases.py,sha256=EphGQApzRBXI2nG1FL9h8bozY-o4SgdQgpv9YcnCkxs,730
|
|
@@ -86,27 +88,28 @@ pyinfra/operations/bsdinit.py,sha256=okQUQDr2H8Z-cAdfdbPJiuGujsHLuV5gpuMZ1UlICEM
|
|
|
86
88
|
pyinfra/operations/cargo.py,sha256=mXWd6pb0IR6kzJMmPHwXZN-VJ-B_y8AdOFlrRzDQOZI,1104
|
|
87
89
|
pyinfra/operations/choco.py,sha256=8nG0wc1tZEA0L0HTIjgR00IDiONARokyzHyKj-R3xmo,1515
|
|
88
90
|
pyinfra/operations/dnf.py,sha256=3154Rer6dejVB1AK-CqyJhpMVn_djaSDJrVMs62GNcE,5599
|
|
89
|
-
pyinfra/operations/docker.py,sha256=
|
|
90
|
-
pyinfra/operations/files.py,sha256=
|
|
91
|
+
pyinfra/operations/docker.py,sha256=RMkrVpS-eeN5zwGnpb3WeeOAoGvFma-A1aPvjE9M1KY,8336
|
|
92
|
+
pyinfra/operations/files.py,sha256=iQVpI_7guyQRoc1hkMN2HrA1BbriuPvf5blHkhujA2I,54084
|
|
93
|
+
pyinfra/operations/flatpak.py,sha256=c2OAyuAvt3alVm9D8W6gCfmk5JFydcZD36gO_OhB8Bc,1891
|
|
91
94
|
pyinfra/operations/gem.py,sha256=2C85sOwIRMHGvmPg4uAlUVf6MokhiA7LLPqzdJRHsBg,1132
|
|
92
95
|
pyinfra/operations/git.py,sha256=b26tQF_4hykTy0FtxiuCkqPk9i8JdZaz-RBhH4X96yw,11789
|
|
93
96
|
pyinfra/operations/iptables.py,sha256=brYa4kMhZKFTu24BNds_1b6sOaG94EfqWEoWrScx-Ck,9341
|
|
94
97
|
pyinfra/operations/launchd.py,sha256=6HWvqoQ74idV_NStOEmFXwu0dmTv7YDvFtsK8An2Lu4,1177
|
|
95
98
|
pyinfra/operations/lxd.py,sha256=bKm9gsgZaruKYSL7OYFMiou-wGP4BzwIMWzjW4AZYrk,1742
|
|
96
|
-
pyinfra/operations/mysql.py,sha256=
|
|
99
|
+
pyinfra/operations/mysql.py,sha256=ctm2Z6MaB0mOArCNU4TsJzaXiKXQaa_ahmsC5Vvyi10,19857
|
|
97
100
|
pyinfra/operations/npm.py,sha256=bUmfQsClZ2YcHiihiC7k5widIXIi6lbfx_32iyaAKfo,1499
|
|
98
101
|
pyinfra/operations/openrc.py,sha256=GXFoCHEEKeyQyRvrZcNYx8og4fmgmtzTVAViBzt84TE,1580
|
|
99
102
|
pyinfra/operations/pacman.py,sha256=QMjmsBiiw362nhZY0rEDVQL5A32MG3u7GcmX4q4PzfI,1702
|
|
100
103
|
pyinfra/operations/pip.py,sha256=7PpQvZHnwBGZ60V5b0XKNR4tHLW0MXJo6_6UX7HBtGY,5856
|
|
101
104
|
pyinfra/operations/pkg.py,sha256=rORQBbKeb-6gS0LYu0a0VdiWcDZoovcUONCaf6KMdeQ,2298
|
|
102
105
|
pyinfra/operations/pkgin.py,sha256=zhUyGzKjnUfGoyHbMoYMbeeMzcsiOUpBz1zIzppigJ0,1992
|
|
103
|
-
pyinfra/operations/postgres.py,sha256=
|
|
106
|
+
pyinfra/operations/postgres.py,sha256=eh3wjX-l4ri-q3mgfV2bdmVs3m87s3C1_EbJUFss9u4,9700
|
|
104
107
|
pyinfra/operations/postgresql.py,sha256=agZjL2W4yxigk9ThIC0V_3wvmcWVdX308aJO24WkN6g,833
|
|
105
108
|
pyinfra/operations/puppet.py,sha256=eDe8D9jQbHYQ4_r4-dmEZfMASKQvj36BR8z_h8aDfw8,861
|
|
106
109
|
pyinfra/operations/python.py,sha256=u569cdPrPesrmzU09nwIPA3bk6TZ-Qv2QP0lJLcO_bw,2021
|
|
107
110
|
pyinfra/operations/runit.py,sha256=jRR5kt1OUCLbYktnu7yl3YvSiTW51VvEvOuB0yfd7Ww,5126
|
|
108
111
|
pyinfra/operations/selinux.py,sha256=imZ4dbY4tl0GpBSkUgV983jbDDihWNs_OQkOBulT7FQ,5948
|
|
109
|
-
pyinfra/operations/server.py,sha256=
|
|
112
|
+
pyinfra/operations/server.py,sha256=Fi5N53ZirbyybJzonQMIHsgI2CBLGGXTCNrr7IyyW2A,36567
|
|
110
113
|
pyinfra/operations/snap.py,sha256=a-QtNE4Dlsavqq425TUIwpEJu4oGw8UlLRkdTFyT1F8,3049
|
|
111
114
|
pyinfra/operations/ssh.py,sha256=wocoaYDlOhhItItAVQCEfnVowTtkg3AP0hQ3mnpUnl0,5634
|
|
112
115
|
pyinfra/operations/systemd.py,sha256=hPHTjASj6N_fRAzLr3DNHnxxIbiiTIIT9UStSxKDkTk,3984
|
|
@@ -115,6 +118,7 @@ pyinfra/operations/upstart.py,sha256=pHb9RGnVhT14A_y6OezfOH-lmniKpiyJqpeoOJl0beE
|
|
|
115
118
|
pyinfra/operations/vzctl.py,sha256=2u2CDkuDjzHBRQ54HfyfLpLrsbT8U7_05EEjbbhKUiU,3110
|
|
116
119
|
pyinfra/operations/xbps.py,sha256=ru3_srMBUyUXGzAsPo7WwoomfM0AeDglFv8CDqB33B0,1508
|
|
117
120
|
pyinfra/operations/yum.py,sha256=Ig7AzQy1C7I8XM37lWbw0nI5lzFGMoX30P8FV8-V5uA,5600
|
|
121
|
+
pyinfra/operations/zfs.py,sha256=vQoeXcwtf-2WXLwBnkM77EhJ_9dQenPXRGcLW07l2_c,5227
|
|
118
122
|
pyinfra/operations/zypper.py,sha256=z1CWv2uwWBlCLIhHna7U5DojVoKZYoUYpezJ_FM_xK8,5555
|
|
119
123
|
pyinfra/operations/util/__init__.py,sha256=ZAHjeCXtLo0TIOSfZ9h0Sh5IXXRCspfHs3RR1l8tQCE,366
|
|
120
124
|
pyinfra/operations/util/docker.py,sha256=6CvQgeFAXH_lDqKb7RxWpMvlCDwEAXlBaDZoJ8LxrYg,4596
|
|
@@ -125,10 +129,10 @@ pyinfra_cli/__init__.py,sha256=G0X7tNdqT45uWuK3aHIKxMdDeCgJ7zHo6vbxoG6zy_8,284
|
|
|
125
129
|
pyinfra_cli/__main__.py,sha256=WlW7eP0rrL06eguuD_q2RAqgUjg3SW-QnmrayAh2mBQ,887
|
|
126
130
|
pyinfra_cli/commands.py,sha256=J-mCJYvDebJ8M7o3HreB2zToa871-xO6_KjVhPLeHho,1832
|
|
127
131
|
pyinfra_cli/exceptions.py,sha256=iptx9Zj1od7VgSbOyXs7P8tD4zAZ_fwrQFKPlpPrfS0,4806
|
|
128
|
-
pyinfra_cli/inventory.py,sha256=
|
|
132
|
+
pyinfra_cli/inventory.py,sha256=vuSL7dU31hxazHmJoUI0c6QjdItG78x8O5ifLUWuMeI,11292
|
|
129
133
|
pyinfra_cli/log.py,sha256=7WEGtmf3ncF1BtXL2icUjyxeRKy-7XrCcQ2Hg4GWX5Y,2201
|
|
130
|
-
pyinfra_cli/main.py,sha256=
|
|
131
|
-
pyinfra_cli/prints.py,sha256=
|
|
134
|
+
pyinfra_cli/main.py,sha256=5VTniMcbKuIfjPTzaUklad5fM1BW7CUEARoSV9tPf1U,19954
|
|
135
|
+
pyinfra_cli/prints.py,sha256=heCF-ugz0F8gTSr--rYVtRqN6jpAun5DUA4cy0F8l5A,9696
|
|
132
136
|
pyinfra_cli/util.py,sha256=f3iGIPxlUiQJ5LmUGYbEz0QrySQAKmf9xov9WvHXbrk,6364
|
|
133
137
|
pyinfra_cli/virtualenv.py,sha256=6j9W54JkQLN02SrZZIVwszp0GxlaaDEUWFZjBDHIWNA,2466
|
|
134
138
|
tests/test_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -139,12 +143,12 @@ tests/test_api/test_api_config.py,sha256=bf0mDrUie3On6zGC_hJBpv-wvSf3LHBIBzUDvko
|
|
|
139
143
|
tests/test_api/test_api_deploys.py,sha256=h_zbI6CK4K8SdzEr3LEAMPxOf9hnQBdi_suqiNPqHHQ,4200
|
|
140
144
|
tests/test_api/test_api_facts.py,sha256=WnKwgLq7sk2LNO5IgIZbO5HRkDr-2GdUWO_EFfTjhO8,10695
|
|
141
145
|
tests/test_api/test_api_host.py,sha256=U_VW2vTl35vR8EdyIGMKr4y0ydsDLbvHSjZDa99CyNE,1119
|
|
142
|
-
tests/test_api/test_api_inventory.py,sha256=
|
|
146
|
+
tests/test_api/test_api_inventory.py,sha256=rqXd3e_Wwc-SxCzxgR5eLd7ZOdrF8CcHbcTZndLy5gE,2744
|
|
143
147
|
tests/test_api/test_api_operations.py,sha256=GUfnuHK2NoTAGdOT4AbytT9R8i3ZZIvGP7KBfoYcYUQ,20134
|
|
144
148
|
tests/test_api/test_api_util.py,sha256=uHv4oLpoy1_tzOoqFA1zpdvC74SvjitZbxQwp0dmjTs,1716
|
|
145
149
|
tests/test_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
146
150
|
tests/test_cli/test_cli.py,sha256=IeWuhkhLzIkRbOEx5-yaW6xV5l4Y8fxaGaDGlMcOyYE,6016
|
|
147
|
-
tests/test_cli/test_cli_deploy.py,sha256=
|
|
151
|
+
tests/test_cli/test_cli_deploy.py,sha256=3tlXpN_ntCvZDeymfQKrZm0kgADkiLUIAGQg5V8-KrU,5079
|
|
148
152
|
tests/test_cli/test_cli_exceptions.py,sha256=02sjC6rMptuqchgcdjdsVNQbSQYW6HwGutSy6Q6sMs4,3088
|
|
149
153
|
tests/test_cli/test_cli_inventory.py,sha256=xlo-p3HdfVPNqxi7SknEZ2mWrKsdDaK3PoVN-tl95Z0,2394
|
|
150
154
|
tests/test_cli/test_cli_util.py,sha256=-Ehnj0cO-EkF-6KLxcPPcFeuAUMTz-fKITrxhuiYhV4,2562
|
|
@@ -157,12 +161,12 @@ tests/test_connectors/test_dockerssh.py,sha256=MaC9IK1OZDiqoIsuLOZBJnPDglsMoPDoL
|
|
|
157
161
|
tests/test_connectors/test_local.py,sha256=N_FkejDZKu7XLnKeApqfBARYMyxf-hRXCQJrXLHvwRg,7442
|
|
158
162
|
tests/test_connectors/test_ssh.py,sha256=zYL0FbRXzqkYJslhmVeUgSkcHtozhmvZfRcaqDrYKvI,40386
|
|
159
163
|
tests/test_connectors/test_sshuserclient.py,sha256=2PQNLPhNL6lBACc6tQuXmPoog-9L6AdDQNrA-rEw1_8,5734
|
|
160
|
-
tests/test_connectors/test_terraform.py,sha256=
|
|
164
|
+
tests/test_connectors/test_terraform.py,sha256=RZInSjes394eR5CrGGEjzZEFY-UpQj47n4MZH0_ExyY,3779
|
|
161
165
|
tests/test_connectors/test_util.py,sha256=hQir0WyjH0LEF6xvIyHNyqdI5pkJX6qUR9287MgO2bY,4647
|
|
162
166
|
tests/test_connectors/test_vagrant.py,sha256=27qRB7ftjEPaj4ejBNZ-rR4Ou1AD1VyVcf2XjwZPG3M,3640
|
|
163
|
-
pyinfra-3.
|
|
164
|
-
pyinfra-3.
|
|
165
|
-
pyinfra-3.
|
|
166
|
-
pyinfra-3.
|
|
167
|
-
pyinfra-3.
|
|
168
|
-
pyinfra-3.
|
|
167
|
+
pyinfra-3.1.dist-info/LICENSE.md,sha256=gwC95tUll0gwB32tHNkTAasN7Sb6vjWzXa305NwClbI,1076
|
|
168
|
+
pyinfra-3.1.dist-info/METADATA,sha256=NCTtirew0Vm6uxlBVYrUz6b1aVFoJxHGCV9_MzCS6n0,8039
|
|
169
|
+
pyinfra-3.1.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
|
|
170
|
+
pyinfra-3.1.dist-info/entry_points.txt,sha256=BraEFyquy05M8ch33HZXOHoH_m2BTqejL3xX3NrpzOM,471
|
|
171
|
+
pyinfra-3.1.dist-info/top_level.txt,sha256=2K6D1mK35JTSEBgOfEPV-N-uA2SDErxGiE0J-HUMMVI,26
|
|
172
|
+
pyinfra-3.1.dist-info/RECORD,,
|
pyinfra_cli/inventory.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union
|
|
|
6
6
|
|
|
7
7
|
from pyinfra import logger
|
|
8
8
|
from pyinfra.api.inventory import Inventory
|
|
9
|
+
from pyinfra.connectors.sshuserclient.client import get_ssh_config
|
|
9
10
|
from pyinfra.context import ctx_inventory
|
|
10
11
|
|
|
11
12
|
from .exceptions import CliError
|
|
@@ -88,7 +89,34 @@ def _resolves_to_host(maybe_host: str) -> bool:
|
|
|
88
89
|
socket.getaddrinfo(maybe_host, port=None)
|
|
89
90
|
return True
|
|
90
91
|
except socket.gaierror:
|
|
91
|
-
|
|
92
|
+
alias = _get_ssh_alias(maybe_host)
|
|
93
|
+
if not alias:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
socket.getaddrinfo(alias, port=None)
|
|
98
|
+
return True
|
|
99
|
+
except socket.gaierror:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _get_ssh_alias(maybe_host: str) -> Optional[str]:
|
|
104
|
+
logger.debug('Checking if "%s" is an SSH alias', maybe_host)
|
|
105
|
+
|
|
106
|
+
# Note this does not cover the case where `host.data.ssh_config_file` is used
|
|
107
|
+
ssh_config = get_ssh_config()
|
|
108
|
+
|
|
109
|
+
if ssh_config is None:
|
|
110
|
+
logger.debug("Could not load SSH config")
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
options = ssh_config.lookup(maybe_host)
|
|
114
|
+
alias = options.get("hostname")
|
|
115
|
+
|
|
116
|
+
if alias is None or maybe_host == alias:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
return alias
|
|
92
120
|
|
|
93
121
|
|
|
94
122
|
def make_inventory(
|
|
@@ -105,7 +133,11 @@ def make_inventory(
|
|
|
105
133
|
# (1) an inventory file is a common use case and (2) no other option can have a comma or an @
|
|
106
134
|
# symbol in them.
|
|
107
135
|
is_path_or_host_list_or_connector = (
|
|
108
|
-
path.exists(inventory)
|
|
136
|
+
path.exists(inventory)
|
|
137
|
+
or "," in inventory
|
|
138
|
+
or "@" in inventory
|
|
139
|
+
# Special case: passing an arbitrary name and specifying --data ssh_hostname=a.b.c
|
|
140
|
+
or (override_data is not None and "ssh_hostname" in override_data)
|
|
109
141
|
)
|
|
110
142
|
if not is_path_or_host_list_or_connector:
|
|
111
143
|
# Next, try loading the inventory from a python function. This happens before checking for a
|
pyinfra_cli/main.py
CHANGED
|
@@ -361,6 +361,14 @@ def _main(
|
|
|
361
361
|
else:
|
|
362
362
|
logger.info("--> Detected changes:")
|
|
363
363
|
print_meta(state)
|
|
364
|
+
click.echo(
|
|
365
|
+
"""
|
|
366
|
+
Detected changes may not include every change pyinfra will execute.
|
|
367
|
+
Hidden side effects of operations may alter behaviour of future operations,
|
|
368
|
+
this will be shown in the results. The remote state will always be updated
|
|
369
|
+
to reflect the state defined by the input operations.""",
|
|
370
|
+
err=True,
|
|
371
|
+
)
|
|
364
372
|
|
|
365
373
|
# If --debug-facts or --debug-operations, print and exit
|
|
366
374
|
if debug_facts or debug_operations:
|
|
@@ -372,14 +380,6 @@ def _main(
|
|
|
372
380
|
if dry:
|
|
373
381
|
_exit()
|
|
374
382
|
|
|
375
|
-
click.echo(
|
|
376
|
-
"""
|
|
377
|
-
Detected changes may not include every change pyinfra will execute.
|
|
378
|
-
Hidden side effects of operations may alter behaviour of future operations,
|
|
379
|
-
this will be shown in the results. The remote state will always be updated
|
|
380
|
-
to reflect the state defined by the input operations.""",
|
|
381
|
-
err=True,
|
|
382
|
-
)
|
|
383
383
|
if (
|
|
384
384
|
can_diff
|
|
385
385
|
and not yes
|
pyinfra_cli/prints.py
CHANGED
|
@@ -131,6 +131,10 @@ def print_facts(facts):
|
|
|
131
131
|
|
|
132
132
|
|
|
133
133
|
def print_support_info():
|
|
134
|
+
from importlib.metadata import PackageNotFoundError, requires, version
|
|
135
|
+
|
|
136
|
+
from packaging.requirements import Requirement
|
|
137
|
+
|
|
134
138
|
click.echo(
|
|
135
139
|
"""
|
|
136
140
|
If you are having issues with pyinfra or wish to make feature requests, please
|
|
@@ -144,6 +148,18 @@ def print_support_info():
|
|
|
144
148
|
click.echo(" Release: {0}".format(platform.uname()[2]), err=True)
|
|
145
149
|
click.echo(" Machine: {0}".format(platform.uname()[4]), err=True)
|
|
146
150
|
click.echo(" pyinfra: v{0}".format(__version__), err=True)
|
|
151
|
+
|
|
152
|
+
for requirement_string in sorted(requires("pyinfra") or []):
|
|
153
|
+
requirement = Requirement(requirement_string)
|
|
154
|
+
try:
|
|
155
|
+
click.echo(
|
|
156
|
+
" {0}: v{1}".format(requirement.name, version(requirement.name)),
|
|
157
|
+
err=True,
|
|
158
|
+
)
|
|
159
|
+
except PackageNotFoundError:
|
|
160
|
+
# package not installed in this environment
|
|
161
|
+
continue
|
|
162
|
+
|
|
147
163
|
click.echo(" Executable: {0}".format(sys.argv[0]), err=True)
|
|
148
164
|
click.echo(
|
|
149
165
|
" Python: {0} ({1}, {2})".format(
|
|
@@ -278,7 +294,7 @@ def print_results(state: "State"):
|
|
|
278
294
|
|
|
279
295
|
op_meta = state.ops[host][op_hash].operation_meta
|
|
280
296
|
if op_meta.did_succeed(_raise_if_not_complete=False):
|
|
281
|
-
if op_meta.
|
|
297
|
+
if op_meta.did_change():
|
|
282
298
|
hosts_in_op_success.append(host.name)
|
|
283
299
|
else:
|
|
284
300
|
hosts_in_op_no_change.append(host.name)
|
|
@@ -51,3 +51,24 @@ class TestInventoryApi(TestCase):
|
|
|
51
51
|
|
|
52
52
|
assert inventory.get_host("somehost").data.override_data == "override_data"
|
|
53
53
|
assert inventory.get_host("anotherhost").data.override_data == "override_data"
|
|
54
|
+
|
|
55
|
+
def test_inventory_group_data_not_shared(self):
|
|
56
|
+
group_data = {"test": {}}
|
|
57
|
+
hosts = ["hosthost", "anotherhost"]
|
|
58
|
+
|
|
59
|
+
inventory = Inventory(
|
|
60
|
+
(hosts, {}),
|
|
61
|
+
group=(hosts, group_data),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
hosthost = inventory.get_host("hosthost")
|
|
65
|
+
|
|
66
|
+
# Test that modifying host.data.<X> *does not* stick (both on the same
|
|
67
|
+
# host and also other hosts).
|
|
68
|
+
hosthost.data.test["hi"] = "no"
|
|
69
|
+
assert hosthost.data.test == {}
|
|
70
|
+
assert inventory.get_host("anotherhost").data.test == {}
|
|
71
|
+
|
|
72
|
+
# Test that setting host.data.<X> *does* persist
|
|
73
|
+
hosthost.data.somethingelse = {"hello": "world"}
|
|
74
|
+
assert hosthost.data.somethingelse == {"hello": "world"}
|
|
@@ -33,10 +33,14 @@ class TestCliDeployState(PatchSSHTestCase):
|
|
|
33
33
|
assert list(op_meta.names)[0] == correct_op_name
|
|
34
34
|
|
|
35
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
|
|
36
40
|
if correct_host_names is True or host.name in correct_host_names:
|
|
37
|
-
|
|
41
|
+
assert executed is True
|
|
38
42
|
else:
|
|
39
|
-
|
|
43
|
+
assert executed is False
|
|
40
44
|
|
|
41
45
|
def test_deploy(self):
|
|
42
46
|
task_file_path = path.join("tasks", "a_task.py")
|
|
@@ -72,6 +76,7 @@ class TestCliDeployState(PatchSSHTestCase):
|
|
|
72
76
|
("Nested order loop 2/1", ("somehost", "anotherhost")),
|
|
73
77
|
("Nested order loop 2/2", ("somehost", "anotherhost")),
|
|
74
78
|
("Final limited operation", ("somehost",)),
|
|
79
|
+
("Second final limited operation", ("anotherhost", "someotherhost")),
|
|
75
80
|
]
|
|
76
81
|
|
|
77
82
|
# Run 3 iterations of the test - each time shuffling the order of the
|
|
@@ -20,9 +20,9 @@ class TestTerraformConnector(TestCase):
|
|
|
20
20
|
with self.assertRaises(InventoryError) as context:
|
|
21
21
|
list(TerraformInventoryConnector.make_names_data("output_key"))
|
|
22
22
|
|
|
23
|
-
assert (
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
assert context.exception.args[0] == (
|
|
24
|
+
"No Terraform output with key: `output_key`, "
|
|
25
|
+
"valid keys:\n - hello\n - hello.world"
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
@patch("pyinfra.connectors.terraform.local.shell")
|
|
@@ -58,7 +58,7 @@ class TestTerraformConnector(TestCase):
|
|
|
58
58
|
(
|
|
59
59
|
"@terraform/somehost",
|
|
60
60
|
{"ssh_hostname": "somehost"},
|
|
61
|
-
["@terraform"],
|
|
61
|
+
["@terraform", "all"],
|
|
62
62
|
),
|
|
63
63
|
]
|
|
64
64
|
|
|
@@ -71,7 +71,7 @@ class TestTerraformConnector(TestCase):
|
|
|
71
71
|
(
|
|
72
72
|
"@terraform/somehost",
|
|
73
73
|
{"ssh_hostname": "somehost"},
|
|
74
|
-
["@terraform"],
|
|
74
|
+
["@terraform", "all"],
|
|
75
75
|
),
|
|
76
76
|
]
|
|
77
77
|
|
|
@@ -88,7 +88,7 @@ class TestTerraformConnector(TestCase):
|
|
|
88
88
|
(
|
|
89
89
|
"@terraform/a name",
|
|
90
90
|
{"ssh_hostname": "hostname"},
|
|
91
|
-
["@terraform"],
|
|
91
|
+
["@terraform", "all"],
|
|
92
92
|
),
|
|
93
93
|
]
|
|
94
94
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|