Automated bulk command execution on SSH enabled hosts and devices

In the lockdown period, during pandemic, there is nothing much to do over weekend 😏.

Was just thinking of writing ✍️ something, something useful…. then came up with an idea to address a common problem that many sysadmins and netadmins come across.

At times, we want to run set of commands on many devices, hosts to perform some bulk changes, like run package updates, or get some information, like disk space, from set of servers.

To achieve this, I have designed a simple json schema which has predefined credential sets (along with sudo/enable password option), command alias and hosts mapped with credential set and list of command aliases.

Also, I have added multiprocessing in code to run commands concurrently on “maxhosts” defined in json file.

Here is config.json which defined all above stuff, pretty simple to understand.

config.json:


{
   "maxhosts": 5,
   "credsets": {
                 "set1": { "username": "manish", "password": "abcd", "enpass": "xyz123" },
                 "set2": { "username": "pi", "password": "Hello!" }
             },
   "cmds": {
             "listfiles": ["ls -l /home", 2],
             "diskspace": ["df -h", 1],
             "uptime": ["uptime", 1],
             "updatedebian": ["apt-get update && apt-get upgrade -y", 10],
             "DebVer": ["cat /etc/debian_version", 1]
           },
   "hosts": {
               "192.168.0.6": {"type": "linux", "credset": "set1", "cmds": ["updatedebian", "DebVer"] },
               "192.168.0.7": {"type": "linux", "credset": "set1", "port": 1223, "cmds": ["updatedebian"] }
            },
   "logging": {
                "logfile": "/home/manish/pymgt/test.logs"
              }
}

Above config.json is picked by following python code to first extract list of servers/hosts. Then get associated credential and command list to be executed. Each command has delay factor in json (see netmiko documentation), default delay is about 100 sec, so if a command has delay factor of 3, then delay will be close to 300 sec. This is useful when you are performing upgrades etc. Output of login activity, command executions can be seen on standard output and later in logfile (defined in config.json).

pymgt.py:

#!/usr/bin/python3
from netmiko import ConnectHandler
from multiprocessing import Pool
import os
import json
from time import strftime, localtime
import sys

try:
   params = sys.argv[1]
   param, value = params.split('=')
   if param != "--play":
      sys.exit()
   playconf = value
except:
   print("Usage: pymgt.py --play=<json config>")
   sys.exit()

with open(playconf) as cfg:
  cfgdata = json.load(cfg)

def CommitLogs(LogMessage):
    logfile = cfgdata.get('logging').get('logfile')
    try:
         fopen = open(logfile, "a")
         try:
            fopen.write(LogMessage+"\n")
            fopen.close()
         except:
            print("Failed to write ",LogMessage)
         return
    except:
         print("failed to open file", logfile)
    return

def injectcmds(device, cmds):
   try:
      net_connect = ConnectHandler(**device)
      try:
           LogMessage = strftime("%Y-%m-%d %H:%M:%S", localtime())+" "+device.get('host')+" Logged in..."
           print(LogMessage)
           CommitLogs(LogMessage)
           if device.get('secret') is not None:
               net_connect.enable()
               LogMessage = strftime("%Y-%m-%d %H:%M:%S", localtime())+" "+device.get('host')+" Enabled Elevated access...."
               print(LogMessage)
               CommitLogs(LogMessage)
           sshstatus = 0
      except:
           LogMessage = strftime("%Y-%m-%d %H:%M:%S", localtime())+" "+device.get('host')+";Cannot gain elevated access...."
           print(LogMessage)
           CommitLogs(LogMessage)
           sshstatus = -1

   except:
      LogMessage = strftime("%Y-%m-%d %H:%M:%S", localtime())+" "+device.get('host')+";SSH failed as "+device.get('username')
      print(LogMessage)
      CommitLogs(LogMessage)
      sshstatus = -1

   if (sshstatus == 0):
      for cmd in cmds:
        waittime = 10
        cmdobj = cfgdata.get('cmds').get(cmd)
        cmd, delayfac = cmdobj[0],cmdobj[1]
        print(cmd, delayfac)
        try:
          status = net_connect.send_command(cmd, delay_factor=delayfac)
          LogMessage = strftime("%Y-%m-%d %H:%M:%S", localtime())+" "+device.get('host')+" Command:"+cmd+"\nOutput:"+status
          print(LogMessage)
          CommitLogs(LogMessage)
        except:
          LogMessage = strftime("%Y-%m-%d %H:%M:%S", localtime())+" "+device.get('host')+" "+cmd+" command failed"
          print(LogMessage)
          CommitLogs(LogMessage)
   return

def hostcmd(host):
  hostdata = cfgdata.get('hosts').get(host)
  type = hostdata.get('type')
  cmds = hostdata.get('cmds')
  port = hostdata.get('port')
  if port is None:
    port = 22
  credset = hostdata.get('credset')

  credparams = cfgdata.get('credsets').get(credset).keys()
  username = cfgdata.get('credsets').get(credset).get("username")
  password = cfgdata.get('credsets').get(credset).get("password")
  if "enpass" in credparams:
     enpass = cfgdata.get('credsets').get(credset).get("enpass")
  else:
     enpass = None

#  print("Enpass", host, enpass)
  if enpass is None:
      device = {
         'device_type': type,
         'host': host,
         'port': port,
         'username': username,
         'password': password,
      }
  else:
      device = {
         'device_type': type,
         'host': host,
         'port': port,
         'username': username,
         'password': password,
         'secret': enpass,
      }
  injectcmds(device, cmds)

hostlist = list(cfgdata.get('hosts').keys())
maxhosts = cfgdata.get('maxhosts')
with Pool(maxhosts) as p:
    p.map(hostcmd, hostlist)

Lets execute it


manish@hq:~/pymgt $ ./pymgt.py --play=/home/manish/pymgt/config.json
2020-04-12 08:40:56 192.168.0.6 Logged in...
2020-04-12 08:40:57 cloud.mka.in Logged in...
2020-04-12 08:40:58 192.168.0.6 Enabled Elevated access....
apt-get update && apt-get upgrade -y 10
2020-04-12 08:40:59 cloud.mka.in Enabled Elevated access....
apt-get update && apt-get upgrade -y 10
........
........

Here is the sample log output saved in test.log (defined in config.json)


cat test.logs
2020-04-04 18:39:03 192.168.0.7 Logged in...
2020-04-04 18:39:03 192.168.0.6 Logged in...
2020-04-04 18:39:04 192.168.0.7 Command:df -h
Output:Filesystem      Size  Used Avail Use% Mounted on
/dev/root       7.1G  3.6G  3.3G  52% /
devtmpfs        459M     0  459M   0% /dev
tmpfs           464M  115M  349M  25% /dev/shm
tmpfs           464M  6.4M  457M   2% /run
tmpfs           5.0M  4.0K  5.0M   1% /run/lock
tmpfs           464M     0  464M   0% /sys/fs/cgroup
/dev/mmcblk0p1  253M   53M  200M  21% /boot
tmpfs            93M     0   93M   0% /run/user/1000
tmpfs            93M     0   93M   0% /run/user/999
tmpfs            93M     0   93M   0% /run/user/1001
2020-04-04 18:39:04 192.168.0.6 Enabled Elevated access....
2020-04-04 18:39:05 192.168.0.6 Command:ls -l /home
Output:total 8
drwxr-xr-x  7 manish manish 4096 Apr  4 16:17 manish
drwxr-xr-x 17 pi     pi     4096 Apr  4 15:29 pi
2020-04-04 18:39:06 192.168.0.6 Command:uptime
Output: 18:39:06 up  2:27,  6 users,  load average: 2.65, 2.56, 2.65

Hope readers it useful :-), specially sysadms and netadms.