Question

This is a very Fabric specific question, but more experienced python hackers might be able to answer this, even if they don't know Fabric.

I am trying to specify different behaviour in a command depending on which role it is running for, i.e.:

def restart():
    if (SERVERTYPE == "APACHE"):
        sudo("apache2ctl graceful",pty=True)
    elif (SERVERTYPE == "APE"):
        sudo("supervisorctl reload",pty=True)

I was hacking this with functions like this one:

def apache():
    global SERVERTYPE
    SERVERTYPE = "APACHE"
    env.hosts = ['xxx.xxx.com']

But that is obviously not very elegant and I just discovered roles, so my question is:

How do I figure out which role a current instance belongs to?

env.roledefs = {
    'apache': ['xxx.xxx.com'],
    'APE': ['yyy.xxx.com'],
}

Thanks!

Was it helpful?

Solution

Update: Just checked the source code and it seems that this was already available as early as 1.4.2!

update 2: This seems not to work when using the @roles decorator (in 1.5.3)! It only works when specifying the roles using the -R command line flag.

For fabric 1.5.3 the current roles are directly available in `fabric.api.env.roles'. For example:

import fabric.api as fab

fab.env.roledefs['staging'] = ['bbs-evolution.ipsw.dt.ept.lu']
fab.env.roledefs['prod'] = ['bbs-arbiter.ipsw.dt.ept.lu']


@fab.task
def testrole():
    print fab.env.roles

Test output on the console:

› fab -R staging testrole
[bbs-evolution.ipsw.dt.ept.lu] Executing task 'testrole'
['staging']

Done.

Or:

› fab -R staging,prod testrole
[bbs-evolution.ipsw.dt.ept.lu] Executing task 'testrole'
['staging', 'prod']
[bbs-arbiter.ipsw.dt.ept.lu] Executing task 'testrole'
['staging', 'prod']

Done.

With this, we can do a simple in test in a fabric task:

@fab.task
def testrole():
    if 'prod' in fab.env.roles:
        do_production_stuff()
    elif 'staging' in fab.env.roles:
        do_staging_stuff()
    else:
        raise ValueError('No valid role specified!')

OTHER TIPS

For everyone else ever with this question, here is my solution:

The key was finding env.host_string.

This is how I restart different types of servers with one command:

env.roledefs = {
    'apache': ['xxx.xxx.com'],
    'APE': ['yyy.xxx.com']
}

def apache():
    env.roles = ['apache']

...

def restart():
    if env.host_string in env.roledefs['apache']:
        sudo("apache2ctl graceful", pty=True)
    elif env.host_string in env.roledefs['APE']:
        sudo ("supervisorctl reload", pty=True)

enjoy!

I didn't test it, but might work:

def _get_current_role():
    for role in env.roledefs.keys():
        if env.host_string in env.roledefs[role]:
            return role
    return None

The env.roles will give you the roles specified by -R flag or hardcoded in the script itself. It won't contain the roles specified per task using the command line or using @roles decorator. There is currently no way of getting this kind of information.

The next release of fabric (1.9 probably) will provide env.effective_roles attribute with exactly what you want - the roles being used for the currently executed task. The code for that has already been merged into master.

Have a look at this issue.

Using fabric 1.14.0 under Anaconda2 5.1.0... confirm the issue when using the @roles decorator... especially in the case that the @roles decorator is used with multiple arguments and then another task without the @roles decorator (or with different arguments) is called from within the first task. In my experience, this can have the effect of nonsensically mismatching the hosts, depending on how I discover the role (i.e. role = env.effective_roles[0]).

Note that role = env.effective_roles[0] does work well in simple situations, e.g. (a) @roles only specifies one role, and (b) the original task does not call another task.

Note also the situation where -R on command line does not override @roles and must use task:roles=role1 instead: How to run a @roles-decorated fabric task on only a single host ... also wondering how to pass multiple roles to an argument named roles... hmmm, but I digress.

Perhaps there is a better way, but documentation on @roles leaves one wanting. Next step is to probably read through the source code at this point.

In the meantime, I've hacked up the following workaround...

from fabric.api import env
from fabric.decorators import roles
from fabric.decorators import task


def get_host_roles(env, of=None, die=False):
    """
    Get the role(s) for a host at run time
    :param env: Fabric env
    :param of: tuple/set/list
    :param die: boolean
    :return: tuple(host, roles) or tuple(host, role)
    """
    host = env.host
    def valid(role):
        return host in env.roledefs[role]:
    roles = set(filter(valid, env.roledefs.keys()))
    if of:
        roles = tuple(roles & set(of)) # set intersection
        if len(roles) == 1:
            return host, roles[0]
        elif die:
            e = 'Host "%s" is not in just one of the provided roles: %s!' \
                % (host, repr(roles))
            raise Exception(e)
    return host, roles


_roles = ('role1', 'role2')


@task
@roles(*_roles)
def do_something_with_roles():
    host, roles = get_host_roles(env)
    # roles is a tuple with all of the roles the host is in.


@task
@roles(*_roles)
def do_something_with_roles_diy():
    host, roles = get_host_roles(env, _roles)
    # `roles` is a tuple with the set intersection of `_roles` and the
    # host's actual roles... so you handle the situation!
    if 'role1' in roles:
        # do whatever
        pass


@task
@roles(*_roles)
def force_single_role():
    host, role = get_host_roles(env, _roles, True)
    # this usage raises an exception in the instance that the host is not
    # exclusively in either 'role1' or 'role2'.
    # roles is a string with the role for that host.

Hope that helps.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top