def accepts_configuration_value(self): gw = Connection("jumpbox") config = Config(overrides={"gateway": gw}) # TODO: the fact that they will be eq, but _not_ necessarily be # the same object, could be problematic in some cases... cxn = Connection("host", config=config) assert cxn.gateway == gw
def multi_hop_works_ok(self): cxn = self._runtime_cxn(basename="proxyjump_multi") innermost = cxn.gateway.gateway.gateway middle = cxn.gateway.gateway outermost = cxn.gateway assert innermost == Connection("jumpuser3@jumphost3:411") assert middle == Connection("jumpuser2@jumphost2:872") assert outermost == Connection("jumpuser@jumphost:373")
def multihop_plus_wildcards_still_no_recursion(self): conf = self._runtime_config( basename="proxyjump_multi_recursive" ) cxn = Connection("runtime.tld", config=conf) outer = cxn.gateway inner = cxn.gateway.gateway assert outer == Connection("bastion1.tld") assert inner == Connection("bastion2.tld") assert inner.gateway is None
def ipv6_addresses_work_ok_but_avoid_port_shorthand(self): for addr in ("2001:DB8:0:0:0:0:0:1", "2001:DB8::1", "::1"): c = Connection(addr, port=123) assert c.user == get_local_user() assert c.host == addr assert c.port == 123 c2 = Connection("somebody@{}".format(addr), port=123) assert c2.user == "somebody" assert c2.host == addr assert c2.port == 123
def param_comparison_uses_config(self): conf = Config(overrides={"user": "******"}) c = Connection( user="******", host="myhost", port=123, config=conf ) template = "<Connection host=myhost port=123>" assert repr(c) == template
def refuses_to_overwrite_connect_kwargs_with_others(self, client): for key, value, kwargs in ( # Core connection args should definitely not get overwritten! # NOTE: recall that these keys are the SSHClient.connect() # kwarg names, NOT our own config/kwarg names! ("hostname", "nothost", {}), ("port", 17, {}), ("username", "zerocool", {}), # These might arguably still be allowed to work, but let's head # off confusion anyways. ("timeout", 100, { "connect_timeout": 25 }), ): try: Connection("host", connect_kwargs={ key: value }, **kwargs).open() except ValueError as e: err = ( "Refusing to be ambiguous: connect() kwarg '{}' was given both via regular arg and via connect_kwargs!" # noqa ) assert str(e) == err.format(key) else: assert False, "Did not raise ValueError!"
def calls_agent_handler_close_if_enabled(self, Handler, client): c = Connection("host", forward_agent=True) c.create_session() c.close() # NOTE: this will need to change if, for w/e reason, we ever want # to run multiple handlers at once Handler.return_value.close.assert_called_once_with()
def merges_sources(self, client, ssh, invoke, kwarg, expected): config_kwargs = {} if ssh: # SSH config with 2x IdentityFile directives. config_kwargs["runtime_ssh_path"] = join( support, "ssh_config", "runtime_identity.conf") if invoke: # Use overrides config level to mimic --identity use NOTE: (the # fact that --identity is an override, and thus overrides eg # invoke config file values is part of invoke's config test # suite) config_kwargs["overrides"] = { "connect_kwargs": { "key_filename": ["configured.key"] } } conf = Config_(**config_kwargs) connect_kwargs = {} if kwarg: # Stitch in connect_kwargs value connect_kwargs = {"key_filename": ["kwarg.key"]} # Tie in all sources that were configured & open() Connection("runtime", config=conf, connect_kwargs=connect_kwargs).open() # Ensure we got the expected list of keys kwargs = client.connect.call_args[1] if expected: assert kwargs["key_filename"] == expected else: # No key filenames -> it's not even passed in as connect_kwargs # is gonna be a blank dict assert "key_filename" not in kwargs
def loses_to_explicit(self): # Would be "my gateway", as above config = self._runtime_config() cxn = Connection("runtime", config=config, gateway="other gateway") assert cxn.gateway == "other gateway"
def kwarg_wins_over_config(self): # TODO: should this be more of a merge-down? c = Config(overrides={"connect_kwargs": {"origin": "config"}}) cxn = Connection("host", connect_kwargs={"origin": "kwarg"}, config=c) assert cxn.connect_kwargs == {"origin": "kwarg"}
def loses_to_explicit(self): # Would be True, as above config = self._runtime_config() cxn = Connection("runtime", config=config, forward_agent=False) assert cxn.forward_agent is False
def sets_missing_host_key_policy(self, Policy, client): # TODO: should make the policy configurable early on sentinel = Mock() Policy.return_value = sentinel Connection("host") set_policy = client.set_missing_host_key_policy set_policy.assert_called_once_with(sentinel)
def is_connected_still_False_when_connect_fails(self, client): client.connect.side_effect = socket.error cxn = Connection("host") try: cxn.open() except socket.error: pass assert cxn.is_connected is False
def connect_kwargs_protection_not_tripped_by_defaults(self, client): Connection("host", connect_kwargs={"timeout": 300}).open() client.connect.assert_called_with( username=get_local_user(), hostname="host", port=22, timeout=300, )
def passes_through_connect_kwargs(self, client): Connection("host", connect_kwargs={"foobar": "bizbaz"}).open() client.connect.assert_called_with( username=get_local_user(), hostname="host", port=22, foobar="bizbaz", )
def loses_to_explicit(self): # Would be a Connection equal to self._expected_gw, as # above config = self._runtime_config(basename="proxyjump") cxn = Connection("runtime", config=config, gateway="other gateway") assert cxn.gateway == "other gateway"
def short_circuits_if_already_connected(self, client): cxn = Connection("host") # First call will set self.transport to fixture's mock cxn.open() # Second call will check .is_connected which will see active==True, # and short circuit cxn.open() assert client.connect.call_count == 1
def lazily_caches_result(self, client): sentinel1, sentinel2 = object(), object() client.open_sftp.side_effect = [sentinel1, sentinel2] cxn = Connection("host") first = cxn.sftp() # TODO: why aren't we just asserting about calls of open_sftp??? err = "{0!r} wasn't the sentinel object()!" assert first is sentinel1, err.format(first) second = cxn.sftp() assert second is sentinel1, err.format(second)
def uses_proxycommand_as_sock_for_Client_connect(self, moxy, client): "uses ProxyCommand from gateway as 'sock' arg to SSHClient.connect" # Setup main = Connection("host", gateway="net catty %h %p") main.open() # Expect ProxyCommand instantiation moxy.assert_called_once_with("net catty host 22") # Expect result of that as sock arg to connect() sock_arg = client.connect.call_args[1]["sock"] assert sock_arg is moxy.return_value
def uses_gateway_channel_as_sock_for_SSHClient_connect(self, Client): "uses Connection gateway as 'sock' arg to SSHClient.connect" # Setup mock_gw = Mock() mock_main = Mock() Client.side_effect = [mock_gw, mock_main] gw = Connection("otherhost") gw.open = Mock(wraps=gw.open) main = Connection("host", gateway=gw) main.open() # Expect gateway is also open()'d gw.open.assert_called_once_with() # Expect direct-tcpip channel open on 1st client open_channel = mock_gw.get_transport.return_value.open_channel kwargs = open_channel.call_args[1] assert kwargs["kind"] == "direct-tcpip" assert kwargs["dest_addr"], "host" == 22 # Expect result of that channel open as sock arg to connect() sock_arg = mock_main.connect.call_args[1]["sock"] assert sock_arg is open_channel.return_value
def hostname_directive_overrides_host_attr(self): # TODO: not 100% convinced this is the absolute most # obvious API for 'translation' of given hostname to # ssh-configured hostname, but it feels okay for now. path = join(support, "ssh_config", "overridden_hostname.conf") config = Config_(runtime_ssh_path=path) cxn = Connection("aliasname", config=config) assert cxn.host == "realname" assert cxn.original_host == "aliasname" assert cxn.port == 2222
def if_given_an_invoke_Config_we_upgrade_to_our_own_Config(self): # Scenario: user has Fabric-level data present at vanilla # Invoke config level, and is then creating Connection objects # with those vanilla invoke Configs. # (Could also _not_ have any Fabric-level data, but then that's # just a base case...) # TODO: adjust this if we ever switch to all our settings being # namespaced... vanilla = InvokeConfig(overrides={"forward_agent": True}) cxn = Connection("host", config=vanilla) assert cxn.forward_agent is True # not False, which is default
def basic_invocation(self, Remote, client): # Technically duplicates Invoke-level tests, but ensures things # still work correctly at our level. cxn = Connection("host") cxn.sudo("foo") cmd = "sudo -S -p '{}' foo".format(cxn.config.sudo.prompt) # NOTE: this is another spot where Mock.call_args is inexplicably # None despite call_args_list being populated. WTF. (Also, # Remote.return_value is two different Mocks now, despite Remote's # own Mock having the same ID here and in code under test. WTF!!) expected = [call(cxn), call().run(cmd, watchers=ANY)] assert Remote.mock_calls == expected
def sorting_works(self): # Hostname... assert Connection("a-host") < Connection("b-host") # User... assert Connection("a-host", user="******") < Connection( "a-host", user="******") # then port... assert Connection("a-host", port=1) < Connection("a-host", port=2)
def gateway_Connections_get_parent_connection_configs(self): conf = self._runtime_config( basename="proxyjump", overrides={"some_random_option": "a-value"}, ) cxn = Connection("runtime", config=conf) # Sanity assert cxn.config is conf assert cxn.gateway == self._expected_gw # Real check assert cxn.gateway.config.some_random_option == "a-value" # Prove copy not reference # TODO: would we ever WANT a reference? can't imagine... assert cxn.gateway.config is not conf
def _forward_remote(self, kwargs, Client, select, mocket): # TODO: unhappy with how much this duplicates of the code under # test, re: sig/default vals # Set up parameter values/defaults remote_port = kwargs["remote_port"] remote_host = kwargs.get("remote_host", "127.0.0.1") local_port = kwargs.get("local_port", remote_port) local_host = kwargs.get("local_host", "localhost") # Mock/etc setup, anything that can be prepped before the forward # occurs (which is most things) tun_socket = mocket.return_value cxn = Connection("host") # Channel that will yield data when read from chan = Mock() chan.recv.return_value = "data" # And make select() yield it as being ready once, when called select.select.side_effect = _select_result(chan) with cxn.forward_remote(**kwargs): # At this point Connection.open() has run and generated a # Transport mock for us (because SSHClient is mocked). Let's # first make sure we asked it for the port forward... # NOTE: this feels like it's too limited/tautological a test, # until you realize that it's functionally impossible to mock # out everything required for Paramiko's inner guts to run # _parse_channel_open() and suchlike :( call = cxn.transport.request_port_forward.call_args_list[0] assert call[1]["address"] == remote_host assert call[1]["port"] == remote_port # Pretend the Transport called our callback with mock Channel call[1]["handler"](chan, tuple(), tuple()) # Then have to sleep a bit to make sure we give the tunnel # created by that callback to spin up; otherwise ~5% of the # time we exit the contextmanager so fast, the tunnel's "you're # done!" flag is set before it even gets a chance to select() # once. time.sleep(0.01) # And make sure we hooked up to the local socket OK tup = (local_host, local_port) tun_socket.connect.assert_called_once_with(tup) # Expect that our socket got written to by the tunnel (due to the # above-setup select() and channel mocking). Need to do this after # tunnel shutdown or we risk thread ordering issues. tun_socket.sendall.assert_called_once_with("data") # Ensure we closed down the mock socket mocket.return_value.close.assert_called_once_with() # And that the transport canceled the port forward on the remote # end. assert cxn.transport.cancel_port_forward.call_count == 1
def comparison_uses_host_user_and_port(self): # Just host assert Connection("host") == Connection("host") # Host + user c1 = Connection("host", user="******") c2 = Connection("host", user="******") assert c1 == c2 # Host + user + port c1 = Connection("host", user="******", port=123) c2 = Connection("host", user="******", port=123) assert c1 == c2
def calls_Remote_run_with_command_and_kwargs_and_returns_its_result( self, Remote, client): remote = Remote.return_value sentinel = object() remote.run.return_value = sentinel c = Connection("host") r1 = c.run("command") r2 = c.run("command", warn=True, hide="stderr") # NOTE: somehow, .call_args & the methods built on it (like # .assert_called_with()) stopped working, apparently triggered by # our code...somehow...after commit (roughly) 80906c7. # And yet, .call_args_list and its brethren work fine. Wha? Remote.assert_any_call(c) remote.run.assert_has_calls( [call("command"), call("command", warn=True, hide="stderr")]) for r in (r1, r2): assert r is sentinel
def _forward_local(self, kwargs, Client, mocket, select): # Tease out bits of kwargs for use in the mocking/expecting. # But leave it alone for raw passthru to the API call itself. # TODO: unhappy with how much this apes the real code & its sig... local_port = kwargs["local_port"] remote_port = kwargs.get("remote_port", local_port) local_host = kwargs.get("local_host", "localhost") remote_host = kwargs.get("remote_host", "localhost") # These aren't part of the real sig, but this is easier than trying # to reconcile the mock decorators + optional-value kwargs. meh. tunnel_exception = kwargs.pop("tunnel_exception", None) listener_exception = kwargs.pop("listener_exception", False) # Mock setup client = Client.return_value listener_sock = Mock(name="listener_sock") if listener_exception: listener_sock.bind.side_effect = listener_exception data = b("Some data") tunnel_sock = Mock(name="tunnel_sock", recv=lambda n: data) local_addr = Mock() transport = client.get_transport.return_value channel = transport.open_channel.return_value # socket.socket is only called once directly mocket.return_value = listener_sock # The 2nd socket is obtained via an accept() (which should only # fire once & raise EAGAIN after) listener_sock.accept.side_effect = chain( [(tunnel_sock, local_addr)], repeat(socket.error(errno.EAGAIN, "nothing yet")), ) obj = tunnel_sock if tunnel_exception is None else tunnel_exception select.select.side_effect = _select_result(obj) with Connection("host").forward_local(**kwargs): # Make sure we give listener thread enough time to boot up :( # Otherwise we might assert before it does things. (NOTE: # doesn't need to be much, even at 0.01s, 0/100 trials failed # (vs 45/100 with no sleep) time.sleep(0.015) assert client.connect.call_args[1]["hostname"] == "host" listener_sock.setsockopt.assert_called_once_with( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) listener_sock.setblocking.assert_called_once_with(0) listener_sock.bind.assert_called_once_with( (local_host, local_port)) if not listener_exception: listener_sock.listen.assert_called_once_with(1) transport.open_channel.assert_called_once_with( "direct-tcpip", (remote_host, remote_port), local_addr) # Local write to tunnel_sock is implied by its mocked-out # recv() call above... # NOTE: don't assert if explodey; we want to mimic "the only # error that occurred was within the thread" behavior being # tested by thread-exception-handling tests if not (tunnel_exception or listener_exception): channel.sendall.assert_called_once_with(data) # Shutdown, with another sleep because threads. time.sleep(0.015) if not listener_exception: tunnel_sock.close.assert_called_once_with() channel.close.assert_called_once_with() listener_sock.close.assert_called_once_with()
def calls_Transfer_put(self, Transfer): "calls Transfer.put()" c = Connection("host") c.put("meh") Transfer.assert_called_with(c) Transfer.return_value.put.assert_called_with("meh")