Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion docs/api/connectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class MyConnector(BaseConnector):
@staticmethod
def make_names_data(_=None):
... # see above

def run_shell_command(
self,
command: StringCommand,
Expand Down Expand Up @@ -191,6 +191,60 @@ screen.
`disconnect` can be used after all operations complete to clean up any connection/s remaining to the hosts being managed.


## Implementing `run_shell_command`

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.

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.

The recommended approach is to use `extract_control_arguments()` from `pyinfra.connectors.util` which handles this filtering for you:

```py
from pyinfra.connectors.util import extract_control_arguments, make_unix_command_for_host

class MyConnector(BaseConnector):
handles_execution = True

def run_shell_command(
self,
command: StringCommand,
print_output: bool = False,
print_input: bool = False,
**arguments: Unpack["ConnectorArguments"],
) -> Tuple[bool, CommandOutput]:
"""Execute a command with proper shell wrapping."""

# Extract and remove control parameters from arguments
# This modifies arguments dict in place and returns the extracted params
control_args = extract_control_arguments(arguments)

# Generate properly wrapped command with sudo, environment, etc
# arguments now contains only command generation parameters
wrapped_command = make_unix_command_for_host(
self.state,
self.host,
command,
**arguments,
)

# Use control parameters for execution
timeout = control_args.get("_timeout")
success_exit_codes = control_args.get("_success_exit_codes", [0])

# Execute the wrapped command using your connector's method
exit_code, output = self._execute(wrapped_command, timeout=timeout)

# Check success based on exit codes
success = exit_code in success_exit_codes

return success, output
```

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.

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`.


## pyproject.toml

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