DEV Community

TaiKedz
TaiKedz

Posted on

Bash script vs Python script for sysadmin tasks?

Which do you prefer to sling around, a bash command or a python script?

In my goal to write more python and less shell, for the benefit of my colleagues' sanity, I have been trying to favour Python - specifically, readable and maintainable python.

I have moaned before about how sysadmin scripts often look very messy and unmaintainable, and I have found a perfect example of how that arises.

I needed my team to send me the hostnames, IPs and MAC addresses of their VMs to send to IT for reserving in DHCP, and I started cooking up a python script. Just as I was finishing, I had a flash of inspiration and thought... can't I do just a one-liner in shell?

It's just a matter of getting the default IP and looking for the interface that uses it... I wrote the scirpt so that it could give result for a local host, or a series of specified remote hosts.

Which would you prefer to commit to a tooling repo? :-)

Here's the python script, idiomatic, and hopefully easy to read and maintain. I was going to tell my colleagues "Please clone (script repo) and run the script, supplying the hosts as arguments, like 'gather-ips.py jack@machine1 jack@machine2 ...' (yeah, I would need to send instructions too)

#!/usr/bin/env python

import re
import subprocess
import sys

class SubprocessError(Exception):
    pass


def run(command:list[str]) -> str:
    proc = subprocess.Popen(command, stdout=subprocess.PIPE, text=True)
    stdout, _ = proc.communicate()
    if proc.returncode > 0:
        raise SubprocessError(f"Failed {command}, see stderr.")

    return stdout


def read_ipa(ipa_lines):
    ipa_entries = {}

    current = None

    for line in ipa_lines:
        m = re.match(r"^[0-9]+:\s+(\S+?):.*", line)
        if m:
            current = m.group(1)
            ipa_entries[current] = {}
            continue
        if current is None:
            continue

        current_entry = ipa_entries[current]

        eth_m = re.match(r".+link/ether ([a-fA-F0-9:]+)", line)
        if eth_m:
            current_entry["mac"] = eth_m.group(1)

        inet_m = re.match(r"\s+inet ([0-9.]+)/[0-9]+", line)
        if inet_m:
            current_entry["ipv4"] = inet_m.group(1)

    return ipa_entries


def get_default_ip(ipr_lines):
    default_line = [line for line in ipr_lines if 'default via' in line][0]
    return re.findall(r"src [0-9.]+", default_line)[0][4:]


def get_ipra_lines(host):
    if host:
        lines = run(["ssh", host, "ip r; echo ===; ip a"]).split("\n")
        idx = 0
        for n in range(len(lines)):
            if lines[n] == "===":
                idx = n
                break
        ipr_lines = lines[:idx]
        ipa_lines = lines[idx+1:]
    else:
        ipr_lines = run(["ip", "r"]).split("\n")
        ipa_lines = run(["ip", "a"]).split("\n")

    return ipr_lines,ipa_lines


def ublat(host_string:str):
    try:
        i = host_string.index("@")
        return host_string[i+1:]
    except ValueError:
        return host_string


def get_target(host=None):
    ipr_lines, ipa_lines = get_ipra_lines(host)
    entries = read_ipa(ipa_lines)
    def_ip = get_default_ip(ipr_lines)
    hostname = ublat(host) if host else run(["hostname"]).strip()

    for _, data in entries.items():
        if def_ip in data.get("ipv4", ""):
            print(f"{hostname}\t{data.get('ipv4')}\t{data.get('mac')}")
            return
    print(f"# Nothing in {entries}")


def main():
    if not sys.argv[1:] or "get" in sys.argv[1:]:
        get_target()
        return

    for host in sys.argv[1:]:
        get_target(host)


main()
Enter fullscreen mode Exit fullscreen mode

For sake of comparison, this is its bash equivalent - it does EXACTLY the same thing:

command="echo \"\$(hostname)  \$(ip a|grep \"\$(ip r|grep 'default via'|grep -Po '(?<=src )[0-9.]+')\" -B 1|grep -Po '(?<=ether |inet )(\S+)')\"|xargs echo"

if [[ -z "$*" ]]; then
    bash -c "$command"
    exit
fi

for node in "$@"; do 
    ssh "$node" "$command"
done
Enter fullscreen mode Exit fullscreen mode

In the end, I sent a message to the team and told them:

Hi all,

Please run this on each of your VMs and send me the output via email

echo "$(hostname) $(ip a|grep "$(ip r|grep 'default via'|grep -Po '(?<=src )[0-9.]+')\" -B 1|grep -Po '(?<=ether |inet )(\S+)')"|xargs echo

A one-liner in a single message. No sending scripts around. No instructing to clone and execute. Just "run this on each machine."

Which approach would you have taken

Top comments (4)

Collapse
 
moopet profile image
Ben Sinclair

Personally I tend to stick with shell scripts, and make them POSIX if possible. Then they'll work anywhere (anywhere decent...)

If they're more than 50 lines or so, they probably need to be driven by something else, like a Makefile, or replaced with a Python (or similar) script, but that's a dodgy rule of thumb. Really it's down to what kind of stuff you need to do. Non-trivial string manipulation? API libraries? Probably Python.

In your examples, the Python script just ends up running system commands anyway for part of its job :)

The bash script however has reached the point of obscurity in terms of escaped characters and regex. I'd maybe go with awk for that sort of thing - I'm not an awk person so I'd have a bit of trial-and-error, but I imagine it'd be easier to maintain once it's made.

Collapse
 
taikedz profile image
TaiKedz

point of obscurity

I like that.

The bash script is a terse one-liner, but it is as impenetrable as a rock in a mud puddle... it's a counterpoint to the "more than 50 lines" heuristic, which I don't like because.... I could have done this thing in about 5 lines, assuming I forcibly broke at semi-colons... and it would be a most ... emetic piece of code.

The python script goes out of its way to be "readable" whereas the bash script makes no attempt to do so.

I could have added an example of a more readable bash script" which would have spanned quite a few more lines.... but then if the "obscurity" were neatly avoided, perhaps keeping to bash even with a few dozen lines would be acceptable...

Collapse
 
taikedz profile image
TaiKedz

Interesting. I posted this with the tags bash, python, sysadmin, linux

A few minutes later I checked my profile posts... and its tags had been changed to discuss, automation, python, linux

I suspect AI schenanigans trying to tell me what I meant.

WHAT THE ACTUAL.

Collapse
 
sdh0x profile image
sdh0x

there is rule that i like , it says , if the bash script is longer than 50 lines , rewrite in code

and because i used to do some web scrapping with sed awk rg grep and automated web app penetration testing for endpoit and information disclosure discovery and many linux utilities ill say that bash is horrible at syntaxt, like the difference between using "" and '' makes me lose my sanity especially when the script gets nested, so stick to python if your script is going to be longer than 50 lines