-
Notifications
You must be signed in to change notification settings - Fork 49
API Usage
Broker exposes most of the same functionality the CLI provides through a Broker class.
To use this class, simply import:
from broker import BrokerThe Broker class largely accepts the same arguments as you would pass via the CLI. One key difference is that you need to use underscores instead of dashes. For example, a checkout at the CLI that looks like this
broker checkout --nick rhel7 --args-file tests/data/broker_args.json --environment="VAR1=val1,VAR2=val2"
could look like this in an API usage
rhel7_host = Broker(nick="rhel7", args_file="tests/data/broker_args.json", environment={"VAR1": "val1", "VAR2": "val2"}).checkout()Broker will carry out its usual actions and package the resulting host in a Host object. This host object will also include some basic functionality, like the ability to execute SSH commands on the host.
Executed SSH command results are packaged in a Result object containing status (return code), stdout, and stderr.
result = rhel7_host.execute("rpm -qa")
assert result.status == 0
assert "my-package" in result.stdoutThe Broker class has a built-in context manager that automatically performs a checkout upon enter and checkin upon exit. It is the recommended way of interacting with Broker for host management.
In the below two lines of code:
- a container host is created (pulled if needed or applicable)
- a Broker
Hostobject is constructed - the host object runs a command on the container
- output is checked
- the container is checked in
with Broker(container_host="ch-d:rhel7") as container_host:
assert container_host.hostname in container_host.execute("hostname").stdoutPass _count to check out several hosts of the same type in parallel. checkout() returns a list when _count > 1.
hosts = Broker(nick="rhel9", _count=3).checkout()
# hosts is a list of three Host objects
for host in hosts:
print(host.hostname)Return one or more hosts to their provider.
broker_inst = Broker(nick="rhel9")
host = broker_inst.checkout()
# ... do work ...
broker_inst.checkin(host=host)
# or check in all hosts tracked by this Broker instance
broker_inst.checkin()
# checkin sequentially instead of concurrently
broker_inst.checkin(sequential=True)Run a provider action that does not result in a host checkout.
result = Broker(workflow="my-reporting-workflow", extra_arg="value").execute()Extend the lease time for one or more hosts (provider must support it).
broker_inst = Broker(nick="rhel9")
host = broker_inst.checkout()
broker_inst.extend(host=host)Reconstruct hosts from Broker's local inventory, optionally filtered.
broker_inst = Broker()
all_hosts = broker_inst.from_inventory()
# with a filter expression
rhel_hosts = broker_inst.from_inventory(filter='@inv._broker_provider == "AnsibleTower"')Pull the current host list from a provider and update the local inventory.
Broker.sync_inventory("AnsibleTower")
# sync a specific instance
Broker.sync_inventory("AnsibleTower::testing")Check out multiple different host types simultaneously and handle checkin automatically. All checkouts happen in parallel; all checkins happen in parallel on exit.
from broker import Broker
from myproject.hosts import ContentHost
with Broker.multi_manager(
rhel8={"host_class": ContentHost, "workflow": "deploy-base-rhel", "deploy_rhel_version": "8"},
rhel9={"host_class": ContentHost, "workflow": "deploy-base-rhel", "deploy_rhel_version": "9"},
) as host_dict:
rhel8_host = host_dict["rhel8"][0]
rhel9_host = host_dict["rhel9"][0]You are encouraged to build upon the existing Host class Broker provides but need to include it as a base class for Broker to work with it properly. This will allow you to build upon the base functionality Broker already provides while incorporating logic specific to your use cases.
Once you have a new class, you can let Broker know to use it during host construction.
from broker import Broker
from broker.hosts import Host
class MyHost(Host):
...
with Broker(..., host_class=MyHost) as my_host:
...When checking out multiple host types at once, pass a mapping of type name to class via host_classes:
with Broker(..., host_classes={"host": MyHost, "satellite": SatelliteHost}) as hosts:
...Sometimes you might want to define some behavior to occur after a host is checked out but before Broker gives it to you. Alternatively, you may want to define teardown logic that happens right before a host is checked in.
When using the Broker context manager, Broker will run any setup or teardown method defined on the Host object.
Broker will not pass any arguments to the setup or teardown methods, so they must not accept arguments.
class MyHost(Host):
...
def setup(self):
self.register()
def teardown(self):
self.unregister()To prevent a specific host from being checked in when the context manager exits (for example, to keep it around for debugging), set _skip_context_checkin = True on the host instance before the context exits:
with Broker(nick="rhel9") as host:
host._skip_context_checkin = True # keep the host after the with blockEvery host Broker checks out is an instance of Host (or a subclass). The key attributes set during construction are:
| Attribute | Description |
|---|---|
hostname |
The hostname or IP address used for SSH |
name |
The provider-assigned name of the host |
username |
SSH username (defaults to root) |
password |
SSH password, if any |
port |
SSH port (defaults to 22) |
key_filename |
Path to an SSH private key file, if any |
timeout |
SSH connection timeout in seconds |
Any extra keyword arguments returned by the provider are also set as attributes on the host.
| Method | Description |
|---|---|
host.execute(command, timeout=None) |
Run a shell command via SSH; returns a Result
|
host.connect(**kwargs) |
Explicitly open the SSH connection |
host.close() |
Close the SSH connection |
host.setup() |
Called automatically when entering a Broker context manager |
host.teardown() |
Called automatically when exiting a Broker context manager |
host.to_dict() |
Serialize the host to a dictionary |
Host.from_dict(arg_dict) |
Reconstruct a Host from a dictionary (class method) |
host.session returns the underlying SSH session object, which is lazily created on first access. For container hosts without an exposed SSH port a ContainerSession is returned instead; for all other hosts an SSH session is returned using the configured backend (see SSH Backends below).
# Accessing the session directly to call lower-level methods
session = host.session
result = session.run("whoami")
print(result.stdout) # => "root\n"The session object (regardless of the backend in use) exposes the following interface. All file-transfer methods accept string or Path arguments.
Execute a command and return a Result.
result = host.session.run("uname -r")
print(result.status) # exit code (int)
print(result.stdout) # standard output (str)
print(result.stderr) # standard error (str)An optional timeout argument (in seconds) can be passed; 0 means no timeout.
Transfer files using SCP.
# Download a file from the remote host
host.session.scp_read("/etc/hostname") # saved to ./hostname
host.session.scp_read("/etc/hostname", destination="/tmp/") # saved to /tmp/hostname
raw_bytes = host.session.scp_read("/etc/hostname", return_data=True)
# Upload a file to the remote host
host.session.scp_write("/local/path/script.sh") # uploaded to /local/path/script.sh
host.session.scp_write("/local/path/script.sh", destination="/remote/path/script.sh")Transfer files using SFTP.
# Download a remote file
host.session.sftp_read("/var/log/messages") # saved locally to ./messages
host.session.sftp_read("/var/log/messages", destination="/tmp/messages")
data = host.session.sftp_read("/var/log/messages", return_data=True) # returns bytes
# Upload a local file
host.session.sftp_write("/local/path/config.conf") # uploaded to /local/path/config.conf
host.session.sftp_write("/local/path/config.conf", destination="/etc/myapp/config.conf")Both sftp_write and scp_write accept an ensure_dir=True keyword argument (default True) that will create any missing parent directories on the remote host before the transfer.
Open an interactive shell channel on the remote host.
shell = host.session.shell(pty=False) # pty=True requests a pseudo-terminalThe returned object is the backend-specific shell/channel; use it for interactive workflows.
Context manager that tails a file on the remote host. The contents attribute of the yielded object is populated when the context exits.
with host.session.tail_file("/var/log/messages") as tailer:
host.execute("systemctl restart myservice")
# tailer.contents holds everything appended to the file during the block
print(tailer.contents)Copy a file from this host directly to another host (server-side copy between two live sessions).
# copy /tmp/artifact.tar from source_host to dest_host at the same path
source_host.session.remote_copy(
source="/tmp/artifact.tar",
dest_host=dest_host,
dest_path="/tmp/artifact.tar",
)Broker supports multiple pluggable SSH backends. The active backend is controlled by the SSH.BACKEND setting in broker_settings.yaml (or the BROKER_SSH__BACKEND environment variable).
| Backend | Install Extra | Notes |
|---|---|---|
hussh |
broker[hussh] |
Default; recommended |
paramiko |
broker[paramiko] |
Pure-Python; widely compatible |
ssh2-python |
broker[ssh2] |
Libssh2 bindings |
pylibssh |
broker[pylibssh] |
Ansible's libssh bindings |
All backends expose the same Session interface described above. Install the desired backend and set it in your config:
ssh:
backend: husshThe Broker class re-exports the most common exceptions for convenience so you do not need to import them separately from broker.exceptions.
from broker import Broker
try:
host = Broker(nick="rhel9").checkout()
except Broker.AuthenticationError:
print("SSH authentication failed")
except Broker.ProviderError:
print("Provider returned an error")
except Broker.BrokerError as e:
print(f"General broker error: {e}")| Exception | When raised |
|---|---|
Broker.BrokerError |
Base class for all Broker exceptions |
Broker.AuthenticationError |
SSH or provider authentication failure |
Broker.ProviderError |
An error returned by the provider |
Broker.ConfigurationError |
Missing or invalid configuration |
Broker.PermissionError |
Insufficient permissions for the action |
Broker.UserError |
Invalid user input |