pyinfra 3.0.dev0__py2.py3-none-any.whl → 3.0.2__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +115 -97
  3. pyinfra/api/arguments_typed.py +80 -0
  4. pyinfra/api/command.py +5 -3
  5. pyinfra/api/config.py +139 -39
  6. pyinfra/api/connectors.py +5 -2
  7. pyinfra/api/deploy.py +19 -19
  8. pyinfra/api/exceptions.py +35 -4
  9. pyinfra/api/facts.py +62 -86
  10. pyinfra/api/host.py +102 -15
  11. pyinfra/api/inventory.py +4 -0
  12. pyinfra/api/operation.py +188 -120
  13. pyinfra/api/operations.py +66 -113
  14. pyinfra/api/state.py +53 -34
  15. pyinfra/api/util.py +64 -33
  16. pyinfra/connectors/base.py +65 -20
  17. pyinfra/connectors/chroot.py +15 -13
  18. pyinfra/connectors/docker.py +62 -72
  19. pyinfra/connectors/dockerssh.py +20 -19
  20. pyinfra/connectors/local.py +32 -22
  21. pyinfra/connectors/ssh.py +162 -86
  22. pyinfra/connectors/sshuserclient/client.py +1 -1
  23. pyinfra/connectors/terraform.py +57 -39
  24. pyinfra/connectors/util.py +26 -27
  25. pyinfra/connectors/vagrant.py +27 -26
  26. pyinfra/context.py +1 -0
  27. pyinfra/facts/apk.py +7 -2
  28. pyinfra/facts/apt.py +15 -7
  29. pyinfra/facts/brew.py +28 -13
  30. pyinfra/facts/bsdinit.py +9 -6
  31. pyinfra/facts/cargo.py +6 -3
  32. pyinfra/facts/choco.py +8 -4
  33. pyinfra/facts/deb.py +21 -9
  34. pyinfra/facts/dnf.py +11 -6
  35. pyinfra/facts/docker.py +30 -5
  36. pyinfra/facts/files.py +49 -33
  37. pyinfra/facts/gem.py +7 -2
  38. pyinfra/facts/git.py +14 -21
  39. pyinfra/facts/gpg.py +4 -1
  40. pyinfra/facts/hardware.py +186 -138
  41. pyinfra/facts/launchd.py +7 -2
  42. pyinfra/facts/lxd.py +8 -2
  43. pyinfra/facts/mysql.py +19 -12
  44. pyinfra/facts/npm.py +3 -1
  45. pyinfra/facts/openrc.py +8 -2
  46. pyinfra/facts/pacman.py +13 -5
  47. pyinfra/facts/pip.py +2 -0
  48. pyinfra/facts/pkg.py +5 -1
  49. pyinfra/facts/pkgin.py +7 -2
  50. pyinfra/facts/postgres.py +170 -0
  51. pyinfra/facts/postgresql.py +5 -162
  52. pyinfra/facts/rpm.py +21 -15
  53. pyinfra/facts/runit.py +70 -0
  54. pyinfra/facts/selinux.py +12 -4
  55. pyinfra/facts/server.py +240 -82
  56. pyinfra/facts/snap.py +8 -2
  57. pyinfra/facts/systemd.py +37 -13
  58. pyinfra/facts/sysvinit.py +7 -4
  59. pyinfra/facts/upstart.py +7 -2
  60. pyinfra/facts/util/packaging.py +3 -2
  61. pyinfra/facts/vzctl.py +8 -4
  62. pyinfra/facts/xbps.py +7 -2
  63. pyinfra/facts/yum.py +10 -5
  64. pyinfra/facts/zypper.py +9 -4
  65. pyinfra/operations/apk.py +5 -3
  66. pyinfra/operations/apt.py +28 -25
  67. pyinfra/operations/brew.py +60 -29
  68. pyinfra/operations/bsdinit.py +6 -4
  69. pyinfra/operations/cargo.py +3 -1
  70. pyinfra/operations/choco.py +3 -1
  71. pyinfra/operations/dnf.py +16 -20
  72. pyinfra/operations/docker.py +339 -0
  73. pyinfra/operations/files.py +187 -168
  74. pyinfra/operations/gem.py +3 -1
  75. pyinfra/operations/git.py +23 -25
  76. pyinfra/operations/iptables.py +33 -25
  77. pyinfra/operations/launchd.py +5 -6
  78. pyinfra/operations/lxd.py +7 -4
  79. pyinfra/operations/mysql.py +59 -55
  80. pyinfra/operations/npm.py +8 -1
  81. pyinfra/operations/openrc.py +5 -3
  82. pyinfra/operations/pacman.py +6 -7
  83. pyinfra/operations/pip.py +19 -12
  84. pyinfra/operations/pkg.py +3 -1
  85. pyinfra/operations/pkgin.py +5 -3
  86. pyinfra/operations/postgres.py +349 -0
  87. pyinfra/operations/postgresql.py +18 -335
  88. pyinfra/operations/puppet.py +3 -1
  89. pyinfra/operations/python.py +8 -19
  90. pyinfra/operations/runit.py +182 -0
  91. pyinfra/operations/selinux.py +47 -29
  92. pyinfra/operations/server.py +138 -67
  93. pyinfra/operations/snap.py +3 -1
  94. pyinfra/operations/ssh.py +18 -16
  95. pyinfra/operations/systemd.py +18 -12
  96. pyinfra/operations/sysvinit.py +7 -5
  97. pyinfra/operations/upstart.py +7 -5
  98. pyinfra/operations/util/__init__.py +12 -0
  99. pyinfra/operations/util/docker.py +177 -0
  100. pyinfra/operations/util/files.py +24 -16
  101. pyinfra/operations/util/packaging.py +54 -38
  102. pyinfra/operations/util/service.py +39 -47
  103. pyinfra/operations/vzctl.py +12 -10
  104. pyinfra/operations/xbps.py +5 -3
  105. pyinfra/operations/yum.py +15 -19
  106. pyinfra/operations/zypper.py +9 -10
  107. pyinfra/version.py +5 -2
  108. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/METADATA +51 -58
  109. pyinfra-3.0.2.dist-info/RECORD +168 -0
  110. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/WHEEL +1 -1
  111. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/entry_points.txt +0 -3
  112. pyinfra_cli/__main__.py +4 -3
  113. pyinfra_cli/commands.py +3 -2
  114. pyinfra_cli/exceptions.py +75 -43
  115. pyinfra_cli/inventory.py +52 -31
  116. pyinfra_cli/log.py +10 -2
  117. pyinfra_cli/main.py +88 -65
  118. pyinfra_cli/prints.py +37 -109
  119. pyinfra_cli/util.py +15 -10
  120. tests/test_api/test_api.py +2 -0
  121. tests/test_api/test_api_arguments.py +9 -9
  122. tests/test_api/test_api_deploys.py +15 -19
  123. tests/test_api/test_api_facts.py +4 -5
  124. tests/test_api/test_api_operations.py +18 -20
  125. tests/test_api/test_api_util.py +41 -2
  126. tests/test_cli/test_cli.py +14 -50
  127. tests/test_cli/test_cli_deploy.py +17 -14
  128. tests/test_cli/test_cli_exceptions.py +50 -19
  129. tests/test_cli/test_cli_inventory.py +66 -0
  130. tests/test_cli/util.py +1 -1
  131. tests/test_connectors/test_dockerssh.py +11 -8
  132. tests/test_connectors/test_ssh.py +88 -23
  133. tests/test_connectors/test_sshuserclient.py +1 -1
  134. tests/test_connectors/test_terraform.py +11 -8
  135. tests/test_connectors/test_vagrant.py +6 -6
  136. pyinfra/connectors/ansible.py +0 -175
  137. pyinfra/connectors/mech.py +0 -189
  138. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  139. pyinfra/connectors/winrm.py +0 -312
  140. pyinfra/facts/windows.py +0 -366
  141. pyinfra/facts/windows_files.py +0 -90
  142. pyinfra/operations/windows.py +0 -59
  143. pyinfra/operations/windows_files.py +0 -538
  144. pyinfra-3.0.dev0.dist-info/RECORD +0 -170
  145. tests/test_connectors/test_ansible.py +0 -64
  146. tests/test_connectors/test_mech.py +0 -126
  147. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/LICENSE.md +0 -0
  148. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/top_level.txt +0 -0
@@ -1,175 +0,0 @@
1
- """
2
- **Note**: this connector is a work in progress! While it parses the list of
3
- hosts OK, it doesn't handle nested groups properly yet.
4
-
5
- The `@ansible` connector can be used to parse Ansible inventory files.
6
-
7
- .. code:: python
8
-
9
- # Load an Ansible inventory relative to the current directory
10
- pyinfra @ansible/path/to/inventory
11
-
12
- # Load using an absolute path
13
- pyinfra @ansible//absolute/path/to/inventory
14
- """
15
- import json
16
- import re
17
- from collections import defaultdict
18
- from configparser import ConfigParser
19
- from os import path
20
- from typing import TYPE_CHECKING, Optional
21
-
22
- from pyinfra import logger
23
- from pyinfra.api.exceptions import InventoryError
24
- from pyinfra.api.util import memoize
25
-
26
- from .base import BaseConnector
27
-
28
- if TYPE_CHECKING:
29
- from pyinfra.api.host import Host
30
-
31
- try:
32
- import yaml
33
- except ImportError:
34
- yaml = None # type: ignore
35
-
36
-
37
- @memoize
38
- def show_warning():
39
- logger.warning("The @ansible connector is in alpha!")
40
-
41
-
42
- class AnsibleInventoryConnector(BaseConnector):
43
- @staticmethod
44
- def make_names_data(inventory_filename: Optional[str] = None):
45
- show_warning()
46
-
47
- if not inventory_filename:
48
- raise InventoryError("No Ansible inventory filename provided!")
49
-
50
- if not path.exists(inventory_filename):
51
- raise InventoryError(
52
- ("Could not find Ansible inventory file: {0}").format(inventory_filename),
53
- )
54
-
55
- return parse_inventory(inventory_filename)
56
-
57
-
58
- def parse_inventory(inventory_filename: str):
59
- # fallback to INI if no extension
60
- extension = inventory_filename.split(".")[-1] if "." in inventory_filename else "ini"
61
-
62
- # host:set(groups) mapping
63
- host_to_groups = {}
64
-
65
- if extension in ["ini"]:
66
- host_to_groups = parse_inventory_ini(inventory_filename)
67
- elif extension in ["json"]:
68
- with open(inventory_filename, encoding="utf-8") as inventory_file:
69
- inventory_tree = json.load(inventory_file)
70
- # close file early
71
- host_to_groups = parse_inventory_tree(inventory_tree)
72
- elif extension in ["yaml", "yml"]:
73
- if yaml is None:
74
- raise Exception(
75
- (
76
- "To parse YAML Ansible inventories requires `pyyaml`. "
77
- "Install it with `pip install pyyaml`."
78
- ),
79
- )
80
- with open(inventory_filename, encoding="utf-8") as inventory_file:
81
- inventory_tree = yaml.safe_load(inventory_file)
82
- # close file early
83
- host_to_groups = parse_inventory_tree(inventory_tree)
84
- else:
85
- raise InventoryError(("Ansible inventory file format not supported: {0}").format(extension))
86
-
87
- return [(host, {}, sorted(list(host_to_groups.get(host, [])))) for host in host_to_groups]
88
-
89
-
90
- def parse_inventory_ini(inventory_filename: str):
91
- config = ConfigParser(
92
- delimiters=(" "), # we only handle the hostnames for now
93
- allow_no_value=True, # we don't by default have = values
94
- interpolation=None, # remove any Python interpolation
95
- )
96
- config.read(inventory_filename)
97
-
98
- host_to_groups = defaultdict(set)
99
- group_to_hosts = defaultdict(set)
100
- hosts = []
101
-
102
- # First pass - load hosts/groups of hosts
103
- for section in config.sections():
104
- if ":" in section: # ignore :children and :vars sections this time
105
- continue
106
-
107
- options = config.options(section)
108
- for host in _parse_ansible_hosts(options):
109
- hosts.append(host)
110
- host_to_groups[host].add(section)
111
- group_to_hosts[section].add(host)
112
-
113
- # Second pass - load any children groups
114
- for section in config.sections():
115
- if not section.endswith(":children"): # we only support :children for now
116
- continue
117
-
118
- group_name = section.replace(":children", "")
119
-
120
- options = config.options(section)
121
- for sub_group_name in options:
122
- sub_group_hosts = group_to_hosts[sub_group_name]
123
- for host in sub_group_hosts:
124
- host_to_groups[host].add(group_name)
125
-
126
- return host_to_groups
127
-
128
-
129
- def _parse_ansible_hosts(hosts):
130
- for host in hosts:
131
- expand_match = re.search(r"\[[0-9:]+\]", host)
132
- if expand_match:
133
- expand_string = host[expand_match.start() : expand_match.end()]
134
- bits = expand_string[1:-1].split(":") # remove the [] either side
135
-
136
- zfill = 0
137
- if bits[0].startswith("0"):
138
- zfill = len(bits[0])
139
-
140
- start, end = int(bits[0]), int(bits[1])
141
- step = int(bits[2]) if len(bits) > 2 else 1
142
-
143
- for n in range(start, end + 1, step):
144
- number_as_string = "{0}".format(n)
145
- if zfill:
146
- number_as_string = number_as_string.zfill(zfill)
147
-
148
- hostname = host.replace(expand_string, number_as_string)
149
- yield hostname
150
- else:
151
- yield host
152
-
153
-
154
- def parse_inventory_tree(inventory_tree, host_to_groups=dict(), group_stack=set()):
155
- for group in inventory_tree:
156
- # set logic adds tolerance for duplicate group names
157
- groups = group_stack.union({group})
158
-
159
- if "hosts" in inventory_tree[group]:
160
- for host in inventory_tree[group]["hosts"]:
161
- append_groups_to_host(host, groups, host_to_groups)
162
-
163
- if "children" in inventory_tree[group]:
164
- # recursively parse inventory tree
165
- parse_inventory_tree(inventory_tree[group]["children"], host_to_groups, groups)
166
-
167
- return host_to_groups
168
-
169
-
170
- def append_groups_to_host(host: "Host", groups, host_to_groups):
171
- if host in host_to_groups:
172
- # set logic handles de-duplication
173
- host_to_groups[host] = host_to_groups[host].union(groups)
174
- else:
175
- host_to_groups[host] = groups
@@ -1,189 +0,0 @@
1
- """
2
- The ``@mech`` connector reads the current mech status and generates an inventory
3
- for any running VMs.
4
-
5
- .. code:: python
6
-
7
- # Run on all hosts
8
- pyinfra @mech ...
9
-
10
- # Run on a specific VM
11
- pyinfra @mech/my-vm-name ...
12
-
13
- # Run on multiple named VMs
14
- pyinfra @mech/my-vm-name,@mech/another-vm-name ...
15
- """
16
-
17
- import json
18
- from os import path
19
- from queue import Queue
20
- from threading import Thread
21
-
22
- from pyinfra import local, logger
23
- from pyinfra.api.exceptions import InventoryError
24
- from pyinfra.api.util import memoize
25
- from pyinfra.progress import progress_spinner
26
-
27
- from .base import BaseConnector
28
-
29
-
30
- def _get_mech_ssh_config(queue, progress, target):
31
- logger.debug("Loading SSH config for %s", target)
32
-
33
- # Note: We have to work-around the fact that "mech ssh-config somehost"
34
- # does not return the correct "Host" value. When "mech" fixes this
35
- # issue we can simply this code.
36
- lines = local.shell(
37
- "mech ssh-config {0}".format(target),
38
- splitlines=True,
39
- )
40
-
41
- newlines = []
42
- for line in lines:
43
- if line.startswith("Host "):
44
- newlines.append("Host " + target)
45
- else:
46
- newlines.append(line)
47
-
48
- queue.put(newlines)
49
-
50
- progress(target)
51
-
52
-
53
- @memoize
54
- def get_mech_config(limit=None):
55
- logger.info("Getting Mech config...")
56
-
57
- if limit and not isinstance(limit, (list, tuple)):
58
- limit = [limit]
59
-
60
- # Note: There is no "--machine-readable" option to 'mech status'
61
- with progress_spinner({"mech ls"}) as progress:
62
- output = local.shell(
63
- "mech ls",
64
- splitlines=True,
65
- )
66
- progress("mech ls")
67
-
68
- targets = []
69
-
70
- for line in output:
71
- address = ""
72
-
73
- data = line.split()
74
- target = data[0]
75
-
76
- if len(data) == 5:
77
- address = data[1]
78
-
79
- # Skip anything not in the limit
80
- if limit is not None and target not in limit:
81
- continue
82
-
83
- # For each vm that has an address, fetch it's SSH config in a thread
84
- if address != "" and address[0].isdigit():
85
- targets.append(target)
86
-
87
- threads = []
88
- config_queue = Queue() # type: ignore
89
-
90
- with progress_spinner(targets) as progress:
91
- for target in targets:
92
- thread = Thread(
93
- target=_get_mech_ssh_config,
94
- args=(config_queue, progress, target),
95
- )
96
- threads.append(thread)
97
- thread.start()
98
-
99
- for thread in threads:
100
- thread.join()
101
-
102
- queue_items = list(config_queue.queue)
103
-
104
- lines = []
105
- for output in queue_items:
106
- lines.extend(output)
107
-
108
- return lines
109
-
110
-
111
- @memoize
112
- def get_mech_options():
113
- if path.exists("@mech.json"):
114
- with open("@mech.json", "r", encoding="utf-8") as f:
115
- return json.loads(f.read())
116
- return {}
117
-
118
-
119
- def _make_name_data(host):
120
- mech_options = get_mech_options()
121
- mech_host = host["Host"]
122
-
123
- data = {
124
- "ssh_hostname": host["HostName"],
125
- }
126
-
127
- for config_key, data_key in (
128
- ("Port", "ssh_port"),
129
- ("User", "ssh_user"),
130
- ("IdentityFile", "ssh_key"),
131
- ):
132
- if config_key in host:
133
- data[data_key] = host[config_key]
134
-
135
- # Update any configured JSON data
136
- if mech_host in mech_options.get("data", {}):
137
- data.update(mech_options["data"][mech_host])
138
-
139
- # Work out groups
140
- groups = mech_options.get("groups", {}).get(mech_host, [])
141
-
142
- if "@mech" not in groups:
143
- groups.append("@mech")
144
-
145
- return "@mech/{0}".format(host["Host"]), data, groups
146
-
147
-
148
- class MechInventoryConnector(BaseConnector):
149
- @staticmethod
150
- def make_names_data(limit=None):
151
- mech_ssh_info = get_mech_config(limit)
152
-
153
- logger.debug("Got Mech SSH info: \n%s", mech_ssh_info)
154
-
155
- hosts = []
156
- current_host = None
157
-
158
- for line in mech_ssh_info:
159
- if not line:
160
- if current_host:
161
- hosts.append(_make_name_data(current_host))
162
-
163
- current_host = None
164
- continue
165
-
166
- key, value = line.strip().split(" ", 1)
167
-
168
- if key == "Host":
169
- if current_host:
170
- hosts.append(_make_name_data(current_host))
171
-
172
- # Set the new host
173
- current_host = {
174
- key: value,
175
- }
176
-
177
- elif current_host:
178
- current_host[key] = value
179
-
180
- else:
181
- logger.debug("Extra Mech SSH key/value (%s=%s)", key, value)
182
-
183
- if current_host:
184
- hosts.append(_make_name_data(current_host))
185
-
186
- if not hosts:
187
- raise InventoryError("No running Mech instances found!")
188
-
189
- return hosts
@@ -1,28 +0,0 @@
1
- import base64
2
-
3
- import winrm
4
-
5
-
6
- class PyinfraWinrmSession(winrm.Session):
7
- """This is our subclassed Session that allows for env setting"""
8
-
9
- def run_cmd(self, command, args=(), env=None):
10
- shell_id = self.protocol.open_shell(env_vars=env)
11
- command_id = self.protocol.run_command(shell_id, command, args)
12
- rs = winrm.Response(self.protocol.get_command_output(shell_id, command_id))
13
- self.protocol.cleanup_command(shell_id, command_id)
14
- self.protocol.close_shell(shell_id)
15
- return rs
16
-
17
- def run_ps(self, script, env=None):
18
- """base64 encodes a Powershell script and executes the powershell
19
- encoded script command
20
- """
21
- # must use utf16 little endian on windows
22
- encoded_ps = base64.b64encode(script.encode("utf_16_le")).decode("ascii")
23
- rs = self.run_cmd("powershell -encodedcommand {0}".format(encoded_ps), env=env)
24
- if len(rs.std_err):
25
- # if there was an error message, clean it it up and make it human
26
- # readable
27
- rs.std_err = self._clean_error_msg(rs.std_err)
28
- return rs
@@ -1,312 +0,0 @@
1
- """
2
- .. warning::
3
- This connector is in alpha and may change in future releases.
4
-
5
- Some Windows facts and Windows operations work but this is to be considered
6
- experimental. For now, only ``winrm_username`` and ``winrm_password`` is
7
- being used. There are other methods for authentication, but they have not yet
8
- been added/experimented with.
9
-
10
- The ``@winrm`` connector can be used to communicate with Windows instances that have WinRM enabled.
11
-
12
- Examples using ``@winrm``:
13
-
14
- .. code:: python
15
-
16
- # Get the windows_home fact
17
- pyinfra @winrm/192.168.3.232 --winrm-username vagrant \\
18
- --winrm-password vagrant --winrm-port 5985 -vv --debug fact windows_home
19
-
20
- # Create a directory
21
- pyinfra @winrm/192.168.3.232 --winrm-username vagrant \\
22
- --winrm-password vagrant --winrm-port 5985 windows_files.windows_directory 'c:\temp'
23
-
24
- # Run a powershell command ('ps' is the default shell-executable for the winrm connector)
25
- pyinfra @winrm/192.168.3.232 --winrm-username vagrant \\
26
- --winrm-password vagrant --winrm-port 5985 exec -- write-host hello
27
-
28
- # Run a command using the command prompt:
29
- pyinfra @winrm/192.168.3.232 --winrm-username vagrant \\
30
- --winrm-password vagrant --winrm-port 5985 --shell-executable cmd exec -- date /T
31
-
32
- # Run a command using the winrm ntlm transport
33
- pyinfra @winrm/192.168.3.232 --winrm-username vagrant \\
34
- --winrm-password vagrant --winrm-port 5985 --winrm-transport ntlm exec -- hostname
35
- """
36
-
37
- import base64
38
- import ntpath
39
-
40
- import click
41
-
42
- from pyinfra import logger
43
- from pyinfra.api.exceptions import ConnectError, PyinfraError
44
- from pyinfra.api.util import get_file_io, memoize, sha1_hash
45
-
46
- from .base import BaseConnector, make_keys
47
- from .pyinfrawinrmsession import PyinfraWinrmSession
48
- from .util import make_win_command
49
-
50
-
51
- class DataKeys:
52
- hostname = "WinRM hostname to connect to"
53
- port = "WinRM port to connect to"
54
- user = "WinRM username"
55
- password = "WinRM password"
56
- transport = "WinRM transport (default: ``plaintext``)"
57
- read_timeout_sec = "Read timeout in seconds (default: ``30``)"
58
- operation_timeout_sec = "Operation timeout in seconds (default: ``20``)"
59
-
60
-
61
- DATA_KEYS = make_keys("winrm", DataKeys)
62
-
63
-
64
- def _raise_connect_error(host, message, data):
65
- message = "{0} ({1})".format(message, data)
66
- raise ConnectError(message)
67
-
68
-
69
- @memoize
70
- def show_warning():
71
- logger.warning("The @winrm connector is alpha!")
72
-
73
-
74
- def _make_winrm_kwargs(state, host):
75
- kwargs = {}
76
-
77
- for key, value in (
78
- ("username", host.data.get(DATA_KEYS.user)),
79
- ("password", host.data.get(DATA_KEYS.password)),
80
- ("winrm_port", int(host.data.get(DATA_KEYS.port, 0))),
81
- ("winrm_transport", host.data.get(DATA_KEYS.transport, "plaintext")),
82
- (
83
- "winrm_read_timeout_sec",
84
- host.data.get(DATA_KEYS.read_timeout_sec, 30),
85
- ),
86
- (
87
- "winrm_operation_timeout_sec",
88
- host.data.get(DATA_KEYS.operation_timeout_sec, 20),
89
- ),
90
- ):
91
- if value:
92
- kwargs[key] = value
93
-
94
- # FUTURE: add more auth
95
- # pywinrm supports: basic, certificate, ntlm, kerberos, plaintext, ssl, credssp
96
- # see https://github.com/diyan/pywinrm/blob/master/winrm/__init__.py#L12
97
-
98
- return kwargs
99
-
100
-
101
- class WinRMConnector(BaseConnector):
102
- @staticmethod
103
- def make_names_data(hostname):
104
- show_warning()
105
-
106
- yield "@winrm/{0}".format(hostname), {"winrm_hostname": hostname}, []
107
-
108
- def connect(self):
109
- """
110
- Connect to a single host. Returns the winrm Session if successful.
111
- """
112
-
113
- kwargs = _make_winrm_kwargs(self.state, self.host)
114
- logger.debug("Connecting to: %s (%s)", self.host.name, kwargs)
115
-
116
- # Hostname can be provided via winrm config (alias), data, or the hosts name
117
- hostname = kwargs.pop(
118
- "hostname",
119
- self.host.data.get(DATA_KEYS.hostname, self.host.name),
120
- )
121
-
122
- try:
123
- # Create new session
124
- host_and_port = "{}:{}".format(hostname, self.host.data.get(DATA_KEYS.port))
125
- logger.debug("host_and_port: %s", host_and_port)
126
-
127
- session = PyinfraWinrmSession(
128
- host_and_port,
129
- auth=(
130
- kwargs["username"],
131
- kwargs["password"],
132
- ),
133
- transport=kwargs["winrm_transport"],
134
- read_timeout_sec=kwargs["winrm_read_timeout_sec"],
135
- operation_timeout_sec=kwargs["winrm_operation_timeout_sec"],
136
- )
137
-
138
- return session
139
-
140
- # TODO: add exceptions here
141
- except Exception as e:
142
- auth_kwargs = {}
143
-
144
- for key, value in kwargs.items():
145
- if key in ("username", "password"):
146
- auth_kwargs[key] = value
147
-
148
- auth_args = ", ".join(
149
- "{0}={1}".format(key, value) for key, value in auth_kwargs.items()
150
- )
151
- logger.debug("%s", e)
152
- _raise_connect_error(self.host, "Authentication error", auth_args)
153
-
154
- def run_shell_command(
155
- self,
156
- command,
157
- env=None,
158
- success_exit_codes=None,
159
- print_output=False,
160
- print_input=False,
161
- return_combined_output=False,
162
- shell_executable=None,
163
- **ignored_command_kwargs,
164
- ):
165
- """
166
- Execute a command on the specified host.
167
-
168
- Args:
169
- state (``pyinfra.api.State`` obj): state object for this command
170
- hostname (string): hostname of the target
171
- command (string): actual command to execute
172
- success_exit_codes (list): all values in the list that will return success
173
- print_output (boolean): print the output
174
- print_intput (boolean): print the input
175
- return_combined_output (boolean): combine the stdout and stderr lists
176
- shell_executable (string): shell to use - 'cmd'=cmd, 'ps'=powershell(default)
177
- env (dict): environment variables to set
178
-
179
- Returns:
180
- tuple: (exit_code, stdout, stderr)
181
- stdout and stderr are both lists of strings from each buffer.
182
- """
183
-
184
- command = make_win_command(command)
185
-
186
- logger.debug("Running command on %s: %s", self.host.name, command)
187
-
188
- if print_input:
189
- click.echo("{0}>>> {1}".format(self.host.print_prefix, command), err=True)
190
-
191
- # get rid of leading/trailing quote
192
- tmp_command = command.strip("'")
193
-
194
- if print_output:
195
- click.echo(
196
- "{0}>>> {1}".format(self.host.print_prefix, command),
197
- err=True,
198
- )
199
-
200
- if not shell_executable:
201
- shell_executable = "ps"
202
- logger.debug("shell_executable:%s", shell_executable)
203
-
204
- # we use our own subclassed session that allows for env setting from open_shell.
205
- if shell_executable in ["cmd"]:
206
- response = self.host.connection.run_cmd(tmp_command, env=env) # type: ignore
207
- else:
208
- response = self.host.connection.run_ps(tmp_command, env=env) # type: ignore
209
-
210
- return_code = response.status_code
211
- logger.debug("response:%s", response)
212
-
213
- std_out_str = response.std_out.decode("utf-8")
214
- std_err_str = response.std_err.decode("utf-8")
215
-
216
- # split on '\r\n' (windows newlines)
217
- std_out = std_out_str.split("\r\n")
218
- std_err = std_err_str.split("\r\n")
219
-
220
- logger.debug("std_out:%s", std_out)
221
- logger.debug("std_err:%s", std_err)
222
-
223
- if print_output:
224
- click.echo(
225
- "{0}>>> {1}".format(self.host.print_prefix, "\n".join(std_out)),
226
- err=True,
227
- )
228
-
229
- if success_exit_codes:
230
- status = return_code in success_exit_codes
231
- else:
232
- status = return_code == 0
233
-
234
- logger.debug("Command exit status: %s", status)
235
-
236
- if return_combined_output:
237
- std_out = [("stdout", line) for line in std_out]
238
- std_err = [("stderr", line) for line in std_err]
239
- return status, std_out + std_err
240
-
241
- return status, std_out, std_err
242
-
243
- def get_file(
244
- state, host, remote_filename, filename_or_io, remote_temp_filename=None, **command_kwargs
245
- ):
246
- raise PyinfraError("Not implemented")
247
-
248
- def _put_file(self, filename_or_io, remote_location, chunk_size=2048):
249
- # this should work fine on smallish files, but there will be perf issues
250
- # on larger files both due to the full read, the base64 encoding, and
251
- # the latency when sending chunks
252
- with get_file_io(filename_or_io) as file_io:
253
- data = file_io.read()
254
- for i in range(0, len(data), chunk_size):
255
- chunk = data[i : i + chunk_size]
256
- ps = (
257
- '$data = [System.Convert]::FromBase64String("{0}"); '
258
- '{1} -Value $data -Encoding byte -Path "{2}"'
259
- ).format(
260
- base64.b64encode(chunk).decode("utf-8"),
261
- "Set-Content" if i == 0 else "Add-Content",
262
- remote_location,
263
- )
264
- status, _stdout, stderr = self.run_shell_command(ps)
265
- if status is False:
266
- logger.error("File upload error: {0}".format("\n".join(stderr)))
267
- return False
268
-
269
- return True
270
-
271
- def put_file(
272
- self,
273
- filename_or_io,
274
- remote_filename,
275
- print_output=False,
276
- print_input=False,
277
- remote_temp_filename=None, # ignored
278
- **command_kwargs,
279
- ):
280
- """
281
- Upload file by chunking and sending base64 encoded via winrm
282
- """
283
-
284
- # TODO: fix this? Workaround for circular import
285
- from pyinfra.facts.windows_files import TempDir
286
-
287
- # Always use temp file here in case of failure
288
- temp_file = ntpath.join(
289
- self.host.get_fact(TempDir),
290
- "pyinfra-{0}".format(sha1_hash(remote_filename)),
291
- )
292
-
293
- if not self._put_file(filename_or_io, temp_file):
294
- return False
295
-
296
- # Execute run_shell_command w/sudo and/or su_user
297
- command = "Move-Item -Path {0} -Destination {1} -Force".format(temp_file, remote_filename)
298
- status, _, stderr = self.run_shell_command(
299
- command, print_output=print_output, print_input=print_input, **command_kwargs
300
- )
301
-
302
- if status is False:
303
- logger.error("File upload error: {0}".format("\n".join(stderr)))
304
- return False
305
-
306
- if print_output:
307
- click.echo(
308
- "{0}file uploaded: {1}".format(self.host.print_prefix, remote_filename),
309
- err=True,
310
- )
311
-
312
- return True