Introduction¶
Fabric Roles¶
In standard Fabric roles provide a many-to-many mapping between tasks and hosts. Once roles are decided, roledefs map hosts to roles. For example, in the case of a “dev” web server, all roles might collapse onto a single server. Install and deploy tasks are nicely related to hosts via roledefs.
For a production cluster, a role is often applied to more than one host. For example, a pool of webapp or memcache servers.
In the first use-case, where roles are collapsed onto a single server, Fabric comes up a little short because roles are only used to construct host lists for tasks. A role has no “configuration state”; once hosts are assigned to tasks, what role instigated that task is out of the picture. More importantly, a single task will only be invoked once per host, even if that host matches more than one role.
An example: the following fabfile runs install on each server:
from fabric.api import * env.roledefs.update({ 'webserver': ['www1', 'www2'], 'dbserver': ['db1'] }) env.roles = ('webserver', 'dbserver') @roles('webserver', 'dbserver') def install(): print("{command} invoked on host {host}".format(**env)) # outputs... # [www1] Executing task 'install' # install invoked on host www1 # [www2] Executing task 'install' # install invoked on host www2 # [db1] Executing task 'install' # install invoked on host db1
That looks like what we’d want. But the next fabfile, which tries to
combine webserver and dbserver onto a single host runs install only
once.
from fabric.api import * env.roledefs.update({ 'webserver': ['dev'], 'dbserver': ['dev'] }) env.roles = ('webserver', 'dbserver') @roles('webserver', 'dbserver') def install(): print("{command} invoked on host {host}".format(**env)) # outputs... # [dev] Executing task 'install' # install invoked on host dev
But, what if we really want install to run once per role? Yes,
we could split out a install_web and install_db. If they both run
the same "apt-get install " + env.packages it sure would be more
DRY not to split them.
Even if we did invoke install for each role, that would just
install env.packages multiple times. Then again, what if we could
also inject role specific settings into the Fabric env, for each
role invocation of a task?
Fabex Task Roles¶
What if we could write
@roles('webserver', 'dbserver') def install(): packages = { 'webserver': 'nginx', 'dbserver': 'postgres' } with settings(packages=packages[env.role]): sudo("apt-get install " + env.packages)
and have install execute once per role. Wouldn’t that be convenient?
That’s exactly what the Fabex task_roles decorator gives you: one
invocation per role, with a role specific env. The above task becomes
@task_roles('webserver', 'dbserver') def install(): sudo("apt-get install " + " ".join(env.packages))
Where does env.packages get set? In a “target configuration” yaml
file.
roledefs: webserver: [server] dbserver: [server] hostenvs: server: {ip: 127.0.0.1, ssh_host: 127.0.0.1, ssh_user: ubuntu} roleenvs: webserver: packages: [nginx] dbserver: packages: [postgres] domain: example.com
The first attribute, roledefs is the standard Fabric role<->host
relation. To this, we add top level hostenvs and roleenvs. The
task_roles decorator pulls attributes from hostenvs for the
matching host and from roleenvs from the invoking role and updates
the env dict with those values.
In addition, any top level attributes are injected, e.g., domain
in the above example. As this configuration information comes from a
file separate from the fabfile, we’d like to be able to point at that
file “on the fly”, in the fab invocation. That allows different
settings for different installations: dev, stage, production, etc.
Where do we keep these yaml targets? Fabex provides a fabex_config
function to initialize and point at a few things. Here’s a complete, working
Fabex fabfile.
from fabex.api import * fabex_config({'target_dir': '.'}) @task_roles('webserver', 'dbserver') def install(): print("installing {packages} on host {host} for {domain}".format(**env))
Save this snippet into fabfile.py, copy the yaml above into a
test.yaml file. Then run
fab target:test install
and you should see
[server] Executing task 'install' [server:webserver] Executing role task 'install' installing ['nginx'] on host server for example.com [server:dbserver] Executing role task 'install' installing ['postgres'] on host server for example.com