SSHScript v2.0 Threading Support
Last Updated on 2023/10/20
Topics
- Every thread has an effective SSHScript session
- Let functions come into play
- Let Threads come into play
- session.bind(),session.thread(): Binding a session to threads or functions
🔵 Every thread has an effective SSHScript session
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:
- To improve performance: Threads can be used to execute multiple tasks simultaneously, which can improve the overall performance of a program.
- To simplify code: Threads can be used to make code more modular and easier to understand.
- To handle multiple network connections simultaneously
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
This image shows the relationship between threads and sessions.
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! )