1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
|
"""
Tests concerned with the ``fab`` tool & how it overrides Invoke defaults.
"""
import os
import sys
import re
from invoke import run
from invoke.util import cd
from unittest.mock import patch
from paramiko.agent import AgentKey, Message
import pytest # because WHY would you expose @skip normally? -_-
from pytest_relaxed import raises
from fabric.config import Config
from fabric.main import make_program
from fabric.exceptions import NothingToDo
from fabric.testing.base import Session
from _util import expect, support, config_file, trap
# Designate a runtime config file intended for the test environment; it does
# things like automatically mute stdin so test harnesses that care about stdin
# don't get upset.
# NOTE: this requires the test environment to have Invoke 1.1.0 or above; for
# now this is fine as we don't do a big serious matrix, we typically use Invoke
# master to allow testing in-dev changes.
# TODO: if that _changes_ then we may have to rethink this so that it goes back
# to being testable on Invoke >=1.0 instead of >=1.1...
os.environ["INVOKE_RUNTIME_CONFIG"] = config_file
class Fab_:
class core_program_behavior:
def version_output_contains_our_name_plus_deps(self):
expect(
"--version",
r"""
Fabric .+
Paramiko .+
Invoke .+
""".strip(),
test="regex",
)
def help_output_says_fab(self):
expect("--help", "Usage: fab", test="contains")
def exposes_hosts_flag_in_help(self):
expect("--help", "-H STRING, --hosts=STRING", test="contains")
def executes_remainder_as_anonymous_task(self, remote):
remote.expect(host="myhost", cmd="whoami")
make_program().run("fab -H myhost -- whoami", exit=False)
@patch("paramiko.agent.Agent.get_keys")
def can_list_agent_keys(self, get_keys):
agent_keys = []
for type_, bits, comment in (
("ecdsa", b"dummy", "woody"),
("rsa", b"ventriloquist", "bob"),
("ed25519", b"stagehand", "smith"),
):
# Looks like a pubkey blob from an agent
m = Message()
m.add_string(type_)
m.add_string(bits)
agent_keys.append(
AgentKey(agent=None, blob=bytes(m), comment=comment)
)
get_keys.return_value = agent_keys
expected = """
0 SHA256:r7SOU1pAlEWmRE57Swf0OQHg9tlYicKaLx2DxGbDVk8 woody (ECDSA)
0 SHA256:2qZYGN+eIVfmhwpQUMje7uG4+7tZquM5LBwNaHCBsqg bob (RSA)
0 SHA256:4seJT+aN1aTPIudGupnXsZ1z20r+GCIAAKEA4MHnwvA smith (ED25519)
""".lstrip()
expect("--list-agent-keys", expected)
def uses_FABRIC_env_prefix(self, environ):
environ["FABRIC_RUN_ECHO"] = "1"
with cd(support):
make_program().run("fab expect-from-env")
def basic_pre_and_post_tasks_still_work(self):
with cd(support):
# Safety
expect("first", "First!\n")
expect("third", "Third!\n")
# Real test
expect("second", "First!\nSecond!\nThird!\n")
class filenames:
def loads_fabfile_not_tasks(self):
"Loads fabfile.py, not tasks.py"
with cd(support):
expect(
"--list",
"""
Available tasks:
basic-run
build
deploy
expect-connect-timeout
expect-from-env
expect-identities
expect-identity
expect-mutation
expect-mutation-to-fail
expect-vanilla-Context
first
hosts-are-host-stringlike
hosts-are-init-kwargs
hosts-are-mixed-values
hosts-are-myhost
mutate
second
third
two-hosts
vanilla-Task-works-ok
""".lstrip(),
)
def loads_fabric_config_files_not_invoke_ones(self):
for type_ in ("yaml", "yml", "json", "py"):
with cd(os.path.join(support, "{}_conf".format(type_))):
# This task, in each subdir, expects data present in a
# fabric.<ext> nearby to show up in the config.
make_program().run("fab expect-conf-value")
class runtime_ssh_config_path:
def _run(
self,
flag="-S",
file_="ssh_config/runtime.conf",
tasks="runtime-ssh-config",
):
with cd(support):
# Relies on asserts within the task, which will bubble up as
# it's executed in-process
cmd = "fab -c runtime_fabfile {} {} -H runtime {}"
make_program().run(cmd.format(flag, file_, tasks))
def capital_F_flag_specifies_runtime_ssh_config_file(self):
self._run(flag="-S")
def long_form_flag_also_works(self):
self._run(flag="--ssh-config")
@raises(IOError)
def IOErrors_if_given_missing_file(self):
self._run(file_="nope/nothere.conf")
@patch.object(Config, "_load_ssh_file")
def config_only_loaded_once_per_session(self, method):
# Task that doesn't make assertions about the config (since the
# _actual_ config it gets is empty as we had to mock out the loader
# method...sigh)
self._run(tasks="dummy dummy")
# Called only once (initial __init__) with runtime conf, instead of
# that plus a few more pairs of calls against the default files
# (which is what happens when clone() isn't preserving the
# already-parsed/loaded SSHConfig)
method.assert_called_once_with("ssh_config/runtime.conf")
class hosts_flag_parameterizes_tasks:
# NOTE: many of these just rely on MockRemote's builtin
# "channel.exec_command called with given command string" asserts.
def single_string_is_single_host_and_single_exec(self, remote):
remote.expect(host="myhost", cmd="nope")
# In addition to just testing a base case, this checks for a really
# dumb bug where one appends to, instead of replacing, the task
# list during parameterization/expansion XD
with cd(support):
make_program().run("fab -H myhost basic-run")
def comma_separated_string_is_multiple_hosts(self, remote):
remote.expect_sessions(
Session("host1", cmd="nope"), Session("host2", cmd="nope")
)
with cd(support):
make_program().run("fab -H host1,host2 basic-run")
def multiple_hosts_works_with_remainder_too(self, remote):
remote.expect_sessions(
Session("host1", cmd="whoami"), Session("host2", cmd="whoami")
)
make_program().run("fab -H host1,host2 -- whoami")
def host_string_shorthand_is_passed_through(self, remote):
remote.expect(host="host1", port=1234, user="someuser")
make_program().run("fab -H someuser@host1:1234 -- whoami")
# NOTE: no mocking because no actual run() under test, only
# parameterization
# TODO: avoiding for now because implementing this requires more work
# at the Invoke level re: deciding when to _not_ pass in the
# session-global config object (Executor's self.config). At the moment,
# our threading-concurrency API is oriented around Group, and we're not
# using it for --hosts, so it's not broken...yet.
@pytest.mark.skip
def config_mutation_not_preserved(self):
with cd(support):
make_program().run(
"fab -H host1,host2 expect-mutation-to-fail"
)
@trap
def pre_post_tasks_are_not_parameterized_across_hosts(self):
with cd(support):
make_program().run(
"fab -H hostA,hostB,hostC second --show-host"
)
output = sys.stdout.getvalue()
# Expect pre once, 3x main, post once, as opposed to e.g. both
# pre and main task
expected = """
First!
Second: hostA
Second: hostB
Second: hostC
Third!
""".lstrip()
assert output == expected
class hosts_task_arg_parameterizes_tasks:
# NOTE: many of these just rely on MockRemote's builtin
# "channel.exec_command called with given command string" asserts.
def single_string_is_single_exec(self, remote):
remote.expect(host="myhost", cmd="nope")
with cd(support):
make_program().run("fab hosts-are-myhost")
def multiple_strings_is_multiple_host_args(self, remote):
remote.expect_sessions(
Session("host1", cmd="nope"), Session("host2", cmd="nope")
)
with cd(support):
make_program().run("fab two-hosts")
def host_string_shorthand_works_ok(self, remote):
remote.expect(host="host1", port=1234, user="someuser")
with cd(support):
make_program().run("fab hosts-are-host-stringlike")
def may_give_Connection_init_kwarg_dicts(self, remote):
remote.expect_sessions(
Session("host1", user="admin", cmd="nope"),
Session("host2", cmd="nope"),
)
with cd(support):
make_program().run("fab hosts-are-init-kwargs")
def may_give_mixed_value_types(self, remote):
remote.expect_sessions(
Session("host1", user="admin", cmd="nope"),
Session("host2", cmd="nope"),
)
with cd(support):
make_program().run("fab hosts-are-mixed-values")
class no_hosts_flag_or_task_arg:
def calls_task_once_with_invoke_context(self):
with cd(support):
make_program().run("fab expect-vanilla-Context")
def vanilla_Invoke_task_works_too(self):
with cd(support):
make_program().run("fab vanilla-Task-works-ok")
@raises(NothingToDo)
def generates_exception_if_combined_with_remainder(self):
make_program().run("fab -- nope")
def invokelike_multitask_invocation_preserves_config_mutation(self):
# Mostly a guard against Executor subclass tweaks breaking Invoke
# behavior added in pyinvoke/invoke#309
with cd(support):
make_program().run("fab mutate expect-mutation")
class connect_timeout:
def dash_t_supplies_default_connect_timeout(self):
with cd(support):
make_program().run("fab -t 5 expect-connect-timeout")
def double_dash_connect_timeout_also_works(self):
with cd(support):
make_program().run(
"fab --connect-timeout 5 expect-connect-timeout"
)
class runtime_identity_file:
def dash_i_supplies_default_connect_kwarg_key_filename(self):
# NOTE: the expect-identity task in tests/_support/fabfile.py
# performs asserts about its context's .connect_kwargs value,
# relying on other tests to prove connect_kwargs makes its way into
# that context.
with cd(support):
make_program().run("fab -i identity.key expect-identity")
def double_dash_identity_also_works(self):
with cd(support):
make_program().run(
"fab --identity identity.key expect-identity"
)
def may_be_given_multiple_times(self):
with cd(support):
make_program().run(
"fab -i identity.key -i identity2.key expect-identities"
)
class secrets_prompts:
@patch("fabric.main.getpass.getpass")
def _expect_prompt(self, getpass, flag, key, value, prompt):
getpass.return_value = value
with cd(support):
# Expect that the given key was found in the context.
cmd = "fab -c prompting {} expect-connect-kwarg --key {} --val {}" # noqa
make_program().run(cmd.format(flag, key, value))
# Then we also expect that getpass was called w/ expected prompt
getpass.assert_called_once_with(prompt)
def password_prompt_updates_connect_kwargs(self):
self._expect_prompt(
flag="--prompt-for-login-password",
key="password",
value="mypassword",
prompt="Enter login password for use with SSH auth: ",
)
def passphrase_prompt_updates_connect_kwargs(self):
self._expect_prompt(
flag="--prompt-for-passphrase",
key="passphrase",
value="mypassphrase",
prompt="Enter passphrase for use unlocking SSH keys: ",
)
class configuration_updating_and_merging:
def key_filename_can_be_set_via_non_override_config_levels(self):
# Proves/protects against #1762, where eg key_filenames gets
# 'reset' to an empty list. Arbitrarily uses the 'yml' level of
# test fixtures, which has a fabric.yml w/ a
# connect_kwargs.key_filename value of [private.key, other.key].
with cd(os.path.join(support, "yml_conf")):
make_program().run("fab expect-conf-key-filename")
def cli_identity_still_overrides_when_non_empty(self):
with cd(os.path.join(support, "yml_conf")):
make_program().run("fab -i cli.key expect-cli-key-filename")
class completion:
# NOTE: most completion tests are in Invoke too; this is just an
# irritating corner case driven by Fabric's 'remainder' functionality.
@trap
def complete_flag_does_not_trigger_remainder_only_behavior(self):
# When bug present, 'fab --complete -- fab' fails to load any
# collections because it thinks it's in remainder-only,
# work-without-a-collection mode.
with cd(support):
make_program().run("fab --complete -- fab", exit=False)
# Cherry-picked safety checks looking for tasks from fixture
# fabfile
output = sys.stdout.getvalue()
for name in ("build", "deploy", "expect-from-env"):
assert name in output
class main:
"__main__"
def python_dash_m_acts_like_fab(self, capsys):
# Rehash of version output test, but using 'python -m fabric'
expected_output = r"""
Fabric .+
Paramiko .+
Invoke .+
""".strip()
output = run("python -m fabric --version", hide=True, in_stream=False)
assert re.match(expected_output, output.stdout)
|