fabric / fabric
1 1
import invoke
2 1
from invoke import Call, Task
3

4 1
from .tasks import ConnectionCall
5 1
from .exceptions import NothingToDo
6 1
from .util import debug
7

8

9 1
class Executor(invoke.Executor):
10
    """
11
    `~invoke.executor.Executor` subclass which understands Fabric concepts.
12

13
    Designed to work in tandem with Fabric's `@task
14
    <fabric.tasks.task>`/`~fabric.tasks.Task`, and is capable of acting on
15
    information stored on the resulting objects -- such as default host lists.
16

17
    This class is written to be backwards compatible with vanilla Invoke-level
18
    tasks, which it simply delegates to its superclass.
19

20
    Please see the parent class' `documentation <invoke.executor.Executor>` for
21
    details on most public API members and object lifecycle.
22
    """
23

24 1
    def normalize_hosts(self, hosts):
25
        """
26
        Normalize mixed host-strings-or-kwarg-dicts into kwarg dicts only.
27

28
        In other words, transforms data taken from the CLI (--hosts, always
29
        strings) or decorator arguments (may be strings or kwarg dicts) into
30
        kwargs suitable for creating Connection instances.
31

32
        Subclasses may wish to override or extend this to perform, for example,
33
        database or custom config file lookups (vs this default behavior, which
34
        is to simply assume that strings are 'host' kwargs).
35

36
        :param hosts:
37
            Potentially heterogenous list of host connection values, as per the
38
            ``hosts`` param to `.task`.
39

40
        :returns: Homogenous list of Connection init kwarg dicts.
41
        """
42 1
        dicts = []
43 1
        for value in hosts or []:
44
            # Assume first posarg to Connection() if not already a dict.
45 1
            if not isinstance(value, dict):
46 1
                value = dict(host=value)
47 1
            dicts.append(value)
48 1
        return dicts
49

50 1
    def expand_calls(self, calls, apply_hosts=True):
51
        # Generate new call list with per-host variants & Connections inserted
52 1
        ret = []
53 1
        cli_hosts = []
54 1
        host_str = self.core[0].args.hosts.value
55 1
        if apply_hosts and host_str:
56 1
            cli_hosts = host_str.split(",")
57 1
        for call in calls:
58 1
            if isinstance(call, Task):
59 1
                call = Call(task=call)
60
            # TODO: expand this to allow multiple types of execution plans,
61
            # pending outcome of invoke#461 (which, if flexible enough to
62
            # handle intersect of dependencies+parameterization, just becomes
63
            # 'honor that new feature of Invoke')
64
            # TODO: roles, other non-runtime host parameterizations, etc
65
            # Pre-tasks get added only once, not once per host.
66 1
            ret.extend(self.expand_calls(call.pre, apply_hosts=False))
67
            # Determine final desired host list based on CLI and task values
68
            # (with CLI, being closer to runtime, winning) and normalize to
69
            # Connection-init kwargs.
70 1
            call_hosts = getattr(call, "hosts", None)
71 1
            cxn_params = self.normalize_hosts(cli_hosts or call_hosts)
72
            # Main task, per host/connection
73 1
            for init_kwargs in cxn_params:
74 1
                ret.append(self.parameterize(call, init_kwargs))
75
            # Deal with lack of hosts list (acts same as `inv` in that case)
76
            # TODO: no tests for this branch?
77 1
            if not cxn_params:
78 1
                ret.append(call)
79
            # Post-tasks added once, not once per host.
80 1
            ret.extend(self.expand_calls(call.post, apply_hosts=False))
81
        # Add remainder as anonymous task
82 1
        if self.core.remainder:
83
            # TODO: this will need to change once there are more options for
84
            # setting host lists besides "-H or 100% within-task"
85 1
            if not cli_hosts:
86 1
                raise NothingToDo(
87
                    "Was told to run a command, but not given any hosts to run it on!"  # noqa
88
                )
89

90 1
            def anonymous(c):
91 1
                c.run(self.core.remainder)
92

93 1
            anon = Call(Task(body=anonymous))
94
            # TODO: see above TODOs about non-parameterized setups, roles etc
95
            # TODO: will likely need to refactor that logic some more so it can
96
            # be used both there and here.
97 1
            for init_kwargs in self.normalize_hosts(cli_hosts):
98 1
                ret.append(self.parameterize(anon, init_kwargs))
99 1
        return ret
100

101 1
    def parameterize(self, call, connection_init_kwargs):
102
        """
103
        Parameterize a Call with its Context set to a per-host Connection.
104

105
        :param call:
106
            The generic `.Call` being parameterized.
107
        :param connection_init_kwargs:
108
            The dict of `.Connection` init params/kwargs to attach to the
109
            resulting `.ConnectionCall`.
110

111
        :returns:
112
            `.ConnectionCall`.
113
        """
114 1
        msg = "Parameterizing {!r} with Connection kwargs {!r}"
115 1
        debug(msg.format(call, connection_init_kwargs))
116
        # Generate a custom ConnectionCall that has init_kwargs (used for
117
        # creating the Connection at runtime) set to the requested params.
118 1
        new_call_kwargs = dict(init_kwargs=connection_init_kwargs)
119 1
        clone = call.clone(into=ConnectionCall, with_=new_call_kwargs)
120 1
        return clone
121

122 1
    def dedupe(self, tasks):
123
        # Don't perform deduping, we will often have "duplicate" tasks w/
124
        # distinct host values/etc.
125
        # TODO: might want some deduplication later on though - falls under
126
        # "how to mesh parameterization with pre/post/etc deduping".
127 1
        return tasks

Read our documentation on viewing source code .

Loading