fabric / fabric
1 2
import copy
2 2
import errno
3 2
import os
4

5 2
from invoke.config import Config as InvokeConfig, merge_dicts
6 2
from paramiko.config import SSHConfig
7

8 2
from .runners import Remote
9 2
from .util import get_local_user, debug
10

11

12 2
class Config(InvokeConfig):
13
    """
14
    An `invoke.config.Config` subclass with extra Fabric-related behavior.
15

16
    This class behaves like `invoke.config.Config` in every way, with the
17
    following exceptions:
18

19
    - its `global_defaults` staticmethod has been extended to add/modify some
20
      default settings (see its documentation, below, for details);
21
    - it triggers loading of Fabric-specific env vars (e.g.
22
      ``FABRIC_RUN_HIDE=true`` instead of ``INVOKE_RUN_HIDE=true``) and
23
      filenames (e.g. ``/etc/fabric.yaml`` instead of ``/etc/invoke.yaml``).
24
    - it extends the API to account for loading ``ssh_config`` files (which are
25
      stored as additional attributes and have no direct relation to the
26
      regular config data/hierarchy.)
27

28
    Intended for use with `.Connection`, as using vanilla
29
    `invoke.config.Config` objects would require users to manually define
30
    ``port``, ``user`` and so forth.
31

32
    .. seealso:: :doc:`/concepts/configuration`, :ref:`ssh-config`
33

34
    .. versionadded:: 2.0
35
    """
36

37 2
    prefix = "fabric"
38

39 2
    def __init__(self, *args, **kwargs):
40
        """
41
        Creates a new Fabric-specific config object.
42

43
        For most API details, see `invoke.config.Config.__init__`. Parameters
44
        new to this subclass are listed below.
45

46
        :param ssh_config:
47
            Custom/explicit `paramiko.config.SSHConfig` object. If given,
48
            prevents loading of any SSH config files. Default: ``None``.
49

50
        :param str runtime_ssh_path:
51
            Runtime SSH config path to load. Prevents loading of system/user
52
            files if given. Default: ``None``.
53

54
        :param str system_ssh_path:
55
            Location of the system-level SSH config file. Default:
56
            ``/etc/ssh/ssh_config``.
57

58
        :param str user_ssh_path:
59
            Location of the user-level SSH config file. Default:
60
            ``~/.ssh/config``.
61

62
        :param bool lazy:
63
            Has the same meaning as the parent class' ``lazy``, but additionall
64
            controls whether SSH config file loading is deferred (requires
65
            manually calling `load_ssh_config` sometime.) For example, one may
66
            need to wait for user input before calling `set_runtime_ssh_path`,
67
            which will inform exactly what `load_ssh_config` does.
68
        """
69
        # Tease out our own kwargs.
70
        # TODO: consider moving more stuff out of __init__ and into methods so
71
        # there's less of this sort of splat-args + pop thing? Eh.
72 2
        ssh_config = kwargs.pop("ssh_config", None)
73 2
        lazy = kwargs.get("lazy", False)
74 2
        self.set_runtime_ssh_path(kwargs.pop("runtime_ssh_path", None))
75 2
        system_path = kwargs.pop("system_ssh_path", "/etc/ssh/ssh_config")
76 2
        self._set(_system_ssh_path=system_path)
77 2
        self._set(_user_ssh_path=kwargs.pop("user_ssh_path", "~/.ssh/config"))
78

79
        # Record whether we were given an explicit object (so other steps know
80
        # whether to bother loading from disk or not)
81
        # This needs doing before super __init__ as that calls our post_init
82 2
        explicit = ssh_config is not None
83 2
        self._set(_given_explicit_object=explicit)
84

85
        # Arrive at some non-None SSHConfig object (upon which to run .parse()
86
        # later, in _load_ssh_file())
87 2
        if ssh_config is None:
88 2
            ssh_config = SSHConfig()
89 2
        self._set(base_ssh_config=ssh_config)
90

91
        # Now that our own attributes have been prepared & kwargs yanked, we
92
        # can fall up into parent __init__()
93 2
        super(Config, self).__init__(*args, **kwargs)
94

95
        # And finally perform convenience non-lazy bits if needed
96 2
        if not lazy:
97 2
            self.load_ssh_config()
98

99 2
    def set_runtime_ssh_path(self, path):
100
        """
101
        Configure a runtime-level SSH config file path.
102

103
        If set, this will cause `load_ssh_config` to skip system and user
104
        files, as OpenSSH does.
105

106
        .. versionadded:: 2.0
107
        """
108 2
        self._set(_runtime_ssh_path=path)
109

110 2
    def load_ssh_config(self):
111
        """
112
        Load SSH config file(s) from disk.
113

114
        Also (beforehand) ensures that Invoke-level config re: runtime SSH
115
        config file paths, is accounted for.
116

117
        .. versionadded:: 2.0
118
        """
119
        # Update the runtime SSH config path (assumes enough regular config
120
        # levels have been loaded that anyone wanting to transmit this info
121
        # from a 'vanilla' Invoke config, has gotten it set.)
122 2
        if self.ssh_config_path:
123 2
            self._runtime_ssh_path = self.ssh_config_path
124
        # Load files from disk if we weren't given an explicit SSHConfig in
125
        # __init__
126 2
        if not self._given_explicit_object:
127 2
            self._load_ssh_files()
128

129 2
    def clone(self, *args, **kwargs):
130
        # TODO: clone() at this point kinda-sorta feels like it's retreading
131
        # __reduce__ and the related (un)pickling stuff...
132
        # Get cloned obj.
133
        # NOTE: Because we also extend .init_kwargs, the actual core SSHConfig
134
        # data is passed in at init time (ensuring no files get loaded a 2nd,
135
        # etc time) and will already be present, so we don't need to set
136
        # .base_ssh_config ourselves. Similarly, there's no need to worry about
137
        # how the SSH config paths may be inaccurate until below; nothing will
138
        # be referencing them.
139 2
        new = super(Config, self).clone(*args, **kwargs)
140
        # Copy over our custom attributes, so that the clone still resembles us
141
        # re: recording where the data originally came from (in case anything
142
        # re-runs ._load_ssh_files(), for example).
143 2
        for attr in (
144
            "_runtime_ssh_path",
145
            "_system_ssh_path",
146
            "_user_ssh_path",
147
        ):
148 2
            setattr(new, attr, getattr(self, attr))
149
        # Load SSH configs, in case they weren't prior to now (e.g. a vanilla
150
        # Invoke clone(into), instead of a us-to-us clone.)
151 2
        self.load_ssh_config()
152
        # All done
153 2
        return new
154

155 2
    def _clone_init_kwargs(self, *args, **kw):
156
        # Parent kwargs
157 2
        kwargs = super(Config, self)._clone_init_kwargs(*args, **kw)
158
        # Transmit our internal SSHConfig via explicit-obj kwarg, thus
159
        # bypassing any file loading. (Our extension of clone() above copies
160
        # over other attributes as well so that the end result looks consistent
161
        # with reality.)
162 2
        new_config = SSHConfig()
163
        # TODO: as with other spots, this implies SSHConfig needs a cleaner
164
        # public API re: creating and updating its core data.
165 2
        new_config._config = copy.deepcopy(self.base_ssh_config._config)
166 2
        return dict(kwargs, ssh_config=new_config)
167

168 2
    def _load_ssh_files(self):
169
        """
170
        Trigger loading of configured SSH config file paths.
171

172
        Expects that ``base_ssh_config`` has already been set to an
173
        `~paramiko.config.SSHConfig` object.
174

175
        :returns: ``None``.
176
        """
177
        # TODO: does this want to more closely ape the behavior of
178
        # InvokeConfig.load_files? re: having a _found attribute for each that
179
        # determines whether to load or skip
180 2
        if self._runtime_ssh_path is not None:
181 2
            path = self._runtime_ssh_path
182
            # Manually blow up like open() (_load_ssh_file normally doesn't)
183 2
            if not os.path.exists(path):
184 2
                msg = "No such file or directory: {!r}".format(path)
185 2
                raise IOError(errno.ENOENT, msg)
186 2
            self._load_ssh_file(os.path.expanduser(path))
187 2
        elif self.load_ssh_configs:
188 2
            for path in (self._user_ssh_path, self._system_ssh_path):
189 2
                self._load_ssh_file(os.path.expanduser(path))
190

191 2
    def _load_ssh_file(self, path):
192
        """
193
        Attempt to open and parse an SSH config file at ``path``.
194

195
        Does nothing if ``path`` is not a path to a valid file.
196

197
        :returns: ``None``.
198
        """
199 2
        if os.path.isfile(path):
200 2
            old_rules = len(self.base_ssh_config._config)
201 2
            with open(path) as fd:
202 2
                self.base_ssh_config.parse(fd)
203 2
            new_rules = len(self.base_ssh_config._config)
204 2
            msg = "Loaded {} new ssh_config rules from {!r}"
205 2
            debug(msg.format(new_rules - old_rules, path))
206
        else:
207 2
            debug("File not found, skipping")
208

209 2
    @staticmethod
210
    def global_defaults():
211
        """
212
        Default configuration values and behavior toggles.
213

214
        Fabric only extends this method in order to make minor adjustments and
215
        additions to Invoke's `~invoke.config.Config.global_defaults`; see its
216
        documentation for the base values, such as the config subtrees
217
        controlling behavior of ``run`` or how ``tasks`` behave.
218

219
        For Fabric-specific modifications and additions to the Invoke-level
220
        defaults, see our own config docs at :ref:`default-values`.
221

222
        .. versionadded:: 2.0
223
        """
224
        # TODO: hrm should the run-related things actually be derived from the
225
        # runner_class? E.g. Local defines local stuff, Remote defines remote
226
        # stuff? Doesn't help with the final config tree tho...
227
        # TODO: as to that, this is a core problem, Fabric wants split
228
        # local/remote stuff, eg replace_env wants to be False for local and
229
        # True remotely; shell wants to differ depending on target (and either
230
        # way, does not want to use local interrogation for remote)
231
        # TODO: is it worth moving all of our 'new' settings to a discrete
232
        # namespace for cleanliness' sake? e.g. ssh.port, ssh.user etc.
233
        # It wouldn't actually simplify this code any, but it would make it
234
        # easier for users to determine what came from which library/repo.
235 2
        defaults = InvokeConfig.global_defaults()
236 2
        ours = {
237
            # New settings
238
            "connect_kwargs": {},
239
            "forward_agent": False,
240
            "gateway": None,
241
            # TODO 3.0: change to True and update all docs accordingly.
242
            "inline_ssh_env": False,
243
            "load_ssh_configs": True,
244
            "port": 22,
245
            "run": {"replace_env": True},
246
            "runners": {"remote": Remote},
247
            "ssh_config_path": None,
248
            "tasks": {"collection_name": "fabfile"},
249
            # TODO: this becomes an override/extend once Invoke grows execution
250
            # timeouts (which should be timeouts.execute)
251
            "timeouts": {"connect": None},
252
            "user": get_local_user(),
253
        }
254 2
        merge_dicts(defaults, ours)
255 2
        return defaults

Read our documentation on viewing source code .

Loading