Skip to the content.

SSHScript v2.0 Threading Support

Last Updated on 2023/10/20

Back to Index

Topics

🔵 Every thread has an effective SSHScript session

image

The initial session (an instance of SSHScriptSession) is created for the main thread. All commands executed by the initial session are executed on the localhost using the subprocess module. This includes one-dollar, two-dollar, with-dollar commands as well as dollar properties, such as $.stdout, $.stderr and $.exitcode.

For example, the following command would be executed by the subprocess module:

$hostname

This is because the effective session is the initial session of the main thread.

If the initial session is connected to a remote server, a new SSHScriptSession instance is returned by the $.connect() method. This new session becomes the effective session of the main thread.

For example, the “hostname” command would be executed by the Paramiko module:

with $.connect('user@remotehost'):
    $hostname

This is because the effective session is the new SSHScriptSession instance returned by $.connect().

SSHScript attaches every dollar-command to a session to execute it. To do this, SSHScript binds a session to every thread. This session is the effective session of the thread.

Every thread carries a stack to hold sessions. Initially, the stack of the main thread has one element: the initial session.

When a new connection is created, a new session is created and placed on top of the stack to become the effective session. This session is removed from the stack when it is closed.

## For example, the following code would execute the commands on localhost using the subprocess module:
$hostname
$whoami

## Then, the new session becomes the effective session of the main thread:
with $.connect('user@remotehost'):
    $hostname
    $whoami

## Finally, the initial session would execute the commands on localhost using the subprocess module again:
$hostname
$whoami

## This is because the connection to the remote host is closed.

🔵 The last connection is the effective session

The effective session is the last connection made to a remote host.

SSHScript 2.0 supports connecting to multiple remote hosts at the same time. When you connect to multiple hosts, the effective session is the session that was last connection to a remote host.

Here is an example:

def get_hostname():
    $hostname
    return $.stdout.strip()

localhostname = get_hostname()
 
## connect to the bridge host 
$.connect('user@bridge1')
$.connect('user@bridge2')
$.connect('user@bridge3')

## the effective session is the last connection.
hostname = get_hostname()
assert  hostname == 'bridge3'

## close the seession to bridge3
$.close()

## the effective session is the bridge2
hostname = get_hostname()
assert  hostname == 'bridge2'

## close the seession to bridge2
$.close()

## the effective session is the bridge1
hostname = get_hostname()
assert  hostname == 'bridge1'

$.close()

## this would be localhost's hostname
assert localhostname == get_hostname()

🔵 Let functions come into play

We often need to execute the same routines on multiple hosts. We can use functions to do this, which makes it easy to update the routine and apply it to all hosts.

Here is an example:

def get_date():
    $date
    profile = {'dat':$.stdout.strip()}
    return profile

profile = {'localhost':get_date()}
accounts = ['user@hostA','user@hostB']
for account in accounts:
    with $.connect(account):
        profile[account] = get_date()

We can easily update the get_date() function to perform a different task on all of the remote hosts. For example, we could update the function to install a new software package or to start a new service.

Using functions to execute routines on multiple hosts is a powerful way to automate system administration tasks.

🔵 Let Threads come into play

There are a few reasons why we might use threads:

Here is an example:

def get_date(profile):
    $date
    profile['date'] = $.stdout.strip()

def get_diskspace(profile):
    $df
    profile['df'] = $.stdout.strip()

def connect(account,profile):
    with $.connect(account) as remote:
        $hostname
        hostname = $.stdout.strip()
        profile[hostname] = {}
        get_date(profile[hostname])
        get_diskspace(profile[hostname])

profile = {}
accounts = ['user@hostA','user@hostB']
threads = []
for account in accounts:
    thread = $.thread(target=get_profile,args=[account,profile])
    thread.start()
    threads.append(thread)
for thread in threads:
    thread.join()
print(profile)

In the above example, the get_date() and get_diskspace() functions were called by different threads. This is okay because the effective sessions of the threads are different.

🔵 session.bind(),session.thread(): Binding a session to threads or functions

When a session is connected to multiple hosts, the effective session is the last connection made.

If you want to execute commands on a previous connection, you can use the session.bind() method. This method allows you to arbitrarily assign the effective session of a thread or function.

Here is an example.

def get_hostname():
    $hostname
    return $.stdout.strip()
    
def get_date(profile):
    $date
    profile['date'] = $.stdout.strip()
    return profile

def zone_job(accounts,zoneprofile):
    for account in accounts:
        with $.connect(account):
            get_date(zoneprofile)

profile = {'zone1':{}, 'zone2':{}, 'zone3':{}}
## these hosts are behind bridge1
accountsZone1 = ['user@zone1HostA','user@zone1HostB']
## these hosts are behind bridge2
accountsZone2 = ['user@zone2HostA','user@zone2HostB']
## these hosts are behind bridge3
accountsZone3 = ['user@zone3HostA','user@zone3HostB']

## connect to the bridge host 
bridgeSession1 = $.connect('user@bridge1')
bridgeSession2 = $.connect('user@bridge2')
bridgeSession3 = $.connect('user@bridge3')

## the effective session is the last connection made.
hostname = get_hostname()
assert  hostname == 'bridge3'

## session.bind(func) returns a new function
## that calls func() by taking bridgeSession1 as its effective session.
hostname = bridgeSession1.bind(get_hostname)()
assert  hostname == 'bridge1'
hostname = bridgeSession2.bind(get_hostname)()
assert  hostname == 'bridge2'

## the effective session of thread1 is the last connection made (aka. bridgeSession3)
thread1 = threading.thread(target=zone_job,args=[accountsZone1,profile['zone1']])
## session.bind(thread) would set the binding session to be the effective session of this thread
bridgeSession1.bind(thread1)

## session.thread() is a handly function for doing the same thing.
thread2 = bridgeSession2.thread(target=zone_job,args=[accountsZone2,profile['zone2']])

## the effective session of thread3 is the last connection made (aka. bridgeSession3)
thread3 = threading.thread(target=zone_job,args=[accountsZone3,profile['zone3']])

thread1.start()
thread2.start()
thread3.start()
thread1.join()
thread2.join()
thread3.join()
bridgeSession1.close()
bridgeSession2.close()
bridgeSession3.close()
print(profile)

This image shows the relationship between hosts image

This image shows the relationship between threads and sessions. image

The session.bind() method is a powerful tool that can be used to control the effective session of a thread or function. This can be useful for a variety of tasks, such as executing commands on specific hosts or debugging multithreaded code.

( This article was written with the help of Google Bard, which is awesome! )