pyinfra 2.9.2__py2.py3-none-any.whl → 3.0b1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +261 -255
  3. pyinfra/api/arguments_typed.py +77 -0
  4. pyinfra/api/command.py +66 -53
  5. pyinfra/api/config.py +27 -22
  6. pyinfra/api/connect.py +1 -1
  7. pyinfra/api/connectors.py +2 -24
  8. pyinfra/api/deploy.py +21 -52
  9. pyinfra/api/exceptions.py +33 -8
  10. pyinfra/api/facts.py +77 -113
  11. pyinfra/api/host.py +150 -82
  12. pyinfra/api/inventory.py +17 -25
  13. pyinfra/api/operation.py +232 -198
  14. pyinfra/api/operations.py +102 -148
  15. pyinfra/api/state.py +137 -79
  16. pyinfra/api/util.py +55 -70
  17. pyinfra/connectors/base.py +150 -0
  18. pyinfra/connectors/chroot.py +160 -169
  19. pyinfra/connectors/docker.py +227 -237
  20. pyinfra/connectors/dockerssh.py +231 -253
  21. pyinfra/connectors/local.py +195 -207
  22. pyinfra/connectors/ssh.py +528 -615
  23. pyinfra/connectors/ssh_util.py +114 -0
  24. pyinfra/connectors/sshuserclient/client.py +5 -3
  25. pyinfra/connectors/terraform.py +86 -65
  26. pyinfra/connectors/util.py +212 -137
  27. pyinfra/connectors/vagrant.py +55 -48
  28. pyinfra/context.py +3 -2
  29. pyinfra/facts/docker.py +1 -0
  30. pyinfra/facts/files.py +45 -32
  31. pyinfra/facts/git.py +3 -1
  32. pyinfra/facts/gpg.py +1 -1
  33. pyinfra/facts/hardware.py +4 -2
  34. pyinfra/facts/iptables.py +5 -3
  35. pyinfra/facts/mysql.py +1 -0
  36. pyinfra/facts/postgres.py +168 -0
  37. pyinfra/facts/postgresql.py +5 -161
  38. pyinfra/facts/selinux.py +3 -1
  39. pyinfra/facts/server.py +77 -30
  40. pyinfra/facts/systemd.py +29 -12
  41. pyinfra/facts/sysvinit.py +10 -10
  42. pyinfra/facts/util/packaging.py +4 -2
  43. pyinfra/local.py +4 -5
  44. pyinfra/operations/apk.py +3 -3
  45. pyinfra/operations/apt.py +25 -47
  46. pyinfra/operations/brew.py +7 -14
  47. pyinfra/operations/bsdinit.py +4 -4
  48. pyinfra/operations/cargo.py +1 -1
  49. pyinfra/operations/choco.py +1 -1
  50. pyinfra/operations/dnf.py +4 -4
  51. pyinfra/operations/files.py +108 -321
  52. pyinfra/operations/gem.py +1 -1
  53. pyinfra/operations/git.py +6 -37
  54. pyinfra/operations/iptables.py +2 -10
  55. pyinfra/operations/launchd.py +1 -1
  56. pyinfra/operations/lxd.py +1 -9
  57. pyinfra/operations/mysql.py +5 -28
  58. pyinfra/operations/npm.py +1 -1
  59. pyinfra/operations/openrc.py +1 -1
  60. pyinfra/operations/pacman.py +3 -3
  61. pyinfra/operations/pip.py +14 -15
  62. pyinfra/operations/pkg.py +1 -1
  63. pyinfra/operations/pkgin.py +3 -3
  64. pyinfra/operations/postgres.py +347 -0
  65. pyinfra/operations/postgresql.py +17 -380
  66. pyinfra/operations/python.py +2 -17
  67. pyinfra/operations/selinux.py +5 -28
  68. pyinfra/operations/server.py +59 -84
  69. pyinfra/operations/snap.py +1 -3
  70. pyinfra/operations/ssh.py +8 -23
  71. pyinfra/operations/systemd.py +7 -7
  72. pyinfra/operations/sysvinit.py +3 -12
  73. pyinfra/operations/upstart.py +4 -4
  74. pyinfra/operations/util/__init__.py +12 -0
  75. pyinfra/operations/util/files.py +2 -2
  76. pyinfra/operations/util/packaging.py +6 -24
  77. pyinfra/operations/util/service.py +18 -37
  78. pyinfra/operations/vzctl.py +2 -2
  79. pyinfra/operations/xbps.py +3 -3
  80. pyinfra/operations/yum.py +4 -4
  81. pyinfra/operations/zypper.py +4 -4
  82. {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/METADATA +19 -22
  83. pyinfra-3.0b1.dist-info/RECORD +163 -0
  84. pyinfra-3.0b1.dist-info/entry_points.txt +11 -0
  85. pyinfra_cli/__main__.py +2 -0
  86. pyinfra_cli/commands.py +7 -2
  87. pyinfra_cli/exceptions.py +83 -42
  88. pyinfra_cli/inventory.py +19 -4
  89. pyinfra_cli/log.py +17 -3
  90. pyinfra_cli/main.py +133 -90
  91. pyinfra_cli/prints.py +93 -129
  92. pyinfra_cli/util.py +60 -29
  93. tests/test_api/test_api.py +2 -0
  94. tests/test_api/test_api_arguments.py +13 -13
  95. tests/test_api/test_api_deploys.py +28 -29
  96. tests/test_api/test_api_facts.py +60 -98
  97. tests/test_api/test_api_operations.py +100 -200
  98. tests/test_cli/test_cli.py +18 -49
  99. tests/test_cli/test_cli_deploy.py +11 -37
  100. tests/test_cli/test_cli_exceptions.py +50 -19
  101. tests/test_cli/util.py +1 -1
  102. tests/test_connectors/test_chroot.py +6 -6
  103. tests/test_connectors/test_docker.py +4 -4
  104. tests/test_connectors/test_dockerssh.py +38 -50
  105. tests/test_connectors/test_local.py +11 -12
  106. tests/test_connectors/test_ssh.py +66 -107
  107. tests/test_connectors/test_terraform.py +9 -15
  108. tests/test_connectors/test_util.py +24 -46
  109. tests/test_connectors/test_vagrant.py +4 -4
  110. pyinfra/api/operation.pyi +0 -117
  111. pyinfra/connectors/ansible.py +0 -171
  112. pyinfra/connectors/mech.py +0 -186
  113. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  114. pyinfra/connectors/winrm.py +0 -320
  115. pyinfra/facts/windows.py +0 -366
  116. pyinfra/facts/windows_files.py +0 -90
  117. pyinfra/operations/windows.py +0 -59
  118. pyinfra/operations/windows_files.py +0 -551
  119. pyinfra-2.9.2.dist-info/RECORD +0 -170
  120. pyinfra-2.9.2.dist-info/entry_points.txt +0 -14
  121. tests/test_connectors/test_ansible.py +0 -64
  122. tests/test_connectors/test_mech.py +0 -126
  123. tests/test_connectors/test_winrm.py +0 -76
  124. {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/LICENSE.md +0 -0
  125. {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/WHEEL +0 -0
  126. {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/top_level.txt +0 -0
@@ -1,313 +1,291 @@
1
- """
2
- **Note**: this connector is in beta!
3
-
4
- The ``@dockerssh`` connector allows you to run commands on Docker containers on a remote machine.
5
-
6
- .. code:: shell
7
-
8
- # A Docker base image must be provided
9
- pyinfra @dockerssh/remotehost:alpine:3.8 ...
10
-
11
- # pyinfra can run on multiple Docker images in parallel
12
- pyinfra @dockerssh/remotehost:alpine:3.8,@dockerssh/remotehost:ubuntu:bionic ...
13
- """
14
-
15
1
  import os
16
2
  from tempfile import mkstemp
17
3
  from typing import TYPE_CHECKING
18
4
 
19
5
  import click
6
+ from typing_extensions import Unpack
20
7
 
21
8
  from pyinfra import logger
22
9
  from pyinfra.api import QuoteString, StringCommand
23
- from pyinfra.api.connectors import BaseConnectorMeta
24
10
  from pyinfra.api.exceptions import ConnectError, InventoryError, PyinfraError
25
11
  from pyinfra.api.util import get_file_io, memoize
26
12
  from pyinfra.progress import progress_spinner
27
13
 
28
- from . import ssh
29
- from .util import make_unix_command_for_host
14
+ from .base import BaseConnector
15
+ from .ssh import SSHConnector
16
+ from .util import extract_control_arguments, make_unix_command_for_host
30
17
 
31
18
  if TYPE_CHECKING:
19
+ from pyinfra.api.arguments import ConnectorArguments
32
20
  from pyinfra.api.host import Host
33
21
  from pyinfra.api.state import State
34
22
 
35
23
 
36
- class Meta(BaseConnectorMeta):
37
- handles_execution = True
38
-
39
-
40
- def remote_remove(
41
- state: "State", host: "Host", filename, print_output: bool = False, print_input: bool = False
42
- ):
43
- """
44
- Deletes a file on a remote machine over ssh.
45
- """
46
- remove_status, _, remove_stderr = ssh.run_shell_command(
47
- state,
48
- host,
49
- "rm -f {0}".format(filename),
50
- print_output=print_output,
51
- print_input=print_input,
52
- )
53
-
54
- if not remove_status:
55
- raise IOError("\n".join(remove_stderr))
56
-
57
-
58
24
  @memoize
59
25
  def show_warning():
60
26
  logger.warning("The @dockerssh connector is in beta!")
61
27
 
62
28
 
63
- def make_names_data(host_image_str):
64
- try:
65
- hostname, image = host_image_str.split(":", 1)
66
- except (AttributeError, ValueError): # failure to parse the host_image_str
67
- raise InventoryError("No ssh host or docker base image provided!")
29
+ class DockerSSHConnector(BaseConnector):
30
+ """
31
+ **Note**: this connector is in beta!
68
32
 
69
- if not image:
70
- raise InventoryError("No docker base image provided!")
33
+ The ``@dockerssh`` connector allows you to run commands on Docker containers \
34
+ on a remote machine.
71
35
 
72
- show_warning()
36
+ .. code:: shell
73
37
 
74
- yield (
75
- "@dockerssh/{0}:{1}".format(hostname, image),
76
- {"ssh_hostname": hostname, "docker_image": image},
77
- ["@dockerssh"],
78
- )
38
+ # A Docker base image must be provided
39
+ pyinfra @dockerssh/remotehost:alpine:3.8 ...
79
40
 
41
+ # pyinfra can run on multiple Docker images in parallel
42
+ pyinfra @dockerssh/remotehost:alpine:3.8,@dockerssh/remotehost:ubuntu:bionic ...
43
+ """
80
44
 
81
- def connect(state: "State", host: "Host"):
82
- if not host.connection:
83
- host.connection = ssh.connect(state, host)
45
+ handles_execution = True
84
46
 
85
- if "docker_container_id" in host.host_data: # user can provide a docker_container_id
86
- return host.connection
47
+ ssh: SSHConnector
87
48
 
88
- try:
89
- with progress_spinner({"docker run"}):
90
- # last line is the container ID
91
- status, stdout, stderr = ssh.run_shell_command(
92
- state,
93
- host,
94
- "docker run -d {0} tail -f /dev/null".format(host.data.docker_image),
95
- )
96
- if not status:
97
- raise IOError("\n".join(stderr))
98
- container_id = stdout[-1]
49
+ def __init__(self, state: "State", host: "Host"):
50
+ super().__init__(state, host)
51
+ self.ssh = SSHConnector(state, host)
99
52
 
100
- except PyinfraError as e:
101
- host.connection = None # fail connection
102
- raise ConnectError(e.args[0])
53
+ @staticmethod
54
+ def make_names_data(name):
55
+ try:
56
+ hostname, image = name.split(":", 1)
57
+ except (AttributeError, ValueError): # failure to parse the name
58
+ raise InventoryError("No ssh host or docker base image provided!")
103
59
 
104
- host.host_data["docker_container_id"] = container_id
105
- return host.connection
60
+ if not image:
61
+ raise InventoryError("No docker base image provided!")
106
62
 
63
+ show_warning()
107
64
 
108
- def disconnect(state: "State", host: "Host"):
109
- container_id = host.host_data["docker_container_id"][:12]
65
+ yield (
66
+ "@dockerssh/{0}:{1}".format(hostname, image),
67
+ {"ssh_hostname": hostname, "docker_image": image},
68
+ ["@dockerssh"],
69
+ )
110
70
 
111
- with progress_spinner({"docker commit"}):
112
- image_id = ssh.run_shell_command(state, host, "docker commit {0}".format(container_id))[1][
113
- -1
114
- ][
115
- 7:19
116
- ] # last line is the image ID, get sha256:[XXXXXXXXXX]...
71
+ def connect(self) -> None:
72
+ self.ssh.connect()
73
+
74
+ if "docker_container_id" in self.host.host_data: # user can provide a docker_container_id
75
+ return
76
+
77
+ try:
78
+ with progress_spinner({"docker run"}):
79
+ # last line is the container ID
80
+ status, output = self.ssh.run_shell_command(
81
+ StringCommand(
82
+ "docker",
83
+ "run",
84
+ "-d",
85
+ self.host.data.docker_image,
86
+ "tail",
87
+ "-f",
88
+ "/dev/null",
89
+ ),
90
+ )
91
+ if not status:
92
+ raise IOError(output.stderr)
93
+ container_id = output.stdout_lines[-1]
94
+
95
+ except PyinfraError as e:
96
+ raise ConnectError(e.args[0])
97
+
98
+ self.host.host_data["docker_container_id"] = container_id
99
+
100
+ def disconnect(self) -> None:
101
+ container_id = self.host.host_data["docker_container_id"][:12]
102
+
103
+ with progress_spinner({"docker commit"}):
104
+ _, output = self.ssh.run_shell_command(StringCommand("docker", "commit", container_id))
105
+
106
+ # Last line is the image ID, get sha256:[XXXXXXXXXX]...
107
+ image_id = output.stdout_lines[-1][7:19]
108
+
109
+ with progress_spinner({"docker rm"}):
110
+ self.ssh.run_shell_command(
111
+ StringCommand("docker", "rm", "-f", container_id),
112
+ )
117
113
 
118
- with progress_spinner({"docker rm"}):
119
- ssh.run_shell_command(
120
- state,
121
- host,
122
- "docker rm -f {0}".format(container_id),
114
+ logger.info(
115
+ "{0}docker build complete, image ID: {1}".format(
116
+ self.host.print_prefix,
117
+ click.style(image_id, bold=True),
118
+ ),
123
119
  )
124
120
 
125
- logger.info(
126
- "{0}docker build complete, image ID: {1}".format(
127
- host.print_prefix,
128
- click.style(image_id, bold=True),
129
- ),
130
- )
131
-
132
-
133
- def run_shell_command(
134
- state: "State",
135
- host: "Host",
136
- command,
137
- get_pty: bool = False,
138
- timeout=None,
139
- stdin=None,
140
- success_exit_codes=None,
141
- print_output: bool = False,
142
- print_input: bool = False,
143
- return_combined_output=False,
144
- **command_kwargs,
145
- ):
146
- container_id = host.host_data["docker_container_id"]
147
-
148
- # Don't sudo/su in Docker - is this the right thing to do? Makes deploys that
149
- # target SSH systems work w/Docker out of the box (ie most docker commands
150
- # are run as root).
151
- for key in ("sudo", "su_user"):
152
- command_kwargs.pop(key, None)
153
-
154
- command = make_unix_command_for_host(state, host, command, **command_kwargs)
155
- command = QuoteString(command)
156
-
157
- docker_flags = "-it" if get_pty else "-i"
158
- docker_command = StringCommand(
159
- "docker",
160
- "exec",
161
- docker_flags,
162
- container_id,
163
- "sh",
164
- "-c",
121
+ def run_shell_command(
122
+ self,
165
123
  command,
166
- )
167
-
168
- return ssh.run_shell_command(
169
- state,
170
- host,
171
- docker_command,
172
- timeout=timeout,
173
- stdin=stdin,
174
- success_exit_codes=success_exit_codes,
175
- print_output=print_output,
176
- print_input=print_input,
177
- return_combined_output=return_combined_output,
178
- )
179
-
180
-
181
- def put_file(
182
- state: "State",
183
- host: "Host",
184
- filename_or_io,
185
- remote_filename,
186
- remote_temp_filename=None,
187
- print_output: bool = False,
188
- print_input: bool = False,
189
- **kwargs, # ignored (sudo/etc)
190
- ):
191
- """
192
- Upload a file/IO object to the target Docker container by copying it to a
193
- temporary location and then uploading it into the container using ``docker cp``.
194
- """
195
-
196
- fd, local_temp_filename = mkstemp()
197
- remote_temp_filename = remote_temp_filename or state.get_temp_filename(local_temp_filename)
198
-
199
- # Load our file or IO object and write it to the temporary file
200
- with get_file_io(filename_or_io) as file_io:
201
- with open(local_temp_filename, "wb") as temp_f:
202
- data = file_io.read()
203
-
204
- if isinstance(data, str):
205
- data = data.encode()
206
-
207
- temp_f.write(data)
208
-
209
- # upload file to remote server
210
- ssh_status = ssh.put_file(state, host, local_temp_filename, remote_temp_filename)
211
- if not ssh_status:
212
- raise IOError("Failed to copy file over ssh")
213
-
214
- try:
215
- docker_id = host.host_data["docker_container_id"]
216
- docker_command = "docker cp {0} {1}:{2}".format(
217
- remote_temp_filename,
218
- docker_id,
219
- remote_filename,
124
+ print_output: bool = False,
125
+ print_input: bool = False,
126
+ **arguments: Unpack["ConnectorArguments"],
127
+ ):
128
+ local_arguments = extract_control_arguments(arguments)
129
+
130
+ container_id = self.host.host_data["docker_container_id"]
131
+
132
+ command = make_unix_command_for_host(self.state, self.host, command, **arguments)
133
+ command = QuoteString(command)
134
+
135
+ docker_flags = "-it" if local_arguments.get("_get_pty") else "-i"
136
+ docker_command = StringCommand(
137
+ "docker",
138
+ "exec",
139
+ docker_flags,
140
+ container_id,
141
+ "sh",
142
+ "-c",
143
+ command,
220
144
  )
221
145
 
222
- status, _, stderr = ssh.run_shell_command(
223
- state,
224
- host,
146
+ return self.ssh.run_shell_command(
225
147
  docker_command,
226
148
  print_output=print_output,
227
149
  print_input=print_input,
150
+ **local_arguments,
228
151
  )
229
- finally:
230
- os.close(fd)
231
- os.remove(local_temp_filename)
232
- remote_remove(
233
- state,
234
- host,
235
- local_temp_filename,
236
- print_output=print_output,
237
- print_input=print_input,
152
+
153
+ def put_file(
154
+ self,
155
+ filename_or_io,
156
+ remote_filename,
157
+ remote_temp_filename=None,
158
+ print_output: bool = False,
159
+ print_input: bool = False,
160
+ **kwargs, # ignored (sudo/etc)
161
+ ):
162
+ """
163
+ Upload a file/IO object to the target Docker container by copying it to a
164
+ temporary location and then uploading it into the container using ``docker cp``.
165
+ """
166
+
167
+ fd, local_temp_filename = mkstemp()
168
+ remote_temp_filename = remote_temp_filename or self.host.get_temp_filename(
169
+ local_temp_filename
238
170
  )
239
171
 
240
- if not status:
241
- raise IOError("\n".join(stderr))
172
+ # Load our file or IO object and write it to the temporary file
173
+ with get_file_io(filename_or_io) as file_io:
174
+ with open(local_temp_filename, "wb") as temp_f:
175
+ data = file_io.read()
242
176
 
243
- if print_output:
244
- click.echo(
245
- "{0}file uploaded to container: {1}".format(
246
- host.print_prefix,
247
- remote_filename,
248
- ),
249
- err=True,
250
- )
177
+ if isinstance(data, str):
178
+ data = data.encode()
251
179
 
252
- return status
180
+ temp_f.write(data)
253
181
 
182
+ # upload file to remote server
183
+ ssh_status = self.ssh.put_file(local_temp_filename, remote_temp_filename)
184
+ if not ssh_status:
185
+ raise IOError("Failed to copy file over ssh")
254
186
 
255
- def get_file(
256
- state: "State",
257
- host: "Host",
258
- remote_filename,
259
- filename_or_io,
260
- remote_temp_filename=None,
261
- print_output: bool = False,
262
- print_input: bool = False,
263
- **kwargs, # ignored (sudo/etc)
264
- ):
265
- """
266
- Download a file from the target Docker container by copying it to a temporary
267
- location and then reading that into our final file/IO object.
268
- """
187
+ try:
188
+ docker_id = self.host.host_data["docker_container_id"]
189
+ docker_command = StringCommand(
190
+ "docker",
191
+ "cp",
192
+ remote_temp_filename,
193
+ f"{docker_id}:{remote_filename}",
194
+ )
269
195
 
270
- remote_temp_filename = remote_temp_filename or state.get_temp_filename(remote_filename)
196
+ status, output = self.ssh.run_shell_command(
197
+ docker_command,
198
+ print_output=print_output,
199
+ print_input=print_input,
200
+ )
201
+ finally:
202
+ os.close(fd)
203
+ os.remove(local_temp_filename)
204
+ self.remote_remove(
205
+ local_temp_filename,
206
+ print_output=print_output,
207
+ print_input=print_input,
208
+ )
271
209
 
272
- try:
273
- docker_id = host.host_data["docker_container_id"]
274
- docker_command = "docker cp {0}:{1} {2}".format(
275
- docker_id,
276
- remote_filename,
277
- remote_temp_filename,
278
- )
210
+ if not status:
211
+ raise IOError(output.stderr)
279
212
 
280
- status, _, stderr = ssh.run_shell_command(
281
- state,
282
- host,
283
- docker_command,
284
- print_output=print_output,
285
- print_input=print_input,
286
- )
213
+ if print_output:
214
+ click.echo(
215
+ "{0}file uploaded to container: {1}".format(
216
+ self.host.print_prefix,
217
+ remote_filename,
218
+ ),
219
+ err=True,
220
+ )
287
221
 
288
- ssh_status = ssh.get_file(state, host, remote_temp_filename, filename_or_io)
289
- finally:
290
- remote_remove(
291
- state,
292
- host,
293
- remote_temp_filename,
294
- print_output=print_output,
295
- print_input=print_input,
296
- )
222
+ return status
223
+
224
+ def get_file(
225
+ self,
226
+ remote_filename,
227
+ filename_or_io,
228
+ remote_temp_filename=None,
229
+ print_output: bool = False,
230
+ print_input: bool = False,
231
+ **kwargs, # ignored (sudo/etc)
232
+ ):
233
+ """
234
+ Download a file from the target Docker container by copying it to a temporary
235
+ location and then reading that into our final file/IO object.
236
+ """
237
+
238
+ remote_temp_filename = remote_temp_filename or self.host.get_temp_filename(remote_filename)
239
+
240
+ try:
241
+ docker_id = self.host.host_data["docker_container_id"]
242
+ docker_command = StringCommand(
243
+ "docker",
244
+ "cp",
245
+ f"{docker_id}:{remote_filename}",
246
+ remote_temp_filename,
247
+ )
297
248
 
298
- if not ssh_status:
299
- raise IOError("failed to copy file over ssh")
249
+ status, output = self.ssh.run_shell_command(
250
+ docker_command,
251
+ print_output=print_output,
252
+ print_input=print_input,
253
+ )
300
254
 
301
- if not status:
302
- raise IOError("\n".join(stderr))
255
+ ssh_status = self.ssh.get_file(remote_temp_filename, filename_or_io)
256
+ finally:
257
+ self.remote_remove(
258
+ remote_temp_filename,
259
+ print_output=print_output,
260
+ print_input=print_input,
261
+ )
303
262
 
304
- if print_output:
305
- click.echo(
306
- "{0}file downloaded from container: {1}".format(
307
- host.print_prefix,
308
- remote_filename,
309
- ),
310
- err=True,
263
+ if not ssh_status:
264
+ raise IOError("failed to copy file over ssh")
265
+
266
+ if not status:
267
+ raise IOError(output.stderr)
268
+
269
+ if print_output:
270
+ click.echo(
271
+ "{0}file downloaded from container: {1}".format(
272
+ self.host.print_prefix,
273
+ remote_filename,
274
+ ),
275
+ err=True,
276
+ )
277
+
278
+ return status
279
+
280
+ def remote_remove(self, filename, print_output: bool = False, print_input: bool = False):
281
+ """
282
+ Deletes a file on a remote machine over ssh.
283
+ """
284
+ remove_status, output = self.ssh.run_shell_command(
285
+ StringCommand("rm", "-f", filename),
286
+ print_output=print_output,
287
+ print_input=print_input,
311
288
  )
312
289
 
313
- return status
290
+ if not remove_status:
291
+ raise IOError(output.stderr)