Package: remoto
remotoA very simplistic remote-command-executor using connections to hosts (ssh,
local, containers, and several others are supported) and Python in the remote
end.All the heavy lifting is done by execnet, while this minimal API provides the
bare minimum to handle easy logging and connections from the remote end.remotois a bit opinionated as it was conceived to replace helpers and
remote utilities forceph-deploy, a tool to run remote commands to configure
and setup the distributed file system Ceph.ceph-medicuses remoto as well to inspect Ceph
clusters.Example UsageThe usage aims to be extremely straightforward, with a very minimal set of
helpers and utilities for remote processes and logging output.The most basic example will use therunhelper to execute a command on the
remote end. It does require a logging object, which needs to be one that, at
the very least, has botherroranddebug. Those are called forstderrandstdoutrespectively.This is how it would look with a basic logger passed in:>>> conn = remoto.Connection('hostname')
>>> run(conn, ['ls', '-a'])
INFO:hostname:Running command: ls -a
DEBUG:hostname:.
DEBUG:hostname:..
DEBUG:hostname:.bash_history
DEBUG:hostname:.bash_logout
DEBUG:hostname:.bash_profile
DEBUG:hostname:.bashrc
DEBUG:hostname:.lesshst
DEBUG:hostname:.pki
DEBUG:hostname:.ssh
DEBUG:hostname:.vim
DEBUG:hostname:.viminfoTherunhelper will display thestderrandstdoutasERRORandDEBUGrespectively.For other types of usage (like checking exit status codes, or raising upon
them)remotodoes provide them too.Remote Commandsprocess.runCalling remote commands can be done in a few different ways. The most simple
one is withprocess.run:>>> from remoto.process import run
>>> from remoto import connection
>>> Connection = connection.get('ssh')
>>> conn = Connection('myhost')
>>> run(conn, ['whoami'])
INFO:myhost:Running command: whoami
DEBUG:myhost:rootNote however, that you are not capturing results or information from the remote
end. The intention here is only to be able to run a command and log its output.
It is afire and forgetcall.process.checkThis callable, allows the caller to deal with thestderr,stdoutand
exit code. It returns it in a 3 item tuple:>>> from remoto.process import check
>>> check(conn, ['ls', '/nonexistent/path'])
([], ['ls: cannot access /nonexistent/path: No such file or directory'], 2)Note that thestdoutandstderritems are returned as lists with the\ncharacters removed.This is useful if you need to process the information back locally, as opposed
to just firing and forgetting (while logging, likeprocess.run).Remote FunctionsThere are two supported ways to execute functions on the remote side. The
library thatremotouses to connect (execnet) only supports a few
backendsnatively, andremotohas extended this ability for other backend
connections like kubernetes.The remote function capabilities are provided byLegacyModuleExecuteandJsonModuleExecute. By default, bothsshandlocalconnection will
use the legacy execution class, and everything else will use thelegacyclass. Thesshandlocalconnections can still be forced to use the new
module execution by setting:conn.remote_import_system = 'json'jsonThe default module fordocker,kubernetes,podman, andopenshift. It does not require any magic on the module to be executed,
however it is worth noting that the librarywilladd the following bit of
magic when sending the module to the remote end for execution:if __name__ == '__main__':
    import json, traceback
    obj = {'return': None, 'exception': None}
    try:
        obj['return'] = function_name(*a)
    except Exception:
        obj['exception'] = traceback.format_exc()
    try:
        print(json.dumps(obj).decode('utf-8'))
    except AttributeError:
        print(json.dumps(obj))This allows the system to executefunction_name(replaced by the real
function to be executed with its arguments), grab any results, serialize them
withjsonand send them back for local processing.If you had a function in a module namedfoothat looks like this:import os

def listdir(path):
    return os.listdir(path)To be able to execute thatlistdirfunction remotely you would need to pass
the module to the connection object and then call that function:>>> import foo
>>> conn = Connection('hostname')
>>> remote_foo = conn.import_module(foo)
>>> remote_foo.listdir('.')
['.bash_logout',
 '.profile',
 '.veewee_version',
 '.lesshst',
 'python',
 '.vbox_version',
 'ceph',
 '.cache',
 '.ssh']Note that functions to be executed remotelycannotaccept objects as
arguments, just normal Python data structures, like tuples, lists and
dictionaries. Also safe to use are ints and strings.legacyWhen using thelegacyexecution model (the default forlocalandsshconnections), modules are required to add the following to the end of
that module:if __name__ == '__channelexec__':
    for item in channel:
        channel.send(eval(item))This piece of code is fully compatible with thejsonexecution model, and
would not cause conflicts.Automatic detection for ssh connectionsThere is automatic detection for the need to connect remotely (via SSH) or not
that it is infered by the hostname of the current host (vs. the host that is
connecting to).If the local host has the same as the remote hostname, a local connection (viaPopen) will be opened and that will be used instead ofssh, and avoiding
the issues of being able to ssh into the same host.Automatic detection for usingsudoThis magical detection can be enabled by using thedetect_sudoflag in theConnectionclass. It is disabled by default.When enabled, it will prefix any command withsudo. This is useful for
libraries that need super user permissions and want to avoid passingsudoeverywhere, which can be non-trivial if dealing withrootusers that are
connecting via SSH.