ssh kitten: match hostnames against both remote hostname and hostname used on the command line to connect to it
This commit is contained in:
parent
d037c0b0fc
commit
855718b179
@ -24,6 +24,8 @@ Comments can be added to the config file as lines starting with the ``#``
|
|||||||
character. This works only if the ``#`` character is the first character
|
character. This works only if the ``#`` character is the first character
|
||||||
in the line.
|
in the line.
|
||||||
|
|
||||||
|
.. _include:
|
||||||
|
|
||||||
You can include secondary config files via the :code:`include` directive. If
|
You can include secondary config files via the :code:`include` directive. If
|
||||||
you use a relative path for :code:`include`, it is resolved with respect to the
|
you use a relative path for :code:`include`, it is resolved with respect to the
|
||||||
location of the current config file. Note that environment variables are
|
location of the current config file. Note that environment variables are
|
||||||
|
|||||||
@ -56,19 +56,18 @@ Additionally, you can pass config options on the command line:
|
|||||||
|
|
||||||
.. code-block:: sh
|
.. code-block:: sh
|
||||||
|
|
||||||
kitty +kitten ssh --kitten hostname=somehost --kitten interpreter=python ...
|
kitty +kitten ssh --kitten interpreter=python servername
|
||||||
|
|
||||||
The :code:`--kitten` argument can be specified multiple times, with directives
|
The :code:`--kitten` argument can be specified multiple times, with directives
|
||||||
from :file:`ssh.conf`. These are merged with :file:`ssh.conf` as if they were
|
from :file:`ssh.conf`. These are merged with :file:`ssh.conf` as if they were
|
||||||
appended to the end of that file. It is important to specify the :opt:`hostname <kitten-ssh.hostname>` as
|
appended to the end of that file. They apply only to the host being SSHed to
|
||||||
the first setting, otherwise the last :opt:`hostname <kitten-ssh.hostname>` from :file:`ssh.conf` will be
|
by this invocation.
|
||||||
used.
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Because of the poor design of SSH, any typing you do before the shell prompt
|
Because of limitations of the design of SSH, any typing you do before the
|
||||||
appears may be lost. So ideally dont start typing till you see the shell
|
shell prompt appears may be lost. So ideally dont start typing till you see
|
||||||
prompt :)
|
the shell prompt 😇.
|
||||||
|
|
||||||
|
|
||||||
A real world example
|
A real world example
|
||||||
|
|||||||
@ -24,11 +24,11 @@ def host_matches(pat: str, hostname: str, username: str) -> bool:
|
|||||||
return fnmatch.fnmatchcase(hostname, pat) and fnmatch.fnmatchcase(username, upat)
|
return fnmatch.fnmatchcase(hostname, pat) and fnmatch.fnmatchcase(username, upat)
|
||||||
|
|
||||||
|
|
||||||
def options_for_host(hostname: str, username: str, per_host_opts: Dict[str, SSHOptions]) -> SSHOptions:
|
def options_for_host(hostname: str, username: str, per_host_opts: Dict[str, SSHOptions], cli_hostname: str = '', cli_uname: str = '') -> SSHOptions:
|
||||||
matches = []
|
matches = []
|
||||||
for spat, opts in per_host_opts.items():
|
for spat, opts in per_host_opts.items():
|
||||||
for pat in spat.split():
|
for pat in spat.split():
|
||||||
if host_matches(pat, hostname, username):
|
if host_matches(pat, hostname, username) or (cli_hostname and host_matches(pat, cli_hostname, cli_uname)):
|
||||||
matches.append(opts)
|
matches.append(opts)
|
||||||
if not matches:
|
if not matches:
|
||||||
return SSHOptions({})
|
return SSHOptions({})
|
||||||
|
|||||||
@ -159,12 +159,14 @@ def get_ssh_data(msg: str, request_id: str) -> Iterator[bytes]:
|
|||||||
raise ValueError('Incorrect password')
|
raise ValueError('Incorrect password')
|
||||||
if rq_id != request_id:
|
if rq_id != request_id:
|
||||||
raise ValueError('Incorrect request id')
|
raise ValueError('Incorrect request id')
|
||||||
|
cli_hostname = env_data['cli_hostname']
|
||||||
|
cli_uname = env_data['cli_uname']
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
yield fmt_prefix(f'!{e}')
|
yield fmt_prefix(f'!{e}')
|
||||||
else:
|
else:
|
||||||
ssh_opts = {k: SSHOptions(v) for k, v in env_data['opts'].items()}
|
ssh_opts = {k: SSHOptions(v) for k, v in env_data['opts'].items()}
|
||||||
resolved_ssh_opts = options_for_host(hostname, username, ssh_opts)
|
resolved_ssh_opts = options_for_host(hostname, username, ssh_opts, cli_hostname, cli_uname)
|
||||||
resolved_ssh_opts.copy = {k: CopyInstruction(*v) for k, v in resolved_ssh_opts.copy.items()}
|
resolved_ssh_opts.copy = {k: CopyInstruction(*v) for k, v in resolved_ssh_opts.copy.items()}
|
||||||
try:
|
try:
|
||||||
data = make_tarfile(resolved_ssh_opts, env_data['env'])
|
data = make_tarfile(resolved_ssh_opts, env_data['env'])
|
||||||
@ -205,7 +207,7 @@ def prepare_exec_cmd(remote_args: Sequence[str], is_python: bool) -> str:
|
|||||||
def bootstrap_script(
|
def bootstrap_script(
|
||||||
script_type: str = 'sh', remote_args: Sequence[str] = (),
|
script_type: str = 'sh', remote_args: Sequence[str] = (),
|
||||||
ssh_opts_dict: Dict[str, Dict[str, Any]] = {},
|
ssh_opts_dict: Dict[str, Dict[str, Any]] = {},
|
||||||
test_script: str = '', request_id: Optional[str] = None
|
test_script: str = '', request_id: Optional[str] = None, cli_hostname: str = '', cli_uname: str = ''
|
||||||
) -> str:
|
) -> str:
|
||||||
if request_id is None:
|
if request_id is None:
|
||||||
request_id = os.environ['KITTY_PID'] + '-' + os.environ['KITTY_WINDOW_ID']
|
request_id = os.environ['KITTY_PID'] + '-' + os.environ['KITTY_WINDOW_ID']
|
||||||
@ -214,7 +216,7 @@ def bootstrap_script(
|
|||||||
ans = f.read()
|
ans = f.read()
|
||||||
pw = uuid4()
|
pw = uuid4()
|
||||||
with tempfile.NamedTemporaryFile(prefix='ssh-kitten-pw-', suffix='.json', dir=cache_dir(), delete=False) as tf:
|
with tempfile.NamedTemporaryFile(prefix='ssh-kitten-pw-', suffix='.json', dir=cache_dir(), delete=False) as tf:
|
||||||
data = {'pw': pw, 'env': dict(os.environ), 'opts': ssh_opts_dict}
|
data = {'pw': pw, 'env': dict(os.environ), 'opts': ssh_opts_dict, 'cli_hostname': cli_hostname, 'cli_uname': cli_uname}
|
||||||
tf.write(json.dumps(data).encode('utf-8'))
|
tf.write(json.dumps(data).encode('utf-8'))
|
||||||
atexit.register(safe_remove, tf.name)
|
atexit.register(safe_remove, tf.name)
|
||||||
replacements = {
|
replacements = {
|
||||||
@ -224,10 +226,6 @@ def bootstrap_script(
|
|||||||
return prepare_script(ans, replacements)
|
return prepare_script(ans, replacements)
|
||||||
|
|
||||||
|
|
||||||
def load_script(script_type: str = 'sh', remote_args: Sequence[str] = (), ssh_opts_dict: Dict[str, Dict[str, Any]] = {}) -> str:
|
|
||||||
return bootstrap_script(script_type, remote_args, ssh_opts_dict=ssh_opts_dict)
|
|
||||||
|
|
||||||
|
|
||||||
def get_ssh_cli() -> Tuple[Set[str], Set[str]]:
|
def get_ssh_cli() -> Tuple[Set[str], Set[str]]:
|
||||||
other_ssh_args: Set[str] = set()
|
other_ssh_args: Set[str] = set()
|
||||||
boolean_ssh_args: Set[str] = set()
|
boolean_ssh_args: Set[str] = set()
|
||||||
@ -393,11 +391,14 @@ def parse_ssh_args(args: List[str], extra_args: Tuple[str, ...] = ()) -> Tuple[L
|
|||||||
|
|
||||||
|
|
||||||
def get_remote_command(
|
def get_remote_command(
|
||||||
remote_args: List[str], hostname: str = 'localhost', interpreter: str = 'sh',
|
remote_args: List[str], hostname: str = 'localhost', cli_hostname: str = '', cli_uname: str = '',
|
||||||
|
interpreter: str = 'sh',
|
||||||
ssh_opts_dict: Dict[str, Dict[str, Any]] = {}
|
ssh_opts_dict: Dict[str, Dict[str, Any]] = {}
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
is_python = 'python' in interpreter.lower()
|
is_python = 'python' in interpreter.lower()
|
||||||
sh_script = load_script(script_type='py' if is_python else 'sh', remote_args=remote_args, ssh_opts_dict=ssh_opts_dict)
|
sh_script = bootstrap_script(
|
||||||
|
script_type='py' if is_python else 'sh', remote_args=remote_args, ssh_opts_dict=ssh_opts_dict,
|
||||||
|
cli_hostname=cli_hostname, cli_uname=cli_uname)
|
||||||
return [f'{interpreter} -c {shlex.quote(sh_script)}']
|
return [f'{interpreter} -c {shlex.quote(sh_script)}']
|
||||||
|
|
||||||
|
|
||||||
@ -436,12 +437,17 @@ def main(args: List[str]) -> NoReturn:
|
|||||||
for i, a in enumerate(found_extra_args):
|
for i, a in enumerate(found_extra_args):
|
||||||
if i % 2 == 1:
|
if i % 2 == 1:
|
||||||
overrides.append(pat.sub(r'\1 ', a.lstrip()))
|
overrides.append(pat.sub(r'\1 ', a.lstrip()))
|
||||||
|
if overrides:
|
||||||
|
overrides.insert(0, f'hostname {uname}@{hostname_for_match}')
|
||||||
so = init_config(overrides)
|
so = init_config(overrides)
|
||||||
sod = {k: v._asdict() for k, v in so.items()}
|
sod = {k: v._asdict() for k, v in so.items()}
|
||||||
cmd += get_remote_command(remote_args, hostname, options_for_host(hostname_for_match, uname, so).interpreter, sod)
|
cmd += get_remote_command(remote_args, hostname, hostname_for_match, uname, options_for_host(hostname_for_match, uname, so).interpreter, sod)
|
||||||
import subprocess
|
import subprocess
|
||||||
with suppress(FileNotFoundError):
|
with suppress(FileNotFoundError):
|
||||||
raise SystemExit(subprocess.run(cmd).returncode)
|
try:
|
||||||
|
raise SystemExit(subprocess.run(cmd).returncode)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
raise SystemExit(1)
|
||||||
raise SystemExit('Could not find the ssh executable, is it in your PATH?')
|
raise SystemExit('Could not find the ssh executable, is it in your PATH?')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -29,10 +29,11 @@ The hostname the following options apply to. A glob pattern to match multiple
|
|||||||
hosts can be used. Multiple hostnames can also be specified separated by spaces.
|
hosts can be used. Multiple hostnames can also be specified separated by spaces.
|
||||||
The hostname can include an optional username in the form :code:`user@host`.
|
The hostname can include an optional username in the form :code:`user@host`.
|
||||||
When not specified options apply to all hosts, until the
|
When not specified options apply to all hosts, until the
|
||||||
first hostname specification is found. Note that the hostname this matches
|
first hostname specification is found. Note that matching of hostname is done against
|
||||||
against is the hostname used by the remote computer, not the name you pass
|
both the hostname used by the remote computer, and the name you pass
|
||||||
to SSH to connect to it. If you wish to include the same basic configuration for many
|
to SSH to connect to it. If either matches, it is considered a match.
|
||||||
different hosts, you can do so with the :code:`include` directive (see :doc:`/conf`).
|
If you wish to include the same basic configuration for many
|
||||||
|
different hosts, you can do so with the :ref:`include <include>` directive.
|
||||||
''')
|
''')
|
||||||
|
|
||||||
opt('+copy', '', option_type='copy', add_to_default=False, long_text=f'''
|
opt('+copy', '', option_type='copy', add_to_default=False, long_text=f'''
|
||||||
@ -84,7 +85,7 @@ The interpreter to use on the remote host. Must be either a POSIX complaint shel
|
|||||||
or a python executable. If the default sh is not available for broken, using
|
or a python executable. If the default sh is not available for broken, using
|
||||||
an alternate interpreter can be useful. Note that as the interpreter is used for
|
an alternate interpreter can be useful. Note that as the interpreter is used for
|
||||||
bootstrapping, hostname specific values are matched again the hostname from the
|
bootstrapping, hostname specific values are matched again the hostname from the
|
||||||
command line args rather than the actual remote hostname.
|
command line args only.
|
||||||
''')
|
''')
|
||||||
|
|
||||||
opt('remote_dir', '.local/share/kitty-ssh-kitten', option_type='relative_dir', long_text='''
|
opt('remote_dir', '.local/share/kitty-ssh-kitten', option_type='relative_dir', long_text='''
|
||||||
|
|||||||
@ -49,6 +49,7 @@ def ref_map() -> Dict[str, str]:
|
|||||||
from kitty.actions import get_all_actions
|
from kitty.actions import get_all_actions
|
||||||
ref_map = {
|
ref_map = {
|
||||||
'layouts': f'{website_url("overview")}#layouts',
|
'layouts': f'{website_url("overview")}#layouts',
|
||||||
|
'include': f'{website_url("conf")}#include',
|
||||||
'watchers': f'{website_url("launch")}#watchers',
|
'watchers': f'{website_url("launch")}#watchers',
|
||||||
'sessions': f'{website_url("overview")}#startup-sessions',
|
'sessions': f'{website_url("overview")}#startup-sessions',
|
||||||
'functional': f'{website_url("keyboard-protocol")}#functional-key-definitions',
|
'functional': f'{website_url("keyboard-protocol")}#functional-key-definitions',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user