Source code for sshMaxArg

#!/usr/bin/env python3
#    Copyright (C) 2015-2022 Germar Reitze
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License along
#    with this program; if not, write to the Free Software Foundation, Inc.,
#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

"""This module determines the maximum possible length of an SSH command.

It can also can run as a stand alone script. The solution is based on
https://www.theeggeadventure.com/wikimedia/index.php/Ssh_argument_length
"""

import random
import string
import subprocess
import socket
import argparse

# must be divisible by 8
_INITIAL_SSH_COMMAND_SIZE = 1048320


[docs] def probe_max_ssh_command_size(config, ssh_command_size=_INITIAL_SSH_COMMAND_SIZE, size_offset=_INITIAL_SSH_COMMAND_SIZE): """Determine the maximum length of SSH commands for the current config Try a SSH command with length ``ssh_command_size``. The command is decreased by ``size_offset`` if it was too long or increased if it worked. The function calls itself recursively until it finds the maximum possible length. The offset ``size_offset`` is bisect in each try. Args: config (config.Config): Back In Time config instance including the details about the current SSH snapshot profile. The current profile must use the SSH mode. ssh_command_size (int): Initial length used for the test argument. size_offset (int): Offset for increase or decrease ``ssh_command_size``. Returns: (int): The maximum possible SSH command length Raises: Exception: If there are unhandled cases or the recurse ends in an undefined state. OSError: If there are unhandled cases. """ size_offset = round(size_offset / 2) # random string of desired length command_string = ''.join(random.choices( string.ascii_uppercase+string.digits, k=ssh_command_size)) # use that string in a printf statement via SSH ssh = config.sshCommand( cmd=['printf', command_string], nice=False, ionice=False, prefix=False) try: proc = subprocess.Popen(ssh, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) out, err = proc.communicate() except OSError as err: # Only handle "Argument to long error" (E2BIG) if err.errno != 7: raise err report_test( ssh_command_size, f'Python exception: "{err.strerror}". Decrease ' f'by {size_offset:,} and try again.') # test again with new ssh_command_size return probe_max_ssh_command_size( config, ssh_command_size - size_offset, size_offset) else: # Successful SSH command if out == command_string: # no increases possible anymore if size_offset == 0: report_test(ssh_command_size, 'Found correct length. Adding ' f'length of "{ssh[-2]}" to it.') # the final command size return ssh_command_size + len(ssh[-2]) # length of "printf" # there is room to increase the length report_test(ssh_command_size, f'Can be longer. Increase by {size_offset:,} ' 'and try again.') # increase by "size_offset" and try again return probe_max_ssh_command_size( config, ssh_command_size + size_offset, size_offset) # command string was too long elif 'Argument list too long' in err: report_test(ssh_command_size, f'stderr: "{err.strip()}". Decrease ' f'by {size_offset:,} and try again.') # reduce by "size_offset" and try again return probe_max_ssh_command_size( config, ssh_command_size - size_offset, size_offset) raise Exception('Unhandled case.\n' f'{ssh[:-1]}\nout="{out}"\nerr="{err}"\n' f'ssh_command_size={ssh_command_size:,}\nsize_offset={size_offset:,}')
[docs] def report_test(ssh_command_size, msg): print(f'Tried length {ssh_command_size:,}... {msg}')
[docs] def report_result(host, max_ssh_cmd_size): print(f'Maximum SSH command length between "{socket.gethostname()}" ' f'and "{host}" is {max_ssh_cmd_size:,}.')
if __name__ == '__main__': parser = argparse.ArgumentParser( description='Check the maximal ssh command size for all ssh profiles in the configurations', formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('SSH_COMMAND_SIZE', type=int, nargs='?', default=_INITIAL_SSH_COMMAND_SIZE, help='Start checking with SSH_COMMAND_SIZE as length') args = parser.parse_args() import config cfg = config.Config() profiles = cfg.profiles() # list of profile IDs # loop over all profiles in the configuration for profile_ID in profiles: cfg.setCurrentProfile(profile_ID) print(f"Profile {profile_ID} - {cfg.profileName()}: Mode = {cfg.snapshotsMode()}") if cfg.snapshotsMode() == "ssh": ssh_command_size = probe_max_ssh_command_size(cfg, args.SSH_COMMAND_SIZE) report_result(cfg.sshHost(), ssh_command_size)