Using Operations¶
Operations tell pyinfra
what to do, for example the server.shell
operation instructs pyinfra
to execute a shell command. Most operations define state rather than actions - so instead of “start this service” you say “this service should be running” - pyinfra
will only execute commands if needed.
For example, these operations will ensure that user pyinfra
exists with home directory /home/pyinfra
, and that the /var/log/pyinfra.log
file exists and is owned by that user:
# Import pyinfra modules, each containing operations to use
from pyinfra.operations import server, files
server.user(
name="Create pyinfra user",
user="pyinfra",
home="/home/pyinfra",
)
files.file(
name="Create pyinfra log file",
path="/var/log/pyinfra.log",
user="pyinfra",
group="pyinfra",
mode="644",
_sudo=True,
)
Uses Files Operations and Server Operations. You can see all available operations in the Operations Index. If you save the file as deploy.py
you can test it out using Docker:
pyinfra @docker/ubuntu:20.04 deploy.py
Important
Operations that rely on one another (interdependency) must be treated with caution. See: deploy limitations.
Global Arguments¶
Global arguments are covered in detail here: Global Arguments. There is a set of arguments available to all operations to control authentication (_sudo
, etc) and operation execution (_shell_executable
, etc):
from pyinfra.operations import apt
apt.update(
name="Update apt repositories",
_sudo=True,
_sudo_user="pyinfra",
)
The host
Object¶
pyinfra
provides a global host
object that can be used to retrieve information and metadata about the current host target. At all times the host
variable represents the current host context, so you can think about the deploy code executing on individual hosts at a time.
The host
object has name
and groups
attributes which can be used to control operation flow:
from pyinfra import host
if host.name == "control-plane-1":
...
if "control-plane" in host.groups:
...
Host Facts¶
Facts allow you to use information about the target host to control and configure operations. A good example is switching between apt
& yum
depending on the Linux distribution. Facts are imported from pyinfra.facts.*
and can be collected using host.get_fact(...)
:
from pyinfra import host
from pyinfra.facts.server import LinuxName
from pyinfra.operations import yum
if host.get_fact(LinuxName) == "CentOS":
yum.packages(
name="Install nano via yum",
packages=["nano"],
_sudo=True
)
See Facts Index for a full list of available facts and arguments.
Host & Group Data¶
Adding data to inventories is covered in detail here: Inventory & Data. Data can be accessed within operations via the host.data
attribute:
from pyinfra import host
from pyinfra.operations import server
# Ensure the state of a user based on host/group data
server.user(
name="Setup the app user",
user=host.data.app_user,
home=host.data.app_dir,
)
The inventory
Object¶
Like host
, there is an inventory
object that can be used to access the entire inventory of hosts. This is useful when you need facts or data from another host like the IP address of another node:
from pyinfra import inventory
from pyinfra.facts.server import Hostname
from pyinfra.operations import files
# Get the other host, load the hostname fact
db_host = inventory.get_host("postgres-main")
db_hostname = db_host.get_fact(Hostname)
files.template(
name="Generate app config",
src="templates/app-config.j2.yaml",
dest="/opt/myapp/config.yaml",
db_hostname=db_hostname,
)
Operation Changes & Output¶
All operations return an operation meta object which provides information about the changes the operation will execute. The meta object provides changes
(integer) and changed
(boolean) attributes initially. This can be used to control subsequent operations:
from pyinfra.operations import server
create_user = server.user(
name="Create user myuser",
user="myuser",
)
# If we added a user above, do something extra
if create_user.changed:
server.shell( # add user to sudo, etc...
Operation Output¶
pyinfra
doesn’t immediately execute normal operations meaning output is not available right away. It is possible to access this output at runtime by providing a callback function using the python.call operation.
from pyinfra import logger
from pyinfra.operations import python, server
result = server.shell(
commands=["echo output"],
)
# result.stdout raises exception here, but works inside callback()
def callback():
logger.info(f"Got result: {result.stdout}")
python.call(
name="Execute callback function",
function=callback,
)
Nested Operations¶
Important
Nested operations are currently in beta. Nested operations should be kept to a minimum as they cannot be tracked as changes prior to execution.
Nested operations are called during the execution phase within a callback function passed into a python.call. Calling a nested operation generates and immediately executes it on the target machine. This is useful in complex scenarios where one operation output is required in another.
Because nested operations are executed immediately, the output is always available right away:
from pyinfra import logger
from pyinfra.operations import python, server
def callback():
result = server.shell(
commands=["echo output"],
)
logger.info(f"Got result: {result.stdout}")
python.call(
name="Execute callback function",
function=callback,
)
Include Multiple Files¶
Including files can be used to break out operations across multiple files. Files can be included using local.include
.
from pyinfra import local
# Include & call all the operations in tasks/install_something.py
local.include("tasks/install_something.py")
See more in examples: groups & roles.
The config
Object¶
Like host
and inventory
, config
can be used to set global defaults for operations. For example, to use sudo in all operations following:
from pyinfra import config
config.SUDO = True
# all operations below will use sudo by default (unless overridden by `_sudo=False`)
Enforcing Requirements¶
The config object can be used to enforce a pyinfra
version or Python package requirements. This can either be defined as a requirements text file path or simply a list of requirements:
# Require a certain pyinfra version
config.REQUIRE_PYINFRA_VERSION = "~=1.1"
# Require certain packages
config.REQUIRE_PACKAGES = "requirements.txt" # path relative to the current working directory
config.REQUIRE_PACKAGES = [
"pyinfra~=1.1",
"pyinfra-docker~=1.0",
]
Examples¶
A great way to learn more about writing pyinfra
deploys is to see some in action. There’s a number of resources for this:
- the pyinfra examples folder on GitHub - a general collection of all kinds of example deploy
- the example deploys in this documentation - these highlight specific common patterns