Part-7: Python for DevOps Automation

Build a Python health checker for disk space, APIs, and JSON reports.

Introduction

30 Second Summary

When a computer or online service starts misbehaving, the useful clues rarely arrive in one neat place. You can lose time switching between checks while the cause stays hidden.

In this project, you will build a Python System Health Checker that combines a Linux disk check with a public REST API check. Each run saves a JSON report that stays useful when a check fails.

What You'll Build

A single run gives you an immediate system status plus a readable health_report.json with the evidence behind it.

By the end of this project, you'll have:

  • An immediate health summary that reports healthy or degraded after every run.
  • A readable health report containing the captured disk table plus the API status code, reported origin, endpoint URL, and overall result.
  • A graceful failure path that records useful error data when the disk command or API check fails.
  • Secret Mission: Add an optional --output argument so each run can save its report to a chosen file.

Are there any prerequisites?

You should have completed Parts 1 through 6.

You also need the Linux environment listed in the project prerequisites.

Before We Start

A clear goal keeps every command and API check connected to a real DevOps outcome. This is your moment to commit to building a Linux System Health Checker that inspects root filesystem disk space, checks a public endpoint, handles failures, and saves a JSON troubleshooting report.

Set Up an Isolated Python Environment

Your System Health Checker needs the Requests library. Installing it into the base Python environment can change packages used by other tools.

A virtual environment gives this project its own dependency toolbox. That isolation keeps Requests 2.34.2 reproducible without changing the base installation.

In this step, get ready to:
  • Verify the local prerequisites.
  • Activate an isolated virtual environment.
  • Install the pinned Requests dependency.
Check the local prerequisites

The checker needs Python 3.10 or newer. The df command from GNU Coreutils must also return information about the root filesystem.

  • Check your Python version by running this command:
python3 --version

What should I see?

The command prints the version used by python3. A result of Python 3.10 or higher meets the Requests requirement.

🙋‍♀️ Seeing an older Python version?

An older version cannot install the pinned Requests release used in this project.

  • Use your Linux distribution's official installation guide to install Python 3.10 or newer.
  • Run the version check again after the installation completes.

Help me resolve my Python version check

  • Confirm the root filesystem command works by running:
df -h /

What does this command prove?

You should see a filesystem table with human-readable disk sizes. This confirms the command needed by the health checker is available.

🙋‍♀️ Do not see a filesystem table?

  • Check that you included a space before the final slash.
  • Confirm that your terminal runs GNU Coreutils commands.

Help me troubleshoot the disk command

Create and activate the environment

The environment needs a dedicated folder so its dependencies stay together. You will use system-health-checker for the project.

  • Create an empty folder named system-health-checker on your Desktop with your Linux file manager.
  • Launch a terminal inside system-health-checker with your file manager's terminal shortcut.

Your terminal is now working inside the empty system-health-checker folder.

  • Create the isolated environment by running this command:
python3 -m venv .venv

What does this command do?

The venv module builds a project-specific Python installation inside .venv. Your base Python packages stay unchanged.

  • Activate the new environment by running this command:
source .venv/bin/activate

How do I know it is active?

Your shell prompt should now begin with (.venv). Commands using python now run through the isolated interpreter.

🙋‍♀️ Environment creation failed?

  • Confirm that your Linux installation includes the Python venv module.
  • Check that the terminal is inside system-health-checker.
  • Run the activation command again after .venv has been created.

Help me activate my virtual environment

Pin and verify Requests

A dependency pin records the exact library release required by the project. Anyone can use the same file to recreate your environment.

  • Create requirements.txt inside system-health-checker with your standard Linux text editor.
  • Add the dependency pin below:
requests==2.34.2

What does this pin control?

The double equals sign fixes Requests at version 2.34.2. Reinstalling from this file produces the same dependency version.

  • Save requirements.txt.
  • Choose a tab below to confirm the saved file.

✔️ Awesome, I've got everything!

Your dependency pin is saved in requirements.txt.

ⓧ I'd like to double check the full code

requests==2.34.2

Why is this the complete file?

This project needs one external dependency. The single line records its exact release.

Does the file look different?

  • Remove any spaces around the double equals sign.
  • Check that the filename is exactly requirements.txt.
  • Confirm that your editor did not add another file extension.

Help me check my requirements file

  • Install the pinned dependency into the active environment by running:
python3 -m pip install -r requirements.txt

What does this command do?

The command asks the active Python interpreter to run pip. Pip reads requirements.txt before installing the pinned Requests release.

🙋‍♀️ Requests did not install?

  • Confirm that your prompt still begins with (.venv).
  • Check that requirements.txt is saved inside system-health-checker.
  • Confirm that your internet connection can reach the package registry.

Help me troubleshoot the Requests installation

Before you run the final check, take a moment to predict which Python environment loads Requests.

  • Verify the isolated Requests import by running this command:
python -c "import requests; print('Requests import OK')"

What should I see?

You should see Requests import OK in the terminal. This proves the active virtual environment can load the installed library.

🙋‍♀️ Import check failed?

  • Reactivate .venv with the activation command from this step.
  • Run the installation command again inside the active environment.
  • Repeat the import check after the installation succeeds.

Help me fix the Requests import

That is the setup complete. Your checker now has an isolated Python environment with its API client ready to import.

Your environment is ready. Next up, you will run a Linux disk command from Python and capture its output.

Capture Disk Command Output

Your isolated Python environment is ready for the first real health signal. The checker starts with disk usage from Linux.

A command result contains captured text plus a process exit code. This first version stores the result so you can discover how an unsuccessful command behaves.

In this step, get ready to:
  • Capture the disk table with Python's subprocess module.
  • Read captured streams from a CompletedProcess result.
  • Test how a missing filesystem path changes the command result.
Capture the root filesystem table

The subprocess.run() function starts another program from Python. It waits for that program to finish before returning a result.

  • Use your Linux text editor to create system_health.py in the current project directory with the code below:
import subprocess

result = subprocess.run(
    ["df", "-h", "/"],
    capture_output=True,
    text=True,
)
print(result.stdout.strip())
print("Return code:", result.returncode)

What does this code do?

  • The command list keeps each terminal token separate.
  • The capture_output=True setting stores both command streams in the result.
  • The text=True setting returns readable strings.
  • The result.returncode value records whether the command succeeded.
  • Save system_health.py.

Before you run it, consider whether the valid root filesystem check returns zero.

  • Run the checker from the active virtual environment by running this command:
python system_health.py

What should you see?

You'll see a filesystem table beginning with a heading such as Filesystem. The final line shows Return code: 0.

Good progress. Python now holds the disk table as data you can inspect.

Missing the filesystem table?

Confirm that your terminal is in the same project directory as system_health.py.

Check that the command list still reads ["df", "-h", "/"].

Help me troubleshoot the missing disk table.

Inspect captured streams

The standard output stream carries the normal disk table. The standard error stream carries command diagnostics.

  • In system_health.py, replace the root path / with /tmp.
  • Save system_health.py.
  • Test the alternate valid path by running this command:
python system_health.py

What changed?

You'll see a row for the filesystem containing /tmp. The return code remains 0 because the path is valid.

A successful command usually leaves stderr empty. Printing that stream makes its captured value visible.

  • Replace the temporary /tmp version with this root-path version that also prints captured standard error:
import subprocess

result = subprocess.run(
    ["df", "-h", "/"],
    capture_output=True,
    text=True,
)
print(result.stdout.strip())
print(result.stderr.strip())
print("Return code:", result.returncode)

Why print standard error?

The extra print line reads result.stderr from the completed process. A successful root check stores an empty string there.

The empty string produces a blank line before the return code. That blank line confirms that Python captured the stream.

  • Save system_health.py.
  • Display the captured standard-error stream by running this command:
python system_health.py

What should the streams contain?

You'll see the disk table from stdout. You'll see a blank line from the empty stderr stream.

The final line still shows Return code: 0.

🙋‍♀️ Seeing an error on the valid path?

Confirm that the command list targets /. A leftover path typo sends diagnostics to stderr.

Help me inspect the captured command streams.

Reveal the missing-path gap

The same result object can hold diagnostics for an unavailable filesystem path. Both print lines make the outcome observable.

  • In system_health.py, replace / with /path-that-does-not-exist.
  • Save system_health.py.

Before you run it, consider whether Python stops before printing the return code.

  • Test the missing path by running this command:
python system_health.py

What did the failed command return?

  • The captured stdout value is empty.
  • The captured stderr value contains an error naming the missing path.
  • The return code is a nonzero integer.
  • The script still reaches every print statement.

You found the gap this version was designed to expose. The captured error preserves the evidence.

Why didn't Python stop?

The subprocess.run() call uses the default check=False behavior. Python returns the completed result even when the command reports a nonzero exit code.

Capturing output stores the failure details. Your script must still decide how to react to the return code.

The experiment has answered the command-behavior question. The project now needs its root-path version restored.

  • Return system_health.py to the root-path version by replacing its contents with the code below:
import subprocess

result = subprocess.run(
    ["df", "-h", "/"],
    capture_output=True,
    text=True,
)
print(result.stdout.strip())
print("Return code:", result.returncode)

What does the restored version keep?

  • The command list targets the root filesystem again.
  • The script prints captured standard output.
  • The script prints the command return code.
  • Standard error remains available through result.stderr.
  • Save system_health.py.

Before you rerun it, consider whether the restored root path returns zero again.

  • Complete the final check by running this command:
python system_health.py

What confirms the restoration?

You'll see the root filesystem table again. The final line shows Return code: 0.

That's the experiment complete. Your checker captures a successful Linux command result while preserving the return code for later decisions.

Still seeing the missing-path error?

Confirm that the command list contains ["df", "-h", "/"].

Confirm that you saved system_health.py before rerunning it.

Help me restore the successful root-path check.

✔️ Awesome, I've got everything!

Your system_health.py file is complete for this step. Your existing requirements.txt stays unchanged.

ⓧ I'd like to double check the full code

import subprocess

result = subprocess.run(
    ["df", "-h", "/"],
    capture_output=True,
    text=True,
)
print(result.stdout.strip())
print("Return code:", result.returncode)

How should this file look?

The file imports subprocess once. The command list targets the root filesystem.

The final two lines print captured standard output plus the process return code.

Your checker can now see a command failure without crashing. Next, you will make Python react to that failure through a controlled error path.

Handle Disk Command Failures

Your first disk check captured the command's terminal output. It also exposed the process exit code without reacting to failure.

A health checker needs a controlled failure path. In this step, check=True turns a nonzero result into an exception that your script catches.

In this step, get ready to:
  • Turn a nonzero df exit into an exception.
  • Catch subprocess.CalledProcessError to print the captured command error.
  • Compare failed command exits with launch failures handled by OSError.
Convert nonzero exits into exceptions

The previous version returned a nonzero code without entering an error handler. Adding check=True makes that failed status trigger CalledProcessError.

  • In system_health.py, replace the current subprocess demo with this controlled failure example:
import subprocess

try:
    result = subprocess.run(
        ["df", "-h", "/path-that-does-not-exist"],
        capture_output=True, text=True, check=True,
    )
    print(result.stdout.strip())
except subprocess.CalledProcessError as error:
    print("Disk check failed:", error.stderr.strip())

What Does This Code Do?

  • The command list keeps df, -h, and the missing path as separate arguments.
  • The check=True argument raises an exception when df returns a nonzero exit code.
  • The CalledProcessError exception carries the captured error stream from the failed command.
  • The error.stderr.strip() expression removes surrounding whitespace before the message is printed.
  • Save system_health.py.

Before you run it, consider which path the missing directory triggers: the success print or the exception block.

  • Test the controlled failure by running this command:
python system_health.py

You'll see one line beginning Disk check failed:. Python does not print an unhandled traceback.

You have turned the failed disk command into a result that an operator can read immediately.

Seeing a Traceback or No Error?

  • Confirm that check=True remains inside the subprocess.run() call.
  • Check that the except line aligns with the try line.
  • Verify that the command still uses /path-that-does-not-exist for this failure test.

Help me debug the disk failure handler.

✔️ Awesome, I've got everything!

Your saved system_health.py now converts the failed disk command into a friendly message.

ⓧ I'd like to double check the full code

import subprocess

try:
    result = subprocess.run(
        ["df", "-h", "/path-that-does-not-exist"],
        capture_output=True, text=True, check=True,
    )
    print(result.stdout.strip())
except subprocess.CalledProcessError as error:
    print("Disk check failed:", error.stderr.strip())
Distinguish exit errors from launch errors

Command automation can fail at two different stages. The exception type tells your script whether the command started before it failed.

Two Failure Stages

A missing path still allows df to start. The command reports a nonzero exit code, so check=True raises CalledProcessError.

An unavailable executable fails before the command can run. A production checker can add except OSError as error: to handle that launch failure with another friendly result.

Before your final run, predict whether Python prints one controlled message or an unhandled traceback.

  • Confirm the failure path by running the checker again:
python system_health.py

You'll see Disk check failed: followed by the command's error. The script finishes without an unhandled traceback.

Your disk check now fails cleanly. Next, you'll use a public REST API to add remote health data.

Check a Public REST API

Your disk check now turns a failed Linux command into a friendly result. Local disk status cannot reveal whether a remote dependency is reachable.

In this step, you will use Requests to query a public REST API. You will validate each HTTP response before trusting it.

In this step, get ready to:
  • Send a timed HTTP GET request to a public endpoint.
  • Validate the HTTP response status.
  • Turn request failures into a friendly message.
Send a timed API request

An HTTP GET request asks a server to return a resource. A timeout limits how long the client waits for incoming data.

Why Requests for this check?

The standard library includes urllib.request for lower-level HTTP work. Requests gives this script a readable GET call.

Its API groups timeout failures with HTTP failures under one exception family. That keeps the health check compact.

  • Use your Linux text editor to create api_demo.py beside requirements.txt.
  • Copy this request check into api_demo.py:
import requests

try:
    response = requests.get("https://httpbin.org/get", timeout=5)
    response.raise_for_status()
    print("API status:", response.status_code)
except requests.exceptions.RequestException as error:
    print("API check failed:", error)

What does this code do?

  • The requests.get() call sends a GET request to the public httpbin endpoint.
  • The timeout=5 argument limits how long Requests waits for data.
  • The raise_for_status() call converts an unsuccessful HTTP response into an exception.
  • The RequestException handler turns HTTP or network failures into one friendly output line.
  • Save api_demo.py.

Before you run it, picture whether the healthy endpoint reaches the success print or the exception handler.

  • Run the API demo from the active virtual environment by using this command:
python api_demo.py

What should I see?

The python command runs api_demo.py with Requests from your active virtual environment.

You should see API status: 200. You have your first remote health signal working inside the checker project.

🙋‍♀️ Request failing?

  • Confirm that your Linux machine has internet access.
  • Check that the endpoint in api_demo.py is https://httpbin.org/get.
  • Confirm that the .venv environment remains active.

Help me diagnose this request failure.

Inspect the response URL

The Requests response stores metadata from the completed request. Its status_code attribute records the HTTP result.

The url attribute exposes the response URL. Printing it confirms which endpoint produced the result.

  • Find print("API status:", response.status_code) in api_demo.py.
  • Add the following line directly below it:
print(response.url)

Why inspect the URL?

The url attribute exposes the final response URL. Printing it turns that metadata into visible evidence from the request.

  • Save api_demo.py.

Before you run it, picture which two response details the terminal displays.

  • Run the updated API demo by using this command:
python api_demo.py

What should I see?

This command reruns the saved script so you can inspect its response metadata.

You should see API status: 200 followed by https://httpbin.org/get. Your check now exposes the endpoint beside its status.

🙋‍♀️ URL missing from the output?

  • Check that print(response.url) sits below the status print inside the try block.
  • Confirm that api_demo.py was saved before the latest run.

Help me find why the response URL is missing.

Test the HTTP failure path

A server can return a response with an unsuccessful HTTP status. The raise_for_status() call routes that response into the existing exception handler.

  • Replace https://httpbin.org/get with https://httpbin.org/status/404 in api_demo.py.
  • Save api_demo.py.

Before you run it, picture whether the 404 response reaches the success print or the exception handler.

  • Run the changed API demo by using this command:
python api_demo.py

What should I see?

This run sends the same timed GET request to an endpoint that returns an unsuccessful status.

You should see one friendly line beginning API check failed:. No unhandled traceback appears.

  • Replace https://httpbin.org/status/404 with https://httpbin.org/get.
  • Save api_demo.py.

Before you run it again, picture which branch the restored endpoint reaches.

  • Verify the restored endpoint by running this command:
python api_demo.py

What should I see?

This run confirms that the healthy endpoint works after the failure test.

You should see API status: 200 followed by https://httpbin.org/get. The healthy path is active again.

  • Delete print(response.url) from api_demo.py.
  • Save api_demo.py.

Before the final run, picture the single success line that remains.

  • Complete the end-of-step verification by running this command:
python api_demo.py

What should I see?

This final run checks the saved version of api_demo.py against the healthy endpoint.

You should see API status: 200. Your timed API check now validates HTTP success without exposing an unhandled request error.

🙋‍♀️ Healthy endpoint still failing?

  • Confirm that api_demo.py contains https://httpbin.org/get.
  • Check that the except block aligns with the try block.
  • Retry the request after confirming internet access.

Help me debug the final API check.

✔️ Awesome, I've got everything!

Your final api_demo.py is saved with the healthy endpoint restored.

ⓧ I'd like to double check the full code

import requests

try:
    response = requests.get("https://httpbin.org/get", timeout=5)
    response.raise_for_status()
    print("API status:", response.status_code)
except requests.exceptions.RequestException as error:
    print("API check failed:", error)

Your remote check now validates HTTP status without crashing on request failures. Next up, you will parse the API's JSON data into the combined system health report.

Build the System Health Checker

Your Linux command check now catches failures. Your Requests API check now validates remote responses.

Those separate demos leave evidence in different places. This step combines them into one JSON report that remains available when either check fails.

In this step, get ready to:
  • Wrap the Linux disk check in a reusable function.
  • Parse the REST API response into a consistent dictionary.
  • Write one JSON report with an overall health status.
Turn the disk check into a function

The check_disk() function returns the same fields for successful commands and failed commands. The rest of the checker can use those fields without handling process details again.

  • Return to system_health.py in your text editor.
  • Delete the current try block below import subprocess.
  • Add the reusable disk check below import subprocess by pasting this code:
def check_disk():
    try:
        result = subprocess.run(
            ["df", "-h", "/"],
            capture_output=True,
            text=True,
            check=True,
        )
        return {
            "status": "success",
            "output": result.stdout.strip(),
            "error": None,
        }
    except subprocess.CalledProcessError as error:
        return {
            "status": "error",
            "output": "",
            "error": error.stderr.strip(),
        }
    except OSError as error:
        return {
            "status": "error",
            "output": "",
            "error": str(error),
        }

What Does This Code Do?

  • The successful path stores the captured disk table under output.
  • The CalledProcessError branch stores errors from nonzero command results.
  • The OSError branch stores failures that stop the command from starting.
  • Every path returns a status value that later code can inspect.
  • Save system_health.py.
  • Test the new disk function directly by running this command:
python -c "import system_health; print(system_health.check_disk())"

What Should You See?

You should see a dictionary whose status value is success. Its output value contains the captured filesystem table.

That is the local half of your checker working. The disk command now produces structured evidence that another function can reuse.

Disk Check Not Returning Success?

Confirm that import subprocess remains at the top of system_health.py. Check that the command arguments are separate strings inside the list.

Make sure the path in check_disk() is /.

Help me debug my disk check.

Add the API check

An HTTP response begins as a response object. Calling response.json() decodes its JSON body into Python data.

The API function selects the reported origin and endpoint URL. It also returns a consistent error dictionary when the request fails.

  • Find this import at the top of system_health.py:
import subprocess

What Are You Changing?

The current file only imports the process tool. The completed checker also needs the JSON writer and the API client.

  • Replace that import with this dependency group and configuration:
import json
import subprocess

import requests

API_URL = "https://httpbin.org/get"
REPORT_FILE = "health_report.json"

Why Keep These Values at the Top?

The API_URL constant gives the remote check one endpoint. The REPORT_FILE constant gives every run one default report path.

Central configuration makes both values easy to find during testing.

  • Add the API function below check_disk() by pasting this code:
def check_api():
    try:
        response = requests.get(API_URL, timeout=5)
        response.raise_for_status()
        data = response.json()
        return {
            "status": "success",
            "status_code": response.status_code,
            "origin": data.get("origin", "unknown"),
            "url": data.get("url", API_URL),
            "error": None,
        }
    except requests.exceptions.RequestException as error:
        return {
            "status": "error",
            "status_code": None,
            "origin": None,
            "url": API_URL,
            "error": str(error),
        }

How Does the API Result Stay Consistent?

  • The timed request prevents the checker from waiting indefinitely for data.
  • The status check converts unsuccessful HTTP responses into handled exceptions.
  • The decoded response supplies origin and url values.
  • The exception branch preserves the endpoint and records the failure text.
  • Save system_health.py.
  • Test the API function directly by running this command:
python -c "import system_health; print(system_health.check_api())"

What Should You See?

With the endpoint available, you should see a dictionary whose status value is success. You should also see status code 200.

The origin value varies by network. The url value should be https://httpbin.org/get.

You now have matching local and remote result shapes. Both checks can feed the same report without exposing their exception logic to the report writer.

API Check Returning an Error?

Confirm that your virtual environment remains active. Check that the endpoint is reachable from your network.

Compare the requests.get() call with the working version in api_demo.py.

Help me debug my API check.

Combine and verify the report

The final function runs both checks before calculating one overall status. It writes the full result even when one check reports an error.

  • Add the report builder below check_api() by pasting this code:
def main():
    disk_result = check_disk()
    api_result = check_api()
    overall_status = (
        "healthy"
        if disk_result["status"] == "success"
        and api_result["status"] == "success"
        else "degraded"
    )
    report = {
        "disk": disk_result,
        "api": api_result,
        "overall_status": overall_status,
    }

    with open(REPORT_FILE, "w", encoding="utf-8") as report_file:
        json.dump(report, report_file, indent=2)

    print(f"System status: {overall_status}")
    print(f"Report saved to {REPORT_FILE}")


if __name__ == "__main__":
    main()

How Does the Report Come Together?

  • The main() function collects one disk result and one API result.
  • The overall status becomes healthy only when both checks succeed.
  • The report dictionary keeps each detailed result beside the summary.
  • The json.dump() call writes readable output using an indent of 2.
  • Save system_health.py.

Before you run the checker, which overall status do you expect from the two checks on your machine?

  • Run the complete checker with this command:
python system_health.py

What Should You See?

When both checks succeed, you should see System status: healthy. The next line should confirm that the report was saved to health_report.json.

A temporary network failure can produce System status: degraded. The report should still be saved.

Before you inspect the file, which three top-level fields do you expect the report to contain?

  • Validate and format the generated report by running this command:
python -m json.tool health_report.json

What Does This Verify?

You should see formatted JSON containing disk. You should also see api and overall_status.

Formatted output confirms that health_report.json contains valid JSON.

✔️ Awesome, I've got everything!

Your completed system_health.py now combines both checks and saves their results.

ⓧ I'd like to double check the full code

import json
import subprocess

import requests

API_URL = "https://httpbin.org/get"
REPORT_FILE = "health_report.json"


def check_disk():
    try:
        result = subprocess.run(
            ["df", "-h", "/"],
            capture_output=True,
            text=True,
            check=True,
        )
        return {
            "status": "success",
            "output": result.stdout.strip(),
            "error": None,
        }
    except subprocess.CalledProcessError as error:
        return {
            "status": "error",
            "output": "",
            "error": error.stderr.strip(),
        }
    except OSError as error:
        return {
            "status": "error",
            "output": "",
            "error": str(error),
        }


def check_api():
    try:
        response = requests.get(API_URL, timeout=5)
        response.raise_for_status()
        data = response.json()
        return {
            "status": "success",
            "status_code": response.status_code,
            "origin": data.get("origin", "unknown"),
            "url": data.get("url", API_URL),
            "error": None,
        }
    except requests.exceptions.RequestException as error:
        return {
            "status": "error",
            "status_code": None,
            "origin": None,
            "url": API_URL,
            "error": str(error),
        }


def main():
    disk_result = check_disk()
    api_result = check_api()
    overall_status = (
        "healthy"
        if disk_result["status"] == "success"
        and api_result["status"] == "success"
        else "degraded"
    )
    report = {
        "disk": disk_result,
        "api": api_result,
        "overall_status": overall_status,
    }

    with open(REPORT_FILE, "w", encoding="utf-8") as report_file:
        json.dump(report, report_file, indent=2)

    print(f"System status: {overall_status}")
    print(f"Report saved to {REPORT_FILE}")


if __name__ == "__main__":
    main()

How to Use This Reference

Compare this reference with your saved system_health.py. The imports and constants should appear before the three functions.

Test Both Failure Paths

A useful health report survives the failure it describes. You will temporarily break each check to confirm that the report records a degraded state.

  • Replace the path literal / inside check_disk() with /path-that-does-not-exist.
  • Save system_health.py.

Before you run this version, do you expect the script to crash or save a degraded report?

  • Run the checker with the missing disk path by using this command:
python system_health.py

The Disk Failure Is Contained

You should see System status: degraded. You should also see confirmation that health_report.json was saved.

The disk section now contains an error result without an unhandled traceback.

  • Restore the path literal inside check_disk() to /.
  • Save system_health.py.
  • Replace https://httpbin.org/get in API_URL with https://httpbin.org/status/503.
  • Save system_health.py.

Before you run the checker again, do you expect HTTP status validation to stop the report from being written?

  • Run the checker with the unsuccessful API endpoint by using this command:
python system_health.py

The API Failure Is Contained

You should see System status: degraded. The report should still be saved to health_report.json.

The API section now preserves the endpoint and stores the request error.

  • Restore API_URL to https://httpbin.org/get.
  • Save system_health.py.

Before the final run, do you expect both restored checks to return the checker to a healthy state?

  • Run the restored checker with this command:
python system_health.py

Your Checker Is Restored

With the endpoint available, you should see System status: healthy. If the network is unavailable, you should see a degraded status and a saved report.

Either result proves that the checker records its findings without an unhandled crash.

  • Confirm the final report is valid by running this command:
python -m json.tool health_report.json

Final Checkpoint

You should see valid formatted JSON with a disk object. You should also see an api object and an overall_status value.

You have built a reusable troubleshooting artifact that combines local command evidence with remote service health.

Secret mission

Make the Report Filename Configurable

Your checker always saves its results to the same file. Add an optional `--output` argument so scheduled runs and operators can choose a report path without editing the script.

Clean Up Your Resources

Clean Up Your Resources

Everything you created lives in a local project directory, so there are no service charges or ongoing costs. Choose a cleanup option based on when you want to use the checker again.

Resources you used:

  • .venv virtual environment with Requests version 2.34.2 installed.
  • requirements.txt, system_health.py, and the optional api_demo.py project files.
  • health_report.json, nightly_health.json, and any other custom JSON reports you generated.

Keep everything running

No action needed. Choose this if you plan to run or extend the checker again soon.

  • Leave the project directory intact.
  • Keep the existing reports as health snapshots.
  • Continue using the active virtual environment in the terminal from earlier.

Pause - I'll come back to this later

Stop the active virtual environment to clear it from your current shell. Your project directory stays available for later.

  • Deactivate the virtual environment by running this command:
deactivate

What does this command do?

The command removes the environment's Python paths from the current shell.

It leaves .venv on disk. It also preserves every project file.

  • Check the terminal prompt.

The (.venv) prefix should be gone.

  • Reactivate the environment later from the project directory by running this command:
source .venv/bin/activate

What happens when you return?

This command restores the isolated environment for the checker. Your prompt should show (.venv) again.

Delete - I don't want to use this again

Deleting the project directory is permanent. The cleanup command targets the directory currently shown in your terminal.

  • Copy any reports you want to keep to another directory.
  • Confirm the terminal from earlier is still inside the project directory by running this command:
ls requirements.txt system_health.py

What does this check prove?

This command looks for two files that belong to the checker. Seeing both names confirms that the terminal is in the project directory.

You should see requirements.txt and system_health.py in the output.

The next command records this exact directory before leaving it. It removes the recorded directory only after the virtual environment is deactivated.

  • Remove the project directory by running this cleanup command:
project_dir="$PWD"
deactivate
cd ..
rm -rf "$project_dir"
test ! -d "$project_dir" && printf 'Project removed.\n'

What does this cleanup do?

  • The first line records the current project path.
  • The next line exits the active virtual environment.
  • The directory change moves the shell to the parent folder.
  • The final lines remove the recorded directory. They print a confirmation when the directory no longer exists.

You should see Project removed. in the terminal.

That closes the loop. The full project directory is gone from this machine.

Nice Work!

Nice Work!

Part 7 is complete! Your Python System Health Checker now turns local disk status plus a public REST API response into a readable JSON report.

You've learned how to:

  • Captured a Linux disk check with subprocess.run() while preserving its output plus exit status.
  • Built a resilient API check with Requests that returns useful data during success or failure.
  • Saved a combined health report to health_report.json with a clear healthy or degraded result.
  • Secret Mission: Made the report filename configurable with --output.

Ready to quiz yourself?