Following up on last week’s Linux and Git foundations, Week 2 shifts focus from atomic commands to building resilient systems automation. While deploying these local configurations, my own automated alerting utility surfaced a physical storage constraint on the host node, turning a routine lab into a real-world Logical Volume Management (LVM) engineering exercise
Optimizing Remote Terminal Management
Operating directly inside hypervisor console interfaces slows execution down due to poor clipboard integration and manual entry bottlenecks. To mimic true remote enterprise server management, I deployed an OpenSSH server on the guest Linux instance to allow clean access via PuTTY from my Windows workstation:
sudo apt install -y openssh-server sudo systemctl enable --now ssh ip -4 a Then in PuTTY: the VM's IP address, save the session, connect. Right-click pastes, and selecting text copies. It's the same machine as the VMware console, so anything I create in one window shows up in the other. This is also how remote servers are managed in real jobs.
Three scripts
All three use set -euo pipefail, check their inputs and exit with a clear message and a non-zero exit code when something is wrong.
1. Disk check
#!/usr/bin/env bash set -euo pipefail threshold="${1:-80}" usage=$(df / --output=pcent | tail -1 | tr -dc '0-9') echo "Root disk usage: ${usage}%" if (( usage >= threshold )); then echo "WARNING: usage is at or above ${threshold}%" >&2 exit 1 fi echo "OK: below ${threshold}%" ${1:-80} means "use the first argument, or 80 if none was given". I tested it with a threshold of 1 to force the warning, and confirmed the exit code was 1.
It reads 49% now, because I’ve since installed Terraform and the Azure CLI. That’s the headroom the resize gave me
2. Multi-file log summary
A for loop over every file passed in, skipping missing ones with a message instead of crashing. It prints how many ERROR and INFO lines each file has.
3. Timestamped backup
Takes a source folder, builds a .tar.gz named with the date and time, and refuses to run if the source folder doesn't exist.
Linting with ShellCheck
sudo apt install -y shellcheck shellcheck scripts/*.sh No output means no warnings, and all three scripts passed. It's a quick habit that teams use on every Bash script.
The problem my script found
I ran my disk-check script for real, and it said my root disk was 85% full. For a VM that will run Terraform, Docker and Kubernetes tools, that's a problem waiting to happen.
I looked for what was using space:
sudo du -xh / --max-depth=1 2>/dev/null | sort -h | tail -8 sudo apt autoremove -y && sudo apt clean sudo journalctl --vacuum-size=100M That only got me from 85% to 76%. Nothing large was wasting space. So I looked at the disk layout instead:
lsblk sudo vgs The disk (sda) was 20 GB, but the logical volume holding my filesystem was only 10 GB. vgs showed 8.22 GB free in the volume group. Ubuntu's installer had left that space unallocated.
Before the fix I saw a 10 GB logical volume with 8.22 GB free in the volume group. I didn’t screenshot that, so here is the state after.
One command fixed it:
bash sudo lvextend -r -l +100%FREE /dev/ubuntu-vg/ubuntu-lv df -h / -r resizes the filesystem in the same step. The volume went from 10 GB to 18 GB, and usage dropped to 42%, with no reboot and nothing deleted.
That reads 49% now, for the same reason.
Reading my own network
I ran these on the VM and read the output:
ip a # my address: 192.168.93.132/24 ip route # default gateway: 192.168.93.2 ping -c 3 8.8.8.8 # 0% loss, about 17 ms average dig github.com +short # DNS answer: 20.26.156.215 curl -I https://github.com # HTTP/2 200 sudo ss -tulpn # which ports are listening, and to whom Things I learned from the output:
-
/24means 24 bits identify the network, which leaves 8 bits for hosts: 256 addresses. -
default via 192.168.93.2is the gateway all non-local traffic goes through. -
curl -Ifetches only the headers.200means OK, and GitHub's response even showed it was served from the UK South Azure region. - In
ss -tulpn,0.0.0.0:22(SSH) is reachable from the network, while127.0.0.1:631(printing) is reachable only from the machine itself. Which ports are exposed to whom is exactly what an Azure network security group controls.
Subnet maths (worked by hand)
- A
/16VNet holds 2^16 = 65,536 addresses. - A
/24subnet holds 256 addresses, but Azure reserves 5 (network address, default gateway, two for Azure DNS, and broadcast), so 251 are usable. - A /16 contains 2^(24-16) = 256 /24 subnets.
I wrote this up in my own words in docs/networking-notes.md in the repo, explaining VNets, subnets, NSGs and DNS the way I'd explain them to a colleague.
What went wrong
- A typo in a command (
cujrlinstead ofcurl) gave me "command not found, did you mean curl". Linux's suggestions are worth reading. - Typos like that cost me less time once I started using Tab for completion.
What I learned
- Scripts that check their inputs and set exit codes are the foundation of automation.
- Running my own script on a real machine found a real problem. That's more convincing than a tutorial exercise.
- When cleanup doesn't help, look at the layout. Free space can exist in a layer you weren't looking at.
- Reading
ss,ip routeanddigoutput is a skill, and the Azure networking I already know maps onto it directly.
What's next
Week 3: Terraform. I'll create Azure resources from code, move state into an Azure storage account, and build a virtual network and subnet for the project.
Repo: link
See you on the next one...





