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 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
- return data[key]
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 _did_change(self) -> bool:
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
- @property
107
- def did_change(self):
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._did_change()
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
@@ -606,7 +606,7 @@ class SSHConnector(BaseConnector):
606
606
  rsync_flags=" ".join(flags),
607
607
  ssh_flags=" ".join(ssh_flags),
608
608
  remote_rsync_command=remote_rsync_command,
609
- user=user,
609
+ user=user or "",
610
610
  hostname=hostname,
611
611
  src=src,
612
612
  dest=dest,
@@ -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 alpha!")
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
- name = ""
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
- for ssh_target in tf_output_value:
106
- if isinstance(ssh_target, dict):
107
- name = ssh_target.pop("name", ssh_target.get("ssh_hostname"))
108
- if name is None:
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
- else:
111
+ for group_name, hosts in tf_output_value.items():
112
+ if not isinstance(hosts, list):
119
113
  raise InventoryError(
120
- "Invalid Terraform list item, should be `dict` or `str` got "
121
- f"`{type(ssh_target).__name__}`",
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+([a-z-]+)\s+([a-z-\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
- QuoteString(path),
125
+ path,
119
126
  linux_stat_command=LINUX_STAT_COMMAND,
120
127
  bsd_stat_command=BSD_STAT_COMMAND,
121
128
  )
@@ -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"}
@@ -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 DockerContainers, DockerNetworks, DockerVolumes
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 = [c for c in host.get_fact(DockerContainers) if container in c["Name"]]
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 = [v for v in host.get_fact(DockerVolumes) if v["Name"] == 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 = [n for n in host.get_fact(DockerNetworks) if n["Name"] == network]
260
+ existent_network = host.get_fact(DockerNetwork, object_id=network)
261
261
 
262
262
  if present:
263
263
  if existent_network:
@@ -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)
@@ -554,7 +554,7 @@ def dump(
554
554
  )
555
555
  """
556
556
 
557
- yield "{0} > {1}".format(
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
- dest,
566
+ ">",
567
+ QuoteString(dest),
567
568
  )
568
569
 
569
570
 
@@ -595,7 +596,7 @@ def load(
595
596
  )
596
597
  """
597
598
 
598
- commands_bits = [
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
+ )
@@ -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
  )
@@ -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.connection:
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 f.read().strip()
884
+ return [key.strip() for key in f.readlines()]
885
885
 
886
- return key.strip()
886
+ return [key.strip()]
887
887
 
888
- public_keys = list(map(read_any_pub_key_file, 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
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyinfra
3
- Version: 3.0.1
3
+ Version: 3.1
4
4
  Summary: pyinfra automates/provisions/manages/deploys infrastructure.
5
5
  Home-page: https://pyinfra.com
6
6
  Author: Nick / Fizzadar
@@ -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=3lRhlZDRKvCNvpziaTglExy2Ep1wd4YdmGDNY4emAdA,13466
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=pB0LpjUqbMLGtoDnsckNw0FRoeo1BopL0fnXVDM1JyU,15112
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=qbrC-Hjvzre2_U0l3hHo4wzZBDFN-zcWBNoWQgIyXxE,12272
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=QvXrmg3G47VwGEiPP-8Nse-9Yostc5N4PxwiiSlhlwo,21124
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=G7lK168Fz0jNFetc_7_bPT-RnoaRDksJat0R26fqkUk,3617
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=Cs5AyfEoxWmKU2wU9pYeTDeikA4mxU4TehwwKr5i57g,2173
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=cHbK2S8jJCLB7pDRBTqekHAkBo3_HnlqVcRdiZu8_VU,11536
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=eRbhK2A1jWy-jrXFdKVm-VkiCfT_PQP5xR6QbP3pQuc,4012
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=Mra-m2iayXkc2LgCk2tuE6M7lZHhOGNJD3WQIPM9t2I,8396
90
- pyinfra/operations/files.py,sha256=9O_HKgmVD_z74jtSivY4pKBPrCDKKHDSy0jAB9QERHU,53639
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=QcYvEQDlPESzDDoJ-HFwJFzN7ftsbsP892LMRZrmaLQ,19873
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=LRoedDevQqiM5eX5Lmzb5mr_E9Od0ROVC0j18ZqaR0w,9661
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=cjQRZMYkSNKDwlKpTSj3iuJO3mNre0jR4R3MTIS_xIk,36462
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=S8aSl8TrF72Ni7eHiB40GjtAJeqyB1VxHobG2k254hQ,10329
132
+ pyinfra_cli/inventory.py,sha256=vuSL7dU31hxazHmJoUI0c6QjdItG78x8O5ifLUWuMeI,11292
129
133
  pyinfra_cli/log.py,sha256=7WEGtmf3ncF1BtXL2icUjyxeRKy-7XrCcQ2Hg4GWX5Y,2201
130
- pyinfra_cli/main.py,sha256=PjZ1Fe3jlodvBsnY-xXU8ASJim2ocU6j6ICQzsoXhRU,19922
131
- pyinfra_cli/prints.py,sha256=za6V-yjXf-LViBW73qWcyfsajCUnf0NCG-7K0ugOA0k,9170
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=VLbV0MXdRLOPvTXJF156ne6rAx1cBlFfgq_1S79s4tw,2013
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=KBnnDsiD21h7t1S2JXpEDpiMxh0AFqwxaEl0z78IE9E,4858
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=Z5MhgDeRDFumu-GlbjMD0ZRkecwBIPP8C8ZVg-mq7C8,3743
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.0.1.dist-info/LICENSE.md,sha256=gwC95tUll0gwB32tHNkTAasN7Sb6vjWzXa305NwClbI,1076
164
- pyinfra-3.0.1.dist-info/METADATA,sha256=vpa6_3c318yjxCjV4OsawHtZFm_ESQ-CuR2ox3hUdQA,8041
165
- pyinfra-3.0.1.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
166
- pyinfra-3.0.1.dist-info/entry_points.txt,sha256=BraEFyquy05M8ch33HZXOHoH_m2BTqejL3xX3NrpzOM,471
167
- pyinfra-3.0.1.dist-info/top_level.txt,sha256=2K6D1mK35JTSEBgOfEPV-N-uA2SDErxGiE0J-HUMMVI,26
168
- pyinfra-3.0.1.dist-info/RECORD,,
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
- return False
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) or "," in inventory or "@" in 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._did_change():
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
- self.assertIn(op_hash, host.op_hash_order)
41
+ assert executed is True
38
42
  else:
39
- self.assertNotIn(op_hash, host.op_hash_order)
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
- context.exception.args[0]
25
- == "No Terraform output with key: `output_key`, valid keys:\n - hello.world"
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