Python Basics for DevOps Project 4

Build a Python health report and publish your DevOps portfolio on GitHub.

Introduction

30 Second Summary

A computer can feel slow long before the cause is obvious. Clear readings make it easier to decide what needs attention.

In this project, you will build a Python script that turns live readings from a standard Linux environment into a local health report. You will also publish all four projects in one GitHub portfolio.

What You'll Build

You run one command to create a report that shows whether each live CPU, memory, or disk reading is within its limit.

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

  • A command-line file checker that identifies existing and missing paths. It also responds safely when you forget the filename.
  • A validated health report that compares live CPU, memory, and disk readings with percentage limits. Its disk check uses a portable default path or a mounted path you choose.
  • A four-project GitHub portfolio that presents your complete Python Basics for DevOps series in one repository.
  • Secret Mission: Add one overall HEALTHY or WARNING result to the report. Publish the improvement to GitHub.

Are there any prerequisites?

You need the local files from Projects 1-3. You also need basic Git knowledge, a GitHub account, and internet access for installing psutil.

Before We Start

Before We Start...

Before the hands-on work begins, take a moment to commit to what you are building and why it matters. You will connect your Python learning to a practical DevOps task and present the complete series as one portfolio.

Set Up the Portfolio Workspace

Your first three Python projects already live in a standard Linux environment. Project 4 adds the local health-report script that completes the series.

A clear folder structure keeps Project 4 files separate from your earlier work. This readiness check also confirms Git, a standard Linux text editor (like nano, vim, or gedit), and virtual environment support before you write code.

In this step, get ready to:
  • Verify the installed Python and Git releases plus an available Linux text editor.
  • Confirm the standard Linux text editor and virtual environment support.
  • Create the four portfolio directories.
Verify your installed tools

A version is the numbered release of a tool. These checks confirm that the standard Linux terminal can reach each tool required for your portfolio.

  • Open the application menu in your standard Linux environment.
  • Search for an installed standard Linux text editor, such as nano, vim, or gedit.
  • Select the available editor to launch it.
  • Open the editor's help or information view if it provides one.
  • Find the editor's version details in that view.
  • Confirm the editor opens files and accepts text input.
  • Close the editor's information view if you opened it.
  • Open the standard Linux terminal from your application menu.
  • Check the Python and Git releases plus an available text editor from the standard Linux terminal by running these commands:
python3 --version
git --version
command -v nano || command -v vim || command -v gedit

What Do These Checks Show?

The first two commands ask Python and Git to print their numbered releases. The final command prints the path for the first available standard Linux text editor, such as nano, vim, or gedit.

Python must report 3.6 or newer because psutil 7.2.2 requires that minimum release.

🙋‍♀️ Is Python Older Than 3.6?

  • Pause the project if Python reports a release older than 3.6.
  • Upgrade Python through your standard Linux environment's package-management process before continuing.
  • Rerun the three version checks after the upgrade.

Help me check whether my Python version can run psutil 7.2.2.

Confirm virtual environment support

A virtual environment gives Project 4 its own package space. This check confirms Python can create that isolated space before the project needs it.

  • Check Python's virtual-environment support by running this command:
python3 -m venv --help

What Does This Check Do?

The command asks Python to display help for the venv module. Seeing the help text confirms the module is available without creating an environment yet.

✔️ I see venv help

The help text confirms virtual-environment support is ready. Continue to the workspace instructions below.

ⓧ venv or pip support is missing

A standard Linux environment may use apt to add Python's environment and installer support. If your distribution uses another package manager, install its equivalent virtual-environment and pip packages so future third-party packages stay out of the system Python.

  • Prepare to authorize the package-manager change with your Linux account password if requested.
  • Prepare to confirm the package installation if prompted.
  • Install the missing Python support through the Linux package manager by running:
sudo apt update
sudo apt install python3-venv python3-pip

What Do These Commands Install?

The first command refreshes the available package information. The second command installs Python's virtual-environment and pip support through the Linux package manager.

This process does not use pip to install a third-party package into the system Python.

  • Verify the newly installed virtual-environment support by running the check again:
python3 -m venv --help

What Should You See Now?

You should now see the help text for the venv module. This confirms Python can create the isolated environment required later.

🙋‍♀️ Still Missing venv Support?

  • Check that both package-manager commands completed without an installation failure.
  • Confirm that your internet connection remained available while the packages downloaded.

Help me fix missing venv support on Linux Mint.

Create the portfolio workspace

The portfolio needs one top-level home with a separate directory for each project. Project 4 becomes your active directory for the next part of the build.

  • Create the four portfolio directories and enter project-4 by running these commands:
mkdir -p ~/python-basics-devops/project-1 ~/python-basics-devops/project-2 ~/python-basics-devops/project-3 ~/python-basics-devops/project-4
cd ~/python-basics-devops/project-4

What Do These Commands Create?

The mkdir -p command creates project-1, project-2, project-3, and project-4 inside ~/python-basics-devops.

The -p option creates the required parent directory. The second command places your terminal inside ~/python-basics-devops/project-4.

Before you check, which four directory names do you expect the terminal to list?

  • Verify the top-level portfolio structure by running this command:
ls -1 ~/python-basics-devops

What Should You See?

The output contains exactly four lines. They are project-1, project-2, project-3, and project-4.

That result proves your earlier projects have dedicated destinations. It also proves Project 4 has a clean place for its Python files.

🙋‍♀️ Seeing a Missing Directory?

  • Check that the portfolio path is spelled ~/python-basics-devops in the folder-creation command.
  • Rerun the folder-creation command above if one of the four directories is missing.

Help me fix my portfolio directory structure.

Your tools are verified and the four-project workspace is ready. Next, you'll use imports to build a file checker that reads its filename from the command line.

Create a File Checker

Your portfolio folders are ready. Now you can use Python to make a script respond to a filename supplied from the terminal.

The first version exposes what happens when that filename is missing. You will see the failure once, then replace it with a clear message and a safe exit.

In this step, get ready to:
  • Create the warning-limits file used by the health report.
  • Build a command-line file checker with sys.argv.
  • Handle a missing filename without a traceback.
Create the limits file and checker

A module is a toolbox of reusable Python features. The os module checks paths. The sys module gives your script the words supplied in its run command.

How does command-line input work?

The script name occupies sys.argv[0]. The first value after it occupies sys.argv[1].

Checking the list length before reading position 1 prevents the final version from requesting an item that does not exist.

  • Open Xed from the Linux Mint application menu.
  • Create a blank document with Xed's new-document control.
  • Add the three warning limits by copying:
cpu=80
memory=80
disk=80
  • Save the document as limits.txt inside ~/python-basics-devops/project-4.
  • Create another blank document with Xed's new-document control.
  • Add the first file-checker version by copying:
import os
import sys

def main():
    # Read the first command-line value as the path to check
    filename = sys.argv[1]

    # Report whether the supplied path exists
    if os.path.exists(filename):
        print(f"Found: {filename}")
    else:
        print(f"Missing: {filename}")

main()

What does this code do?

  • main() groups the checker workflow in one function.
  • filename stores the first argument after the script name.
  • os.path.exists(filename) returns True for an existing path and False for a missing path.
  • The comments explain why each section exists.
  • Save the second document as check_file.py inside ~/python-basics-devops/project-4.
  • Return to the XFCE Terminal session inside ~/python-basics-devops/project-4.
Test the checker and expose the gap

The checker has two useful path outcomes. It still assumes that a filename always exists at position 1.

  • Check the saved limits file by running:
python3 check_file.py limits.txt

You should see Found: limits.txt. This proves the argument reached the script and the path exists.

  • Check a missing path by running:
python3 check_file.py missing.txt

You should see Missing: missing.txt. The checker handled the false path without failing.

Before you run the next command, do you think Python can read position 1 when the command contains only the script name?

  • Run the checker without a filename by entering:
python3 check_file.py

This failure is intentional

The traceback ends with IndexError: list index out of range. The script requested a list position that was never supplied.

That visible failure identifies the exact guard the finished checker needs.

Add a safe argument check

A command-line tool should explain missing input in the learner's language. A short length check can print the requirement before the script touches sys.argv[1].

  • Return to check_file.py in Xed.
  • Replace the file contents with the safe version below:
import os
import sys

def main():
    # Stop safely when the filename argument is missing
    if len(sys.argv) < 2:
        print("Please provide a filename.")
        return

    # Read the supplied path after confirming it exists
    filename = sys.argv[1]

    # Report whether the supplied path exists
    if os.path.exists(filename):
        print(f"Found: {filename}")
    else:
        print(f"Missing: {filename}")

main()

Why is this safer?

  • len(sys.argv) < 2 checks whether the filename argument is absent.
  • print() tells the person running the script what to provide.
  • return stops main() before the unsafe list access.
  • Save check_file.py.
  • Return to XFCE Terminal.
  • Verify the helpful missing-input response by running:
python3 check_file.py

You should see Please provide a filename. with no traceback. That is the hard part fixed: the checker now fails safely.

  • Confirm the existing-file path still works by running:
python3 check_file.py limits.txt

You should see Found: limits.txt. The guard protects missing input without changing valid commands.

Still seeing a traceback?

Check that the guard appears before filename = sys.argv[1]. Python executes the file from top to bottom.

Confirm that return aligns with print() inside the missing-argument branch.

Help me fix the missing-filename guard.

✔️ Awesome, I've got everything!

Your checker now handles existing paths, missing paths, and missing command-line input.

ⓧ I'd like to double check the full code

import os
import sys

def main():
    # Stop safely when the filename argument is missing
    if len(sys.argv) < 2:
        print("Please provide a filename.")
        return

    # Read the supplied path after confirming it exists
    filename = sys.argv[1]

    # Report whether the supplied path exists
    if os.path.exists(filename):
        print(f"Found: {filename}")
    else:
        print(f"Missing: {filename}")

main()

Your checker now turns missing input into a useful instruction. Next, you will install psutil inside an isolated environment so Python can collect live system readings.

Install psutil in a Virtual Environment

Your file checker now reads a filename through sys.argv and checks it with os.path.exists(). Its length guard replaces the missing-filename traceback with a clear instruction.

The server report needs psutil version 7.2.2 to gather live CPU, memory, and disk readings.

A virtual environment gives Project 4 a private package space. Your system Python stays untouched.

In this step, get ready to:
  • Create an isolated .venv for Project 4.
  • Install psutil version 7.2.2 inside the virtual environment.
  • Save a reproducible requirements.txt before leaving the environment.
Give Project 4 its own package space

Think of .venv as a labelled toolbox that belongs only to Project 4. Anything installed while it is active stays separate from your system packages.

  • Create .venv and activate it by running these commands:
python3 -m venv .venv
source .venv/bin/activate

What do these commands do?

  • The first command creates the isolated environment inside the current project-4 folder.
  • The second command activates that environment for the current terminal session.
  • Confirm activation by checking for (.venv) at the start of the terminal prompt.

Good, the (.venv) marker confirms that Project 4 now has its own active package space.

Don't see (.venv) in the prompt?

Check that your terminal is still inside ~/python-basics-devops/project-4. Confirm that the activation command uses the path .venv/bin/activate.

If virtual environment support is missing, return to the setup fix from Step 1 before trying these commands again.

Help me activate the Project 4 virtual environment.

Fill the private toolbox

A package is reusable Python code prepared by someone else. The pip command installs packages into the active environment.

Why psutil instead of shell commands?

Shell monitoring commands produce text that changes between tools and systems. Parsing that output adds fragile formatting work to every reading.

Psutil provides the measurements directly through Python. This keeps the report focused on variables, decisions, loops, and error handling.

  • Install the pinned psutil package into the active environment by running:
pip install psutil==7.2.2

What does the install command do?

The exact pin tells pip to install version 7.2.2. Pinning gives every rebuilt environment the same package release.

  • Check the final installation line in the terminal.

You should see Successfully installed psutil-7.2.2 at the end of the installation output.

Did the installation fail?

Check that (.venv) appears in your prompt before running the install command. This prevents the package from going into the system Python.

If the output mentions a build or download problem, keep the full message for troubleshooting. Do not install psutil into the system Python as a workaround.

Help me install psutil inside .venv.

Before you verify the environment, which package line do you expect pip to report?

  • Verify the package installed inside .venv by running:
python3 -m pip freeze

What does freeze show?

The command lists the packages installed for the active Python environment. Each package appears with its installed version.

You should see exactly psutil==7.2.2.

Record the dependency and practise the cycle

A pinned requirements.txt records the package needed to rebuild this environment. The virtual environment can stay disposable because this file preserves its dependency.

  • Write the installed package list to requirements.txt by running:
python3 -m pip freeze > requirements.txt

Why redirect the package list?

The redirect writes the output from pip freeze into requirements.txt. The file now holds the exact package pin used by Project 4.

  • Switch back to Xed from earlier.
  • Select ~/python-basics-devops/project-4/requirements.txt using Xed's file picker.

You should see one line containing psutil==7.2.2.

Don't see requirements.txt?

Confirm that Xed is looking inside ~/python-basics-devops/project-4. The redirect creates the file in the terminal's current folder.

If the file is empty, check that (.venv) is still visible before running the redirect command again.

Help me find or rebuild requirements.txt.

✔️ Awesome, I've got everything!

Your existing scripts are unchanged. Your new requirements.txt now records the isolated psutil dependency.

ⓧ I'd like to double check the full code

import os
import sys

def main():
    # Stop safely when the filename argument is missing
    if len(sys.argv) < 2:
        print("Please provide a filename.")
        return

    # Read the supplied path after confirming it exists
    filename = sys.argv[1]

    # Report whether the supplied path exists
    if os.path.exists(filename):
        print(f"Found: {filename}")
    else:
        print(f"Missing: {filename}")

main()

Why is this file unchanged?

The file checker keeps the safe argument guard from the previous step. This environment step adds psutil without changing the checker logic.

cpu=80
memory=80
disk=80

What does this file preserve?

The three original warning limits remain unchanged. The final server report uses these values later.

psutil==7.2.2

What does this file preserve?

This line pins the only third-party package installed inside Project 4's virtual environment.

Your dependency is now recorded in a file that can rebuild the package environment. Repeat the activation cycle once to prove that the isolated package remains available.

  • Leave the active virtual environment by running:
deactivate

What does deactivate do?

This command returns the terminal to the system Python environment. It keeps .venv and its installed package on disk.

You should see the (.venv) marker disappear from the terminal prompt.

Before you reactivate the environment, what should return to the start of the prompt?

  • Reactivate the existing environment by running:
source .venv/bin/activate

What does activation change?

Activation makes the environment's Python and pip commands available in the current terminal session. It reuses the existing .venv.

You should see (.venv) return to the start of the prompt.

Before you check again, which single package line should still be available inside the reactivated environment?

  • Confirm the pinned package remains installed by running:
python3 -m pip freeze

What does this check prove?

The result proves that deactivation did not delete the environment. Reactivation restores access to the package already installed inside it.

You should still see exactly psutil==7.2.2.

Before the final command, what should disappear from the prompt when you leave the environment?

  • Return the terminal to the system environment by running:
deactivate

Why deactivate again?

The command completes the practice cycle. Your terminal leaves .venv while the environment remains ready for the next step.

The (.venv) marker should now be gone. That confirms you can enter the isolated environment when Project 4 needs psutil and leave it afterward.

Did the practice cycle behave differently?

If activation does not restore the prompt marker, confirm that the terminal remains in ~/python-basics-devops/project-4.

If the package check does not show the pinned line, confirm that (.venv) appears before running the check.

Help me verify the virtual environment cycle.

Project 4 now has an isolated psutil installation and a pinned dependency file. Next up, you'll use that package to build the local server health report.

Build the Server Health Report

Your Project 4 virtual environment now contains psutil. Your file checker also handles missing command-line input without a traceback.

This step turns those pieces into a local monitoring workflow. Your computer acts as the server, while the script checks its current workload and writes a health report.

In this step, get ready to:
  • Validate the percentage limits loaded from limits.txt.
  • Collect CPU, memory, and disk readings from a configurable path.
  • Generate server_report.txt through a safe command-line workflow.
Understand the system you are monitoring

What is a server?

A server is a computer that provides a service to other computers. Website servers deliver pages. Database servers store and return data. File servers share files.

Your own computer acts like a small server in this project. The script reads its health data locally, so no remote machine is required.

Monitoring means checking a system's health at regular points in time. These checks help you notice pressure before it prevents the system from doing its job.

What does monitoring measure?

  • CPU usage shows how busy the computer's processor is. A reading near 20% is light work. A sustained reading near 95% can indicate heavy applications, too many processes, or a performance problem.
  • Memory usage shows how much RAM active programs use. A reading of 75% means three quarters of the available memory is occupied. A system can slow down or become unstable as memory fills.
  • Disk usage shows how much storage is occupied. A reading of 90% means the disk is nearly full. A full disk can block file saves, log writes, and software installation.
  • Reactivate the existing .venv environment by running:
source .venv/bin/activate

You should see (.venv) at the start of the terminal prompt. That marker confirms Python can import the isolated psutil package.

Don't see the environment marker?

Confirm that the terminal is inside ~/python-basics-devops/project-4.

Check that the .venv folder still exists before running the activation command again.

Help me reactivate the Project 4 environment.

Build the validated monitoring functions

Each function has one job. Small functions make it easier to test file reading, metric collection, and report formatting before the full command runs.

  • Open Xed from the Linux Mint application menu.
  • Create server_report.py inside ~/python-basics-devops/project-4.
  • Add the imports and shared settings by copying:
import os
import sys

import psutil

# Keep shared report settings in one place
OUTPUT_FILE = "server_report.txt"
CHECKS = ["cpu", "memory", "disk"]
DEFAULT_DISK_PATH = os.path.abspath(os.sep)

Why use a default disk path?

  • OUTPUT_FILE stores the generated report name.
  • CHECKS keeps the metric order consistent.
  • os.sep supplies the current operating system's root separator. os.path.abspath() normalizes it as the default disk path.
  • Save server_report.py.
  • Return to XFCE Terminal.
  • Verify the default disk path by running:
python3 -c "import server_report; print(server_report.DEFAULT_DISK_PATH)"

On Linux, you should see /. The value comes from the operating system instead of a Linux-only string inside the metric function.

  • Return to server_report.py in Xed.
  • Add the validated limit reader below the shared settings by copying:
def read_limits(limits_filename):
    # Convert each key=value row into a checked percentage
    limits = {}
    with open(limits_filename, "r", encoding="utf-8") as limits_file:
        for line in limits_file:
            metric_name, limit_text = line.strip().split("=")
            limit_value = int(limit_text)

            # Reject percentages outside the possible range
            if limit_value < 0 or limit_value > 100:
                raise ValueError

            limits[metric_name] = limit_value
    return limits

How are the limits validated?

  • metric_name stores a key such as cpu.
  • limit_text holds the text after the equals sign.
  • limit_value holds the converted whole number.
  • Raising ValueError rejects impossible percentages below 0 or above 100.
  • Save server_report.py.
  • Verify the CPU limit loaded as a number by running:
python3 -c "import server_report; print(server_report.read_limits('limits.txt')['cpu'])"

You should see 80. This proves the function parsed the file and returned a numeric limit.

Don't see 80?

Confirm that each line in limits.txt contains one equals sign and a whole number.

Check that limit_value is created with int(limit_text) before the range check.

Help me test the validated limit reader.

  • Return to server_report.py in Xed.
  • Add the metric collector below read_limits() by copying:
def get_metrics(disk_path):
    # Collect one live percentage for each monitored resource
    return {
        "cpu": psutil.cpu_percent(interval=1),
        "memory": psutil.virtual_memory().percent,
        "disk": psutil.disk_usage(disk_path).percent,
    }

How are live metrics collected?

  • psutil.cpu_percent(interval=1) measures CPU activity over one second.
  • psutil.virtual_memory().percent returns the current RAM-use percentage.
  • psutil.disk_usage(disk_path).percent measures the partition containing the selected path.
  • Save server_report.py.
  • Collect one set of live readings from the default disk path by running:
python3 -c "import server_report; print(server_report.get_metrics(server_report.DEFAULT_DISK_PATH))"

You should see a dictionary containing cpu, memory, and disk keys. Your percentages vary with the computer's current workload.

  • Return to server_report.py in Xed.
  • Add the report builder below get_metrics() by copying:
def build_report(limits, metrics, disk_path):
    # Start with a title and the path measured for disk usage
    report_lines = ["LOCAL SERVER HEALTH REPORT", f"Disk path: {disk_path}"]

    # Compare each live reading with its matching limit
    for metric_name in CHECKS:
        metric_value = metrics[metric_name]
        if metric_value >= limits[metric_name]:
            status = "Over limit"
        else:
            status = "Within limit"
        report_lines.append(
            f"{metric_name.upper()}: {metric_value:.1f}% - {status}"
        )
    return report_lines

How is the report assembled?

  • metric_name identifies the resource being checked.
  • metric_value stores that resource's live percentage.
  • The comparison labels a reading Over limit when it meets or exceeds its configured threshold.
  • The selected disk path appears near the top so the report identifies what it measured.
  • Save server_report.py.
  • Verify the formatter with controlled values by running:
python3 -c "import server_report; print(*server_report.build_report({'cpu': 80, 'memory': 80, 'disk': 80}, {'cpu': 20, 'memory': 90, 'disk': 50}, '/'), sep='\n')"

You should see CPU and disk marked Within limit. Memory should be marked Over limit.

Complete and test the safe command

What is error handling?

Error handling prepares a program for problems such as missing files or invalid values. A try block contains work that can fail. Matching except blocks replace a crash with a useful response.

The command-line guard runs before the try block because missing input can be identified directly. The exceptions handle failures that appear while processing valid-looking input.

  • Return to server_report.py in Xed.
  • Add the report writer below build_report() by copying:
def save_report(report_lines):
    # Write one report entry per line
    with open(OUTPUT_FILE, "w", encoding="utf-8") as report_file:
        for report_line in report_lines:
            report_file.write(report_line + "\n")

How is the report saved?

save_report() opens the output file in write mode. It replaces an older report so the file always reflects the latest readings.

  • Return to server_report.py in Xed.
  • Add the input resolver below save_report() by copying:
def get_input_paths():
    # Stop safely before reading a missing filename argument
    if len(sys.argv) < 2:
        print("Please provide a limits filename.")
        return None

    # Resolve the required file and optional disk path
    limits_filename = sys.argv[1]
    disk_path = (
        os.path.abspath(sys.argv[2])
        if len(sys.argv) > 2
        else DEFAULT_DISK_PATH
    )

    # Reject paths that cannot be used by the workflow
    if not os.path.exists(limits_filename):
        print("Limits file not found.")
        return None
    if not os.path.exists(disk_path):
        print("Disk path not found.")
        return None

    return limits_filename, disk_path

How are command-line paths handled?

  • len(sys.argv) < 2 catches the missing required filename before position 1 is read.
  • The second argument is optional. When supplied, os.path.abspath() normalizes it for psutil.disk_usage().
  • Both paths are checked before the workflow begins.
  • Return to server_report.py in Xed.
  • Add the main workflow and script entry point below get_input_paths() by copying:
def main():
    # Stop when path validation has already printed a helpful message
    input_paths = get_input_paths()
    if input_paths is None:
        return

    limits_filename, disk_path = input_paths

    try:
        # Run the report pipeline in its required order
        limits = read_limits(limits_filename)
        metrics = get_metrics(disk_path)
        report_lines = build_report(limits, metrics, disk_path)
        save_report(report_lines)
        print("Report saved to server_report.txt")
    except (ValueError, KeyError):
        print("Limits must include cpu, memory, and disk as numbers from 0 to 100.")
    except OSError as error:
        print(f"Could not read a file or disk path: {error}")

if __name__ == "__main__":
    main()

How does the command stay safe?

  • input_paths receives the validated limits filename and disk path.
  • The pipeline reads limits, collects metrics, builds lines, and saves the report in that order.
  • except (ValueError, KeyError) handles invalid percentages, non-numeric values, and missing metric names.
  • if __name__ == "__main__": runs the workflow only when you execute the file as a script.
  • Save server_report.py.
  • Return to XFCE Terminal.
  • Verify the missing-argument guard by running:
python3 server_report.py

You should see Please provide a limits filename. with no traceback. The command checks its required input before reading it.

  • Open limits.txt in Xed.
  • Replace cpu=80 with cpu=500.
  • Save limits.txt.
  • Test the invalid percentage by running:
python3 server_report.py limits.txt

You should see Limits must include cpu, memory, and disk as numbers from 0 to 100. The range check rejects the impossible percentage before a report is written.

  • Restore the valid warning limits by replacing the file contents with:
cpu=80
memory=80
disk=80

Before you run the finished workflow, which readings do you think might be over their 80% limits?

  • Save limits.txt.
  • Generate the report with the portable default disk path by running:
python3 server_report.py limits.txt

You should see Report saved to server_report.txt. The one-second CPU sample means the command can pause briefly before printing.

  • Open server_report.txt in Xed.

You should see the report title, Disk path: /, and one status line each for CPU, memory, and disk. Your percentages depend on the current computer workload.

  • Regenerate the report for the current Project 4 path by running:
python3 server_report.py limits.txt .

Reopen server_report.txt. The disk path line should now contain the absolute path to project-4.

Report not created?

Check that limits.txt contains all three required names with values from 0 to 100.

Confirm that the terminal prompt begins with (.venv) before running the report.

Help me debug the finished server report.

✔️ Awesome, I've got everything!

Your report now validates its configuration, handles missing input, and records the disk path it measured.

ⓧ I'd like to double check the full code

import os
import sys

import psutil

# Keep shared report settings in one place
OUTPUT_FILE = "server_report.txt"
CHECKS = ["cpu", "memory", "disk"]
DEFAULT_DISK_PATH = os.path.abspath(os.sep)

def read_limits(limits_filename):
    # Convert each key=value row into a checked percentage
    limits = {}
    with open(limits_filename, "r", encoding="utf-8") as limits_file:
        for line in limits_file:
            metric_name, limit_text = line.strip().split("=")
            limit_value = int(limit_text)
            # Reject percentages outside the possible range
            if limit_value < 0 or limit_value > 100:
                raise ValueError
            limits[metric_name] = limit_value
    return limits

def get_metrics(disk_path):
    # Collect one live percentage for each monitored resource
    return {
        "cpu": psutil.cpu_percent(interval=1),
        "memory": psutil.virtual_memory().percent,
        "disk": psutil.disk_usage(disk_path).percent,
    }

def build_report(limits, metrics, disk_path):
    # Start with a title and the path measured for disk usage
    report_lines = ["LOCAL SERVER HEALTH REPORT", f"Disk path: {disk_path}"]
    # Compare each live reading with its matching limit
    for metric_name in CHECKS:
        metric_value = metrics[metric_name]
        if metric_value >= limits[metric_name]:
            status = "Over limit"
        else:
            status = "Within limit"
        report_lines.append(
            f"{metric_name.upper()}: {metric_value:.1f}% - {status}"
        )
    return report_lines

def save_report(report_lines):
    # Write one report entry per line
    with open(OUTPUT_FILE, "w", encoding="utf-8") as report_file:
        for report_line in report_lines:
            report_file.write(report_line + "\n")

def get_input_paths():
    # Stop safely before reading a missing filename argument
    if len(sys.argv) < 2:
        print("Please provide a limits filename.")
        return None
    # Resolve the required file and optional disk path
    limits_filename = sys.argv[1]
    disk_path = (
        os.path.abspath(sys.argv[2])
        if len(sys.argv) > 2
        else DEFAULT_DISK_PATH
    )
    # Reject paths that cannot be used by the workflow
    if not os.path.exists(limits_filename):
        print("Limits file not found.")
        return None
    if not os.path.exists(disk_path):
        print("Disk path not found.")
        return None
    return limits_filename, disk_path

def main():
    # Stop when path validation has already printed a helpful message
    input_paths = get_input_paths()
    if input_paths is None:
        return
    limits_filename, disk_path = input_paths
    try:
        # Run the report pipeline in its required order
        limits = read_limits(limits_filename)
        metrics = get_metrics(disk_path)
        report_lines = build_report(limits, metrics, disk_path)
        save_report(report_lines)
        print("Report saved to server_report.txt")
    except (ValueError, KeyError):
        print("Limits must include cpu, memory, and disk as numbers from 0 to 100.")
    except OSError as error:
        print(f"Could not read a file or disk path: {error}")

if __name__ == "__main__":
    main()

Your local monitor now produces a validated report for a disk path you can choose. Next, you will package all four projects into one GitHub portfolio.

Publish the Four-Project Portfolio

Your local health report now combines the Python skills from your earlier projects. This step packages the complete series into one Git repository that someone else can review.

You will publish that repository with GitHub. A top-level guide presents the series clearly. Ignore rules keep the local virtual environment and generated report outside the portfolio.

In this step, get ready to:
  • Gather the visible files from Projects 1 through 3.
  • Create the portfolio guide and ignore rules.
  • Commit the complete series and publish it to GitHub.
Gather the portfolio files

Finding each older project path can be fiddly because every project may live in a different folder. The copy pattern keeps visible project files while leaving any older hidden .git folders behind.

  • Return to the top-level python-basics-devops folder in the active XFCE Terminal by running this command:
cd ~/python-basics-devops

What does this command do?

This moves the terminal from project-4 to the folder that contains all four project directories. Git needs to start here so one history can cover the complete series.

  • Copy the visible Project 1 files by replacing the example source path before running this command:
cp -r "/home/your-name/path-to-project-1/"* ~/python-basics-devops/project-1/

What does this command do?

The command copies the visible contents from your saved Project 1 folder into project-1. The wildcard leaves hidden items such as an older .git folder outside the copy.

  • Repeat the copy for Project 2 with its real source path and the ~/python-basics-devops/project-2/ destination.
  • Repeat the copy for Project 3 with its real source path and the ~/python-basics-devops/project-3/ destination.
  • Return to Xed from earlier.
  • Open one copied file from each matching project folder to confirm the visible files reached their new destinations.

Seeing a copied file open from each destination confirms Projects 1 through 3 are inside the portfolio workspace.

Missing files from an earlier project?

Check that the source path points to the folder containing the saved project files. Keep the quotation marks around a path that contains spaces.

Use the matching destination folder for each project. Project 2 belongs in project-2. Project 3 belongs in project-3.

Help me copy my earlier project files into the portfolio folders.

The top-level README.md works like a label on a portfolio case. It explains what each folder demonstrates.

  • Create README.md inside ~/python-basics-devops with Xed.
  • Paste this portfolio guide into README.md:
# Python Basics for DevOps

This repository holds my four-project Python learning series.

## Projects

- `project-1`: My first Python basics project.
- `project-2`: My second Python basics project.
- `project-3`: My third Python basics project.
- `project-4`: A validated local server health report using Python and psutil.

## What I learned

I practiced variables, strings, numbers, lists, dictionaries, decisions, loops,
functions, comments, error handling, input validation, file reading and writing,
imports, virtual environments, packages, command-line input, Git, and GitHub.

## Run Project 4

```bash
cd project-4
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install -r requirements.txt
python3 server_report.py limits.txt
deactivate
```

The script validates percentage limits from 0 to 100. It writes live CPU,
memory, and disk results to `server_report.txt`.

Pass an optional disk path after `limits.txt` to monitor another mounted path.

What does this guide include?

  • The opening identifies this repository as your four-project Python learning series.
  • The project list gives each folder a clear purpose.
  • The skills section summarizes the Python and DevOps concepts you practiced.
  • The final section shows how to recreate the Project 4 environment and run the report.
  • Save README.md in Xed.

The saved Xed tab now shows the complete portfolio guide without an unsaved-change marker.

README saved in the wrong folder?

Check the file location in Xed. The file belongs directly inside ~/python-basics-devops beside the four project folders.

Help me place my portfolio README in the correct folder.

The top-level .gitignore defines files Git should leave on your computer. This keeps machine-specific packages and generated output out of the repository.

  • Create .gitignore inside ~/python-basics-devops with Xed.
  • Paste these ignore patterns into .gitignore:
.venv/
__pycache__/
*.pyc
server_report.txt

What do these patterns protect?

  • The .venv/ pattern keeps the disposable virtual environment out of Git.
  • The __pycache__/ pattern ignores Python cache directories.
  • The *.pyc pattern ignores compiled Python files.
  • The server_report.txt pattern ignores the generated report.
  • Save .gitignore in Xed.

The portfolio now has its public guide and its boundary for local-only files.

Can't see .gitignore?

Files beginning with a dot can be hidden in file browsers. Check the filename in the Xed tab to confirm it is exactly .gitignore.

Help me confirm my .gitignore filename and location.

✔️ Awesome, I've got everything!

Great. Double check that README.md and .gitignore are saved directly inside ~/python-basics-devops.

ⓧ I'd like to double check the full code

.venv/
__pycache__/
*.pyc
server_report.txt

Compare the ignore rules

Your top-level .gitignore should contain these four patterns in this order.

# Python Basics for DevOps

This repository holds my four-project Python learning series.

## Projects

- `project-1`: My first Python basics project.
- `project-2`: My second Python basics project.
- `project-3`: My third Python basics project.
- `project-4`: A validated local server health report using Python and psutil.

## What I learned

I practiced variables, strings, numbers, lists, dictionaries, decisions, loops,
functions, comments, error handling, input validation, file reading and writing,
imports, virtual environments, packages, command-line input, Git, and GitHub.

## Run Project 4

```bash
cd project-4
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install -r requirements.txt
python3 server_report.py limits.txt
deactivate
```

The script validates percentage limits from 0 to 100. It writes live CPU,
memory, and disk results to `server_report.txt`.

Pass an optional disk path after `limits.txt` to monitor another mounted path.

Compare the portfolio guide

Your top-level README.md should describe the four projects and include the Project 4 run commands.

import os
import sys

def main():
    # Stop safely when the filename argument is missing
    if len(sys.argv) < 2:
        print("Please provide a filename.")
        return

    # Read the supplied path after confirming it exists
    filename = sys.argv[1]

    # Report whether the supplied path exists
    if os.path.exists(filename):
        print(f"Found: {filename}")
    else:
        print(f"Missing: {filename}")

main()

Compare the file checker

The Project 4 file checker should include its imports, safe argument guard, and reusable path check.

cpu=80
memory=80
disk=80

Compare the warning limits

The three values in limits.txt should be restored to numeric limits.

psutil==7.2.2

Compare the dependency pin

The requirements file should contain the exact psutil package pin used by Project 4.

import os
import sys

import psutil

# Keep shared report settings in one place
OUTPUT_FILE = "server_report.txt"
CHECKS = ["cpu", "memory", "disk"]
DEFAULT_DISK_PATH = os.path.abspath(os.sep)

def read_limits(limits_filename):
    # Convert each key=value row into a checked percentage
    limits = {}
    with open(limits_filename, "r", encoding="utf-8") as limits_file:
        for line in limits_file:
            metric_name, limit_text = line.strip().split("=")
            limit_value = int(limit_text)
            # Reject percentages outside the possible range
            if limit_value < 0 or limit_value > 100:
                raise ValueError
            limits[metric_name] = limit_value
    return limits

def get_metrics(disk_path):
    # Collect one live percentage for each monitored resource
    return {
        "cpu": psutil.cpu_percent(interval=1),
        "memory": psutil.virtual_memory().percent,
        "disk": psutil.disk_usage(disk_path).percent,
    }

def build_report(limits, metrics, disk_path):
    # Start with a title and the path measured for disk usage
    report_lines = ["LOCAL SERVER HEALTH REPORT", f"Disk path: {disk_path}"]
    # Compare each live reading with its matching limit
    for metric_name in CHECKS:
        metric_value = metrics[metric_name]
        if metric_value >= limits[metric_name]:
            status = "Over limit"
        else:
            status = "Within limit"
        report_lines.append(
            f"{metric_name.upper()}: {metric_value:.1f}% - {status}"
        )
    return report_lines

def save_report(report_lines):
    # Write one report entry per line
    with open(OUTPUT_FILE, "w", encoding="utf-8") as report_file:
        for report_line in report_lines:
            report_file.write(report_line + "\n")

def get_input_paths():
    # Stop safely before reading a missing filename argument
    if len(sys.argv) < 2:
        print("Please provide a limits filename.")
        return None
    # Resolve the required file and optional disk path
    limits_filename = sys.argv[1]
    disk_path = (
        os.path.abspath(sys.argv[2])
        if len(sys.argv) > 2
        else DEFAULT_DISK_PATH
    )
    # Reject paths that cannot be used by the workflow
    if not os.path.exists(limits_filename):
        print("Limits file not found.")
        return None
    if not os.path.exists(disk_path):
        print("Disk path not found.")
        return None
    return limits_filename, disk_path

def main():
    # Stop when path validation has already printed a helpful message
    input_paths = get_input_paths()
    if input_paths is None:
        return
    limits_filename, disk_path = input_paths
    try:
        # Run the report pipeline in its required order
        limits = read_limits(limits_filename)
        metrics = get_metrics(disk_path)
        report_lines = build_report(limits, metrics, disk_path)
        save_report(report_lines)
        print("Report saved to server_report.txt")
    except (ValueError, KeyError):
        print("Limits must include cpu, memory, and disk as numbers from 0 to 100.")
    except OSError as error:
        print(f"Could not read a file or disk path: {error}")

if __name__ == "__main__":
    main()

Compare the health report

The Project 4 script should match the working report you tested in the previous step.

Commit a clean local snapshot

A repository stores the portfolio folder plus its history. Git records each deliberate snapshot so you can see how the work changes over time.

How does Git record your work?

Staging chooses the files that belong in the next snapshot. A commit saves that snapshot with a message.

A remote is the online copy of the local repository. You will connect that copy after the local commit is clean.

  • Initialize ~/python-basics-devops on the main branch by running:
git init -b main

What does this command do?

This creates the local Git history for the whole portfolio. The -b main option names the initial branch main.

🙋‍♀️ Git rejected the branch option?

The branch option requires Git 2.28.0 or newer. The setup check from Step 1 showed the installed Git release.

Help me initialize this repository on the main branch.

Before you check, do you think Git will include the Project 4 virtual environment?

  • Check the ignored virtual environment by running:
git status --short --ignored project-4/.venv

What does this check prove?

The short status format includes ignored paths for this check. The !! marker identifies a path excluded by Git.

You should see exactly !! project-4/.venv/. Your private package space stays local while requirements.txt records how to rebuild it.

Is .venv missing from the ignored output?

Confirm the top-level file is named exactly .gitignore. Check that its first line is .venv/.

Make sure the terminal is inside ~/python-basics-devops when you run the check.

Help me make Git ignore project-4/.venv.

  • Stage the portfolio and save its first snapshot by running these commands:
git add .
git commit -m "Complete Python Basics for DevOps series"

What do these commands do?

  • The git add . command stages the files allowed by .gitignore.
  • The commit saves that staged snapshot with the message Complete Python Basics for DevOps series.

Before you check, do you expect any portfolio files to remain outside the commit?

  • Check the local repository state by running:
git status --short

How do I read this result?

Short status lists files with uncommitted changes. A blank result means the snapshot contains every eligible file.

You should see no output. That clean status confirms the complete local portfolio snapshot is saved.

Still seeing files in Git status?

Files with status markers were changed after the commit or were not included in it. Review each listed path before creating another commit.

If the commit asks for your identity, follow Git's guidance to configure your author details. Run the commit again afterward.

Help me get a clean Git status after my portfolio commit.

Push the portfolio to GitHub

The local commit is ready for an online home. An empty GitHub repository avoids competing starter files and gives your local history a clean destination.

  • Create a new repository in your GitHub account.
  • Enter python-basics-devops as the repository name.
  • Leave the README initialization option unselected.
  • Leave the license initialization option unselected.
  • Leave the .gitignore initialization option unselected.
  • Create the empty repository.
  • Copy its Quick Setup URL into REMOTE-URL.
  • Replace REMOTE-URL with your copied Quick Setup URL before running these commands:
git remote add origin REMOTE-URL
git remote -v

What do these commands do?

The first command names your GitHub repository origin. The second command displays the saved remote addresses so you can check the connection before pushing.

You should see origin listed for the configured remote URLs. This confirms the local portfolio points to the repository you just created.

Remote URL looks wrong?

Copy the full URL from the repository's Quick Setup area. Check that the URL points to python-basics-devops in your GitHub account.

Help me verify my GitHub remote URL before pushing.

The first push may trigger a GitHub authentication prompt. This prompt protects access to your account.

Before you push, do you expect the empty GitHub repository to gain your local main branch?

  • Follow the GitHub sign-in prompt if authentication is requested.
  • Push the local main branch by running:
git push -u origin main

What does this push do?

This uploads the local main branch to origin. The upstream connection lets later pushes use the same destination.

The first push output should include [new branch] main -> main. Your four-project portfolio is now published online.

Push did not reach GitHub?

Check the remote with git remote -v if the push points to the wrong repository. Copy the full Quick Setup URL again if needed.

Complete the GitHub authentication flow if access is denied. Keep credentials outside your project files.

Help me push my local main branch to GitHub.

Before you run the final local check, do you expect Git to report any pending changes?

  • Confirm the committed portfolio remains clean by running:
git status --short

What confirms a clean repository?

This check lists only uncommitted changes. No output confirms the local files still match the published commit.

You should see no output. The local snapshot is clean after the push.

  • Inspect the latest commit by running:
git log --oneline -1

What does this history check show?

The one-line history format shows an abbreviated commit ID plus its message. The -1 option limits the result to the newest commit.

You should see one line ending in Complete Python Basics for DevOps series.

  • Open the published python-basics-devops repository in your browser.
  • Confirm the top level shows project-1, project-2, project-3, project-4, README.md, and .gitignore.
  • Open the project-4 folder on GitHub.
  • Confirm it shows limits.txt, check_file.py, requirements.txt, and server_report.py.
  • Confirm .venv and server_report.txt are absent from the published folder.

That repository view proves the four-project series is online. It also proves generated and machine-specific files stayed out of the portfolio.

Secret mission

Add an Overall Health Result

Your report lists each metric separately. Add one final HEALTHY or WARNING line, prove both paths with controlled readings, and publish the improvement to GitHub.

Clean Up Your Resources

Clean Up Your Resources

Your local files plus the published GitHub repository can stay in place without ongoing project costs. Decide whether to keep them, pause your work, or delete them.

Resources you used:

  • The local ~/python-basics-devops portfolio folder containing Projects 1-4.
  • The ignored project-4/.venv virtual environment containing psutil 7.2.2.
  • The published python-basics-devops repository on GitHub.

Keep everything running

No action is needed because server_report.py exits after creating the report. Your local portfolio plus its published repository remain available for future demonstrations.

  • Leave ~/python-basics-devops on your computer.
  • Leave the python-basics-devops GitHub repository online.
  • Keep project-4/requirements.txt so you can rebuild project-4/.venv when needed.

Pause - I'll come back to this later

Pausing closes your editing tools while preserving every project file. Your portfolio remains ready for the next time you want to run or improve it.

  • Close the Xed window from earlier.
  • Close XFCE Terminal from earlier.
  • Leave ~/python-basics-devops unchanged.
  • Leave the python-basics-devops GitHub repository online.

Delete - I don't want to use this again

Removing your project resources is a big step. The local options separate package cleanup from complete portfolio deletion.

  • Remove only project-4/.venv to reclaim its local package space by running this command:
rm -rf ~/python-basics-devops/project-4/.venv

What Does This Remove?

This removes the isolated packages installed for Project 4. Your scripts plus requirements.txt remain inside ~/python-basics-devops.

  • Remove the entire local portfolio when you no longer want Projects 1-4 by running this command:
rm -rf ~/python-basics-devops

What Leaves Your Computer?

This removes the entire local portfolio from your computer. The published GitHub repository stays online.

The online repository has a separate deletion path.

Repository Deletion Is Permanent

Deleting the online portfolio can feel drastic. GitHub treats repository deletion as permanent.

Some deleted repositories may be restorable within 90 days.

  • Open the python-basics-devops repository on GitHub.
  • Select Settings in the repository navigation.
  • Select General in the settings sidebar.
  • Scroll to Danger Zone.
  • Select Delete this repository.
  • Complete the confirmation steps shown by GitHub.

GitHub removes the online portfolio after it confirms the deletion.

Nice Work!

Nice Work!

You made it! Your four-project GitHub portfolio now showcases a validated Python health report with safe input handling and configurable disk monitoring.

You've learned how to:

  • Build a safe command-line file checker with sys.argv and os.path.exists(). A length guard replaces missing-input tracebacks with a clear instruction.
  • Create a psutil health report that validates limits from 0 to 100. Descriptive variables and purpose-focused comments keep the workflow readable.
  • Monitor a configurable disk path with an operating-system-aware default. Publish the four-project portfolio through GitHub.
  • Secret Mission: Add an overall HEALTHY or WARNING result. Prove both outcomes with controlled data and publish the second commit.

Ready to quiz yourself?