Skip to content

Commit 8ac7dba

Browse files
authored
docs/connectors: document command wrapping and parameter filtering best practices
1 parent da02d47 commit 8ac7dba

File tree

1 file changed

+55
-1
lines changed

1 file changed

+55
-1
lines changed

docs/api/connectors.md

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class MyConnector(BaseConnector):
5656
@staticmethod
5757
def make_names_data(_=None):
5858
... # see above
59-
59+
6060
def run_shell_command(
6161
self,
6262
command: StringCommand,
@@ -191,6 +191,60 @@ screen.
191191
`disconnect` can be used after all operations complete to clean up any connection/s remaining to the hosts being managed.
192192

193193

194+
## Implementing `run_shell_command`
195+
196+
When implementing `run_shell_command`, connectors should use pyinfra's command wrapping utilities rather than manually constructing commands. The `make_unix_command_for_host()` function from `pyinfra.connectors.util` handles shell wrapping, sudo elevation, environment variables, working directory changes, command retries and shell executable selection.
197+
198+
Its worth being aware that when passing `arguments` to `make_unix_command_for_host()`, connector control parameters must be filtered out. These parameters (`_success_exit_codes`, `_timeout`, `_get_pty`, `_stdin`) are defined in `pyinfra.api.arguments.ConnectorArguments` and are meant for the connector's internal logic after command generation, not for command construction itself.
199+
200+
The recommended approach is to use `extract_control_arguments()` from `pyinfra.connectors.util` which handles this filtering for you:
201+
202+
```py
203+
from pyinfra.connectors.util import extract_control_arguments, make_unix_command_for_host
204+
205+
class MyConnector(BaseConnector):
206+
handles_execution = True
207+
208+
def run_shell_command(
209+
self,
210+
command: StringCommand,
211+
print_output: bool = False,
212+
print_input: bool = False,
213+
**arguments: Unpack["ConnectorArguments"],
214+
) -> Tuple[bool, CommandOutput]:
215+
"""Execute a command with proper shell wrapping."""
216+
217+
# Extract and remove control parameters from arguments
218+
# This modifies arguments dict in place and returns the extracted params
219+
control_args = extract_control_arguments(arguments)
220+
221+
# Generate properly wrapped command with sudo, environment, etc
222+
# arguments now contains only command generation parameters
223+
wrapped_command = make_unix_command_for_host(
224+
self.state,
225+
self.host,
226+
command,
227+
**arguments,
228+
)
229+
230+
# Use control parameters for execution
231+
timeout = control_args.get("_timeout")
232+
success_exit_codes = control_args.get("_success_exit_codes", [0])
233+
234+
# Execute the wrapped command using your connector's method
235+
exit_code, output = self._execute(wrapped_command, timeout=timeout)
236+
237+
# Check success based on exit codes
238+
success = exit_code in success_exit_codes
239+
240+
return success, output
241+
```
242+
243+
Without proper command wrapping, shell operators and complex commands will fail. For example `timeout 60 bash -c 'command' || true` executed without shell wrapping will result in `bash: ||: command not found`. PyInfra operations and fact gathering rely on shell operators (`&&`, `||`, pipes, redirects) so using `make_unix_command_for_host()` ensures your connector handles these correctly.
244+
245+
For complete examples see pyinfra's built-in connectors in `pyinfra/connectors/docker.py`, `pyinfra/connectors/chroot.py`, `pyinfra/connectors/ssh.py` and `pyinfra/connectors/local.py`, as well as the command wrapping utilities in `pyinfra/connectors/util.py`.
246+
247+
194248
## pyproject.toml
195249

196250
In order for pyinfra to gain knowledge about your connector, you need to add the following snippet to your connector's `pyproject.toml`:

0 commit comments

Comments
 (0)