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

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

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

11

12 1
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
    - it adds a new optional constructor, `from_v1`, which :ref:`generates
28
      configuration data from Fabric 1 <from-v1>`.
29

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

34
    .. seealso:: :doc:`/concepts/configuration`, :ref:`ssh-config`
35

36
    .. versionadded:: 2.0
37
    """
38

39 1
    prefix = "fabric"
40

41 1
    @classmethod
42
    def from_v1(cls, env, **kwargs):
43
        """
44
        Alternate constructor which uses Fabric 1's ``env`` dict for settings.
45

46
        All keyword arguments besides ``env`` are passed unmolested into the
47
        primary constructor, with the exception of ``overrides``, which is used
48
        internally & will end up resembling the data from ``env`` with the
49
        user-supplied overrides on top.
50

51
        .. warning::
52
            Because your own config overrides will win over data from ``env``,
53
            make sure you only set values you *intend* to change from your v1
54
            environment!
55

56
        For details on exactly which ``env`` vars are imported and what they
57
        become in the new API, please see :ref:`v1-env-var-imports`.
58

59
        :param env:
60
            An explicit Fabric 1 ``env`` dict (technically, any
61
            ``fabric.utils._AttributeDict`` instance should work) to pull
62
            configuration from.
63

64
        .. versionadded:: 2.4
65
        """
66
        # TODO: automagic import, if we can find a way to test that
67
        # Use overrides level (and preserve whatever the user may have given)
68
        # TODO: we really do want arbitrary number of config levels, don't we?
69
        # TODO: most of these need more care re: only filling in when they
70
        # differ from the v1 default. As-is these won't overwrite runtime
71
        # overrides (due to .setdefault) but they may still be filling in empty
72
        # values to stomp on lower level config levels...
73 1
        data = kwargs.pop("overrides", {})
74
        # TODO: just use a dataproxy or defaultdict??
75 1
        for subdict in ("connect_kwargs", "run", "sudo", "timeouts"):
76 1
            data.setdefault(subdict, {})
77
        # PTY use
78 1
        data["run"].setdefault("pty", env.always_use_pty)
79
        # Gateway
80 1
        data.setdefault("gateway", env.gateway)
81
        # Agent forwarding
82 1
        data.setdefault("forward_agent", env.forward_agent)
83
        # Key filename(s)
84 1
        if env.key_filename is not None:
85 1
            data["connect_kwargs"].setdefault("key_filename", env.key_filename)
86
        # Load keys from agent?
87 1
        data["connect_kwargs"].setdefault("allow_agent", not env.no_agent)
88 1
        data.setdefault("ssh_config_path", env.ssh_config_path)
89
        # Sudo password
90 1
        data["sudo"].setdefault("password", env.sudo_password)
91
        # Vanilla password (may be used for regular and/or sudo, depending)
92 1
        passwd = env.password
93 1
        data["connect_kwargs"].setdefault("password", passwd)
94 1
        if not data["sudo"]["password"]:
95 1
            data["sudo"]["password"] = passwd
96 1
        data["sudo"].setdefault("prompt", env.sudo_prompt)
97 1
        data["timeouts"].setdefault("connect", env.timeout)
98 1
        data.setdefault("load_ssh_configs", env.use_ssh_config)
99 1
        data["run"].setdefault("warn", env.warn_only)
100
        # Put overrides back for real constructor and go
101 1
        kwargs["overrides"] = data
102 1
        return cls(**kwargs)
103

104 1
    def __init__(self, *args, **kwargs):
105
        """
106
        Creates a new Fabric-specific config object.
107

108
        For most API details, see `invoke.config.Config.__init__`. Parameters
109
        new to this subclass are listed below.
110

111
        :param ssh_config:
112
            Custom/explicit `paramiko.config.SSHConfig` object. If given,
113
            prevents loading of any SSH config files. Default: ``None``.
114

115
        :param str runtime_ssh_path:
116
            Runtime SSH config path to load. Prevents loading of system/user
117
            files if given. Default: ``None``.
118

119
        :param str system_ssh_path:
120
            Location of the system-level SSH config file. Default:
121
            ``/etc/ssh/ssh_config``.
122

123
        :param str user_ssh_path:
124
            Location of the user-level SSH config file. Default:
125
            ``~/.ssh/config``.
126

127
        :param bool lazy:
128
            Has the same meaning as the parent class' ``lazy``, but
129
            additionally controls whether SSH config file loading is deferred
130
            (requires manually calling `load_ssh_config` sometime.) For
131
            example, one may need to wait for user input before calling
132
            `set_runtime_ssh_path`, which will inform exactly what
133
            `load_ssh_config` does.
134
        """
135
        # Tease out our own kwargs.
136
        # TODO: consider moving more stuff out of __init__ and into methods so
137
        # there's less of this sort of splat-args + pop thing? Eh.
138 1
        ssh_config = kwargs.pop("ssh_config", None)
139 1
        lazy = kwargs.get("lazy", False)
140 1
        self.set_runtime_ssh_path(kwargs.pop("runtime_ssh_path", None))
141 1
        system_path = kwargs.pop("system_ssh_path", "/etc/ssh/ssh_config")
142 1
        self._set(_system_ssh_path=system_path)
143 1
        self._set(_user_ssh_path=kwargs.pop("user_ssh_path", "~/.ssh/config"))
144

145
        # Record whether we were given an explicit object (so other steps know
146
        # whether to bother loading from disk or not)
147
        # This needs doing before super __init__ as that calls our post_init
148 1
        explicit = ssh_config is not None
149 1
        self._set(_given_explicit_object=explicit)
150

151
        # Arrive at some non-None SSHConfig object (upon which to run .parse()
152
        # later, in _load_ssh_file())
153 1
        if ssh_config is None:
154 1
            ssh_config = SSHConfig()
155 1
        self._set(base_ssh_config=ssh_config)
156

157
        # Now that our own attributes have been prepared & kwargs yanked, we
158
        # can fall up into parent __init__()
159 1
        super(Config, self).__init__(*args, **kwargs)
160

161
        # And finally perform convenience non-lazy bits if needed
162 1
        if not lazy:
163 1
            self.load_ssh_config()
164

165 1
    def set_runtime_ssh_path(self, path):
166
        """
167
        Configure a runtime-level SSH config file path.
168

169
        If set, this will cause `load_ssh_config` to skip system and user
170
        files, as OpenSSH does.
171

172
        .. versionadded:: 2.0
173
        """
174 1
        self._set(_runtime_ssh_path=path)
175

176 1
    def load_ssh_config(self):
177
        """
178
        Load SSH config file(s) from disk.
179

180
        Also (beforehand) ensures that Invoke-level config re: runtime SSH
181
        config file paths, is accounted for.
182

183
        .. versionadded:: 2.0
184
        """
185
        # Update the runtime SSH config path (assumes enough regular config
186
        # levels have been loaded that anyone wanting to transmit this info
187
        # from a 'vanilla' Invoke config, has gotten it set.)
188 1
        if self.ssh_config_path:
189 1
            self._runtime_ssh_path = self.ssh_config_path
190
        # Load files from disk if we weren't given an explicit SSHConfig in
191
        # __init__
192 1
        if not self._given_explicit_object:
193 1
            self._load_ssh_files()
194

195 1
    def clone(self, *args, **kwargs):
196
        # TODO: clone() at this point kinda-sorta feels like it's retreading
197
        # __reduce__ and the related (un)pickling stuff...
198
        # Get cloned obj.
199
        # NOTE: Because we also extend .init_kwargs, the actual core SSHConfig
200
        # data is passed in at init time (ensuring no files get loaded a 2nd,
201
        # etc time) and will already be present, so we don't need to set
202
        # .base_ssh_config ourselves. Similarly, there's no need to worry about
203
        # how the SSH config paths may be inaccurate until below; nothing will
204
        # be referencing them.
205 1
        new = super(Config, self).clone(*args, **kwargs)
206
        # Copy over our custom attributes, so that the clone still resembles us
207
        # re: recording where the data originally came from (in case anything
208
        # re-runs ._load_ssh_files(), for example).
209 1
        for attr in (
210
            "_runtime_ssh_path",
211
            "_system_ssh_path",
212
            "_user_ssh_path",
213
        ):
214 1
            setattr(new, attr, getattr(self, attr))
215
        # Load SSH configs, in case they weren't prior to now (e.g. a vanilla
216
        # Invoke clone(into), instead of a us-to-us clone.)
217 1
        self.load_ssh_config()
218
        # All done
219 1
        return new
220

221 1
    def _clone_init_kwargs(self, *args, **kw):
222
        # Parent kwargs
223 1
        kwargs = super(Config, self)._clone_init_kwargs(*args, **kw)
224
        # Transmit our internal SSHConfig via explicit-obj kwarg, thus
225
        # bypassing any file loading. (Our extension of clone() above copies
226
        # over other attributes as well so that the end result looks consistent
227
        # with reality.)
228 1
        new_config = SSHConfig()
229
        # TODO: as with other spots, this implies SSHConfig needs a cleaner
230
        # public API re: creating and updating its core data.
231 1
        new_config._config = copy.deepcopy(self.base_ssh_config._config)
232 1
        return dict(kwargs, ssh_config=new_config)
233

234 1
    def _load_ssh_files(self):
235
        """
236
        Trigger loading of configured SSH config file paths.
237

238
        Expects that ``base_ssh_config`` has already been set to an
239
        `~paramiko.config.SSHConfig` object.
240

241
        :returns: ``None``.
242
        """
243
        # TODO: does this want to more closely ape the behavior of
244
        # InvokeConfig.load_files? re: having a _found attribute for each that
245
        # determines whether to load or skip
246 1
        if self._runtime_ssh_path is not None:
247 1
            path = self._runtime_ssh_path
248
            # Manually blow up like open() (_load_ssh_file normally doesn't)
249 1
            if not os.path.exists(path):
250 1
                msg = "No such file or directory: {!r}".format(path)
251 1
                raise IOError(errno.ENOENT, msg)
252 1
            self._load_ssh_file(os.path.expanduser(path))
253 1
        elif self.load_ssh_configs:
254 1
            for path in (self._user_ssh_path, self._system_ssh_path):
255 1
                self._load_ssh_file(os.path.expanduser(path))
256

257 1
    def _load_ssh_file(self, path):
258
        """
259
        Attempt to open and parse an SSH config file at ``path``.
260

261
        Does nothing if ``path`` is not a path to a valid file.
262

263
        :returns: ``None``.
264
        """
265 1
        if os.path.isfile(path):
266 1
            old_rules = len(self.base_ssh_config._config)
267 1
            with open(path) as fd:
268 1
                self.base_ssh_config.parse(fd)
269 1
            new_rules = len(self.base_ssh_config._config)
270 1
            msg = "Loaded {} new ssh_config rules from {!r}"
271 1
            debug(msg.format(new_rules - old_rules, path))
272
        else:
273 1
            debug("File not found, skipping")
274

275 1
    @staticmethod
276
    def global_defaults():
277
        """
278
        Default configuration values and behavior toggles.
279

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

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

288
        .. versionadded:: 2.0
289
        """
290
        # TODO: hrm should the run-related things actually be derived from the
291
        # runner_class? E.g. Local defines local stuff, Remote defines remote
292
        # stuff? Doesn't help with the final config tree tho...
293
        # TODO: as to that, this is a core problem, Fabric wants split
294
        # local/remote stuff, eg replace_env wants to be False for local and
295
        # True remotely; shell wants to differ depending on target (and either
296
        # way, does not want to use local interrogation for remote)
297
        # TODO: is it worth moving all of our 'new' settings to a discrete
298
        # namespace for cleanliness' sake? e.g. ssh.port, ssh.user etc.
299
        # It wouldn't actually simplify this code any, but it would make it
300
        # easier for users to determine what came from which library/repo.
301 1
        defaults = InvokeConfig.global_defaults()
302 1
        ours = {
303
            # New settings
304
            "connect_kwargs": {},
305
            "forward_agent": False,
306
            "gateway": None,
307
            # TODO 3.0: change to True and update all docs accordingly.
308
            "inline_ssh_env": False,
309
            "load_ssh_configs": True,
310
            "port": 22,
311
            "run": {"replace_env": True},
312
            "runners": {"remote": Remote},
313
            "ssh_config_path": None,
314
            "tasks": {"collection_name": "fabfile"},
315
            # TODO: this becomes an override/extend once Invoke grows execution
316
            # timeouts (which should be timeouts.execute)
317
            "timeouts": {"connect": None},
318
            "user": get_local_user(),
319
        }
320 1
        merge_dicts(defaults, ours)
321 1
        return defaults

Read our documentation on viewing source code .

Loading