Sometimes Python needs to run shell commands. Maybe you need to check disk space, restart a service, or parse system logs. The `subprocess` module lets you run any command as if you typed it in the terminal. The `os` module gives you access to environment variables, process information, and system paths. Together, they turn Python into a powerful system administration tool.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Run shell commands with subprocess.run()
  • Capture stdout, stderr, and exit codes
  • Manage environment variables with os.environ
  • Work with system paths and directories
  • Get system information (CPU, memory, disk)
  • Build a complete system health checker

2. Why This Matters

Real-world scenario: Your monitoring system needs to check disk usage across 50 servers. You could SSH into each one manually, or write a Python script that uses subprocess to run df -h and parse the output.

3. Core Concepts

Subprocess: Running Shell Commands

import subprocess

# Simple command - runs and waits
result = subprocess.run(['ls', '-la'])
print(f'Exit code: {result.returncode}')

# Capture output
result = subprocess.run(['ls', '-la'], capture_output=True, text=True)
print(f'Stdout: {result.stdout}')
print(f'Stderr: {result.stderr}')

# Check if command succeeded (raises exception on failure)
subprocess.run(['ls', '/nonexistent'], check=True, capture_output=True)

# Shell=True (use with caution!)
result = subprocess.run('ls -la | grep .py', shell=True, capture_output=True, text=True)

# Environment variables
result = subprocess.run(
    ['echo', '$HOME'],
    shell=True,
    capture_output=True,
    text=True,
    env={'HOME': '/custom/home'}
)

# Timeout
result = subprocess.run(['sleep', '10'], timeout=5, capture_output=True, text=True)

# Working directory
result = subprocess.run(['pwd'], cwd='/tmp', capture_output=True, text=True)
print(f'Current dir: {result.stdout}')
subprocess basics
# Real-world examples

# Get disk usage
df = subprocess.run(['df', '-h'], capture_output=True, text=True)
print(df.stdout)

# Check if service is running
def is_service_running(service_name):
    result = subprocess.run(
        ['systemctl', 'is-active', service_name],
        capture_output=True,
        text=True
    )
    return result.stdout.strip() == 'active'

# Get system load
load = subprocess.run(['uptime'], capture_output=True, text=True)
print(f'Load: {load.stdout.strip()}')

# Restart service
def restart_service(service_name):
    try:
        subprocess.run(['sudo', 'systemctl', 'restart', service_name], check=True)
        return True
    except subprocess.CalledProcessError:
        return False

# Get list of running processes
ps = subprocess.run(['ps', 'aux'], capture_output=True, text=True)
for line in ps.stdout.split('\n')[:10]:
    print(line)

# Find files
find = subprocess.run(
    ['find', '/var/log', '-name', '*.log', '-type', 'f', '-mtime', '-1'],
    capture_output=True,
    text=True
)
recent_logs = find.stdout.strip().split('\n')
print(f'Recent logs: {len(recent_logs)}')
Practical subprocess examples

Environment Variables

import os

# Get environment variables
home = os.environ.get('HOME', '/default/home')
path = os.environ['PATH']

# Check if variable exists
if 'DEBUG' in os.environ:
    debug_mode = os.environ['DEBUG'] == 'true'

# Set environment variable (affects current process only)
os.environ['MY_APP_ENV'] = 'production'

# Get all variables
for key, value in os.environ.items():
    print(f'{key}={value}')

# Safe access with default
database_url = os.environ.get('DATABASE_URL', 'postgresql://localhost/mydb')

# Load .env file
def load_dotenv(filepath='.env'):
    if os.path.exists(filepath):
        with open(filepath, 'r') as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith('#'):
                    key, value = line.split('=', 1)
                    os.environ[key] = value

# Example .env file content:
# DATABASE_URL=postgresql://user:pass@localhost/db
# API_KEY=abc123
# DEBUG=true
Managing environment variables

System Information with os and platform

import os
import platform
import sys

# Operating system
print(f'OS: {os.name}')           # 'posix', 'nt', 'java'
print(f'Platform: {sys.platform}') # 'linux', 'win32', 'darwin'
print(f'System: {platform.system()}')  # 'Linux', 'Windows', 'Darwin'

# CPU info
print(f'CPU count: {os.cpu_count()}')
print(f'Architecture: {platform.machine()}')

# User info
print(f'User: {os.getlogin()}')
print(f'User ID: {os.getuid()}')
print(f'Home directory: {os.path.expanduser("~")}')

# Process info
print(f'Process ID: {os.getpid()}')
print(f'Parent PID: {os.getppid()}')

# Working directory
print(f'Current directory: {os.getcwd()}')
os.chdir('/tmp')
print(f'New directory: {os.getcwd()}')

# System paths
print(f'Path separator: {os.pathsep}')
print(f'Line separator: {repr(os.linesep)}')

# Environment variables for paths
print(f'PATH: {os.environ.get("PATH")}')

# Check if running as root/sudo
def is_admin():
    if platform.system() == 'Windows':
        import ctypes
        return ctypes.windll.shell32.IsUserAnAdmin() != 0
    else:
        return os.geteuid() == 0

print(f'Is admin: {is_admin()}')
Getting system information

Working with Processes

import os
import signal
import psutil  # pip install psutil

# Get current process
process = psutil.Process()
print(f'PID: {process.pid}')
print(f'Name: {process.name()}')
print(f'Memory: {process.memory_info().rss / 1024 / 1024:.2f} MB')
print(f'CPU percent: {process.cpu_percent()}%')

# Get all processes
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']):
    try:
        print(f"{proc.info['pid']:6} {proc.info['name']:20} CPU: {proc.info['cpu_percent']:5}% MEM: {proc.info['memory_percent']:.1f}%")
    except (psutil.NoSuchProcess, psutil.AccessDenied):
        pass

# Kill a process by name
def kill_process_by_name(name):
    for proc in psutil.process_iter(['pid', 'name']):
        if proc.info['name'] == name:
            proc.kill()
            print(f'Killed {name} (PID: {proc.info["pid"]})')
            return True
    return False

# Check if process is running
def is_process_running(name):
    for proc in psutil.process_iter(['name']):
        if proc.info['name'] == name:
            return True
    return False

# Send custom signal
def send_signal(pid, sig):
    os.kill(pid, sig)

# Example: reload nginx (SIGHUP)
# find_nginx_pid() then os.kill(pid, signal.SIGHUP)
Process management with psutil

4. Complete Project: System Health Monitor

#!/usr/bin/env python3
"""
system_health.py - Comprehensive system health monitor
"""

import os
import sys
import subprocess
import json
import psutil
import platform
import argparse
from datetime import datetime
from pathlib import Path

class SystemHealth:
    """Monitor system health and resources"""
    
    def __init__(self, threshold_cpu=80, threshold_memory=80, threshold_disk=85):
        self.threshold_cpu = threshold_cpu
        self.threshold_memory = threshold_memory
        self.threshold_disk = threshold_disk
        self.alerts = []
    
    def get_cpu_info(self):
        """Get CPU information"""
        cpu_percent = psutil.cpu_percent(interval=1)
        cpu_count = psutil.cpu_count()
        cpu_freq = psutil.cpu_freq()
        
        return {
            'usage_percent': cpu_percent,
            'cores_physical': cpu_count,
            'cores_logical': psutil.cpu_count(logical=True),
            'frequency_mhz': round(cpu_freq.current, 2) if cpu_freq else None,
            'per_core': psutil.cpu_percent(interval=1, percpu=True)
        }
    
    def get_memory_info(self):
        """Get memory information"""
        mem = psutil.virtual_memory()
        swap = psutil.swap_memory()
        
        return {
            'total_gb': round(mem.total / (1024**3), 2),
            'available_gb': round(mem.available / (1024**3), 2),
            'used_gb': round(mem.used / (1024**3), 2),
            'used_percent': mem.percent,
            'swap_total_gb': round(swap.total / (1024**3), 2),
            'swap_used_gb': round(swap.used / (1024**3), 2),
            'swap_used_percent': swap.percent if swap.total > 0 else 0
        }
    
    def get_disk_info(self):
        """Get disk information"""
        disks = []
        for partition in psutil.disk_partitions():
            try:
                usage = psutil.disk_usage(partition.mountpoint)
                disks.append({
                    'device': partition.device,
                    'mountpoint': partition.mountpoint,
                    'fstype': partition.fstype,
                    'total_gb': round(usage.total / (1024**3), 2),
                    'used_gb': round(usage.used / (1024**3), 2),
                    'free_gb': round(usage.free / (1024**3), 2),
                    'used_percent': usage.percent
                })
            except PermissionError:
                continue
        
        return disks
    
    def get_network_info(self):
        """Get network information"""
        net = psutil.net_io_counters()
        interfaces = {}
        
        for name, stats in psutil.net_if_stats().items():
            interfaces[name] = {
                'is_up': stats.isup,
                'speed': stats.speed,
                'mtu': stats.mtu
            }
        
        return {
            'bytes_sent_gb': round(net.bytes_sent / (1024**3), 2),
            'bytes_recv_gb': round(net.bytes_recv / (1024**3), 2),
            'packets_sent': net.packets_sent,
            'packets_recv': net.packets_recv,
            'interfaces': interfaces
        }
    
    def get_top_processes(self, n=10):
        """Get top CPU and memory consuming processes"""
        processes = []
        for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent', 'memory_rss', 'status']):
            try:
                processes.append(proc.info)
            except (psutil.NoSuchProcess, psutil.AccessDenied):
                continue
        
        # Sort by CPU
        by_cpu = sorted(processes, key=lambda x: x.get('cpu_percent', 0), reverse=True)[:n]
        # Sort by memory
        by_memory = sorted(processes, key=lambda x: x.get('memory_percent', 0), reverse=True)[:n]
        
        return {'by_cpu': by_cpu, 'by_memory': by_memory}
    
    def get_system_info(self):
        """Get general system information"""
        boot_time = datetime.fromtimestamp(psutil.boot_time())
        uptime = datetime.now() - boot_time
        
        return {
            'hostname': platform.node(),
            'system': platform.system(),
            'release': platform.release(),
            'version': platform.version(),
            'architecture': platform.machine(),
            'python_version': sys.version,
            'boot_time': boot_time.isoformat(),
            'uptime_days': uptime.days,
            'uptime_hours': uptime.seconds // 3600,
            'load_avg': psutil.getloadavg() if hasattr(psutil, 'getloadavg') else None
        }
    
    def check_thresholds(self, data):
        """Check if any metrics exceed thresholds"""
        alerts = []
        
        # CPU check
        cpu_percent = data['cpu']['usage_percent']
        if cpu_percent > self.threshold_cpu:
            alerts.append(f"⚠️ CPU usage is {cpu_percent}% (threshold: {self.threshold_cpu}%)")
        
        # Memory check
        mem_percent = data['memory']['used_percent']
        if mem_percent > self.threshold_memory:
            alerts.append(f"⚠️ Memory usage is {mem_percent}% (threshold: {self.threshold_memory}%)")
        
        # Disk checks
        for disk in data['disks']:
            if disk['used_percent'] > self.threshold_disk:
                alerts.append(f"⚠️ Disk {disk['device']} at {disk['mountpoint']} is {disk['used_percent']}% full")
        
        return alerts
    
    def generate_report(self):
        """Generate complete health report"""
        report = {
            'timestamp': datetime.now().isoformat(),
            'system': self.get_system_info(),
            'cpu': self.get_cpu_info(),
            'memory': self.get_memory_info(),
            'disks': self.get_disk_info(),
            'network': self.get_network_info(),
            'top_processes': self.get_top_processes(5),
            'alerts': []
        }
        
        report['alerts'] = self.check_thresholds(report)
        
        return report
    
    def print_report(self):
        """Print formatted report to console"""
        report = self.generate_report()
        
        print("\n" + "="*70)
        print(f"📊 SYSTEM HEALTH REPORT - {report['timestamp']}")
        print("="*70)
        
        # System info
        sys_info = report['system']
        print(f"\n🖥️  SYSTEM")
        print(f"   Hostname: {sys_info['hostname']}")
        print(f"   OS: {sys_info['system']} {sys_info['release']}")
        print(f"   Architecture: {sys_info['architecture']}")
        print(f"   Uptime: {sys_info['uptime_days']}d {sys_info['uptime_hours']}h")
        
        # CPU
        cpu = report['cpu']
        print(f"\n💻 CPU")
        print(f"   Usage: {cpu['usage_percent']}%")
        print(f"   Cores: {cpu['cores_physical']} physical, {cpu['cores_logical']} logical")
        if cpu['per_core']:
            cores_str = ' '.join([f"{c}%" for c in cpu['per_core'][:8]])
            print(f"   Per core: {cores_str}...")
        
        # Memory
        mem = report['memory']
        print(f"\n💾 MEMORY")
        print(f"   Used: {mem['used_gb']}GB / {mem['total_gb']}GB ({mem['used_percent']}%)")
        print(f"   Available: {mem['available_gb']}GB")
        if mem['swap_total_gb'] > 0:
            print(f"   Swap: {mem['swap_used_gb']}GB / {mem['swap_total_gb']}GB ({mem['swap_used_percent']}%)")
        
        # Disk
        print(f"\n💿 DISK")
        for disk in report['disks']:
            bar = '█' * int(disk['used_percent'] / 5) + '░' * (20 - int(disk['used_percent'] / 5))
            print(f"   {disk['device']:12} [{bar}] {disk['used_percent']:.0f}%")
            print(f"      {disk['mountpoint']}: {disk['used_gb']}GB / {disk['total_gb']}GB")
        
        # Top processes
        print(f"\n📈 TOP PROCESSES BY CPU")
        for proc in report['top_processes']['by_cpu'][:5]:
            cpu_pct = proc.get('cpu_percent', 0)
            mem_pct = proc.get('memory_percent', 0)
            print(f"   {proc['name']:25} CPU: {cpu_pct:5.1f}%  MEM: {mem_pct:5.1f}% (PID: {proc['pid']})")
        
        # Alerts
        if report['alerts']:
            print(f"\n⚠️  ALERTS")
            for alert in report['alerts']:
                print(f"   {alert}")
        
        print("\n" + "="*70 + "\n")
    
    def save_report(self, filepath='health_report.json'):
        """Save report to JSON file"""
        report = self.generate_report()
        with open(filepath, 'w') as f:
            json.dump(report, f, indent=2)
        print(f"✓ Report saved to {filepath}")


def main():
    parser = argparse.ArgumentParser(description='System Health Monitor')
    parser.add_argument('--save', '-s', action='store_true', help='Save report to file')
    parser.add_argument('--output', '-o', default='health_report.json', help='Output file path')
    parser.add_argument('--threshold-cpu', type=int, default=80, help='CPU alert threshold')
    parser.add_argument('--threshold-mem', type=int, default=80, help='Memory alert threshold')
    parser.add_argument('--threshold-disk', type=int, default=85, help='Disk alert threshold')
    
    args = parser.parse_args()
    
    monitor = SystemHealth(
        threshold_cpu=args.threshold_cpu,
        threshold_memory=args.threshold_mem,
        threshold_disk=args.threshold_disk
    )
    
    monitor.print_report()
    
    if args.save:
        monitor.save_report(args.output)


if __name__ == '__main__':
    main()
Complete system health monitor

5. Common Errors & Solutions

6. Summary Checklist

7. Next Steps

Next lesson: Python Lesson 4.6: Regular Expressions (re module)

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen January 30, 2025

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment