365 Python Devops Projects
365 Python Devops Projects
365 8 3 ∞
Projects Learning Parts Difficulty Tiers Real-World Uses
■
■ Linux ■ Docker Kubernetes ■ AWS ■ CI/CD ■ Monitoring
■ Structure
This book contains 365 Python projects organised into 8 thematic parts, progressing from absolute Python
basics through to production-grade DevOps automation. Each project is a complete, runnable script with
every line explained.
■ Line-by-Line Explanations
After each code block you will find a breakdown of every significant line — what it does, why it is written that
way, and what happens if you omit it. This is not just 'what' but 'why'.
■ GitHub/GitLab Ready
Each project is a self-contained .py file. The companion repository structure
(part_01_foundations/day_001_hello_world.py) mirrors the book chapters. Clone the repo, run the script,
read the explanation — learn by doing.
■ Prerequisites
Python 3.8 or newer. A Linux terminal (Ubuntu/Debian recommended). Docker for container projects. AWS
CLI configured for cloud projects. All third-party libraries are noted at the top of each script with pip install
instructions.
■ Learning Path
Days 1-30: Python core + CLI scripting. Days 31-80: File I/O, YAML, JSON, automation. Days 81-130:
Networking, HTTP, APIs. Days 131-180: Docker and containers. Days 181-230: AWS and cloud automation.
Days 231-280: CI/CD pipelines and testing. Days 281-330: Kubernetes and infrastructure. Days 331-365:
Monitoring, observability, and capstone projects.
Difficulty Legend
Beginner Core Python, standard library only. No prior DevOps knowledge required.
Intermediate Common third-party libraries. Assumes Linux comfort and basic DevOps concepts.
Advanced Production patterns, cloud SDKs, Kubernetes APIs. Builds on all prior projects.
Table of Contents
PART I — PYTHON FOUNDATIONS (Days 1–30)
Day 1: Hello, DevOps World — Beginner
Day 2: Environment Variable Manager — Beginner
Day 3: File & Directory Scanner — Beginner
Day 4: Log File Parser — Beginner
Day 5: System Health Monitor — Beginner
Each project in this part is a complete, runnable Python script. Read the description,
5 study the code, then check the line-by-line explanation below it. Type the code
yourself for best retention.
Projects
Your first Python script — printing system info to stdout, the DevOps way.
■ Source Code
1 import platform # Standard library for OS/hardware info
2 import sys # Provides access to Python interpreter details
3 import os # Operating system interface
4
5 def main():
6 # [Link]() returns 'Linux', 'Windows', or 'Darwin'
7 print(f"OS : {[Link]()} {[Link]()}")
8 # [Link] gives the full Python version string
9 print(f"Python : {[Link]()[0]}")
10 # [Link]() returns the current working directory
11 print(f"Working Dir : {[Link]()}")
12 # [Link]() returns the login name of the current user
13 try:
14 print(f"User : {[Link]()}")
15 except OSError:
16 # Fallback when running in non-interactive shells (CI/CD)
17 print(f"User : {[Link]('USER', 'unknown')}")
18
19 if __name__ == "__main__":
20 # This guard ensures main() only runs when script is executed directly
21 # not when it is imported as a module
22 main()
■ Line-by-Line Explanation
1 import platform
loads the platform module from Python's standard library.
2 [Link]()
returns the OS name; essential for writing cross-platform DevOps scripts.
3 [Link]
a string like '3.11.0 (default, ...)'; we split on whitespace and take index 0.
4 [Link]()
Current Working Directory; important to know when scripts run in pipelines.
5 try/except OSError
[Link]() can fail inside Docker containers or CI runners. Always handle it.
6 if __name__ == '__main__'
Python sets __name__ to '__main__' only when the file is executed directly. This pattern is the foundation of
reusable, importable modules.
Read, set, and validate environment variables — core to 12-factor app config.
■ Source Code
1 import os
2 import sys
3
4 # Dictionary of required environment variables and their descriptions
5 REQUIRED_VARS = {
6 "DB_HOST": "Database hostname",
7 "DB_PORT": "Database port (e.g. 5432)",
8 "APP_ENV": "Deployment environment (dev/staging/prod)",
9 }
10
11 def check_env():
12 """Check that all required environment variables are set."""
13 missing = [] # List to collect missing variable names
14
15 for var, description in REQUIRED_VARS.items():
16 # [Link]() returns None if variable is not set
17 # unlike [Link][var] which raises KeyError
18 value = [Link](var)
19 if value is None:
20 [Link](f" - {var}: {description}")
21 else:
22 # Mask secrets: only show first 3 chars + asterisks
23 masked = value[:3] + "*" * (len(value) - 3) if len(value) > 3 else "***"
24 print(f"[OK] {var} = {masked}")
25
26 if missing:
27 print("\n[ERROR] Missing required environment variables:")
28 for m in missing:
29 print(m)
30 # Exit with code 1 to signal failure to the calling process/pipeline
31 [Link](1)
32 else:
33 print("\n[PASS] All required environment variables are set.")
34
35 def set_defaults():
36 """Set safe default values for optional variables."""
37 defaults = {
38 "LOG_LEVEL": "INFO",
39 "MAX_RETRIES": "3",
40 "TIMEOUT": "30",
41 }
■ Line-by-Line Explanation
1 REQUIRED_VARS dict
Maps variable names to human-readable descriptions. This pattern makes your validation self-documenting.
2 [Link](var)
Safe dictionary-style access. Returns None instead of raising an exception if the key is missing.
3 [Link](1)
Exits the process with a non-zero return code. CI/CD pipelines (Jenkins, GitHub Actions) treat any non-zero exit as
a FAILURE and stop the pipeline.
4 Masking secrets
Never print raw secrets to logs. The slice value[:3] + '*'*(len-3) pattern reveals enough to confirm the right variable
is set without exposing the value.
5 [Link]()
Atomic check-and-set: if the key exists, it does nothing; if not, it sets the default. Safe to call multiple times.
Recursively walk a directory tree, categorize files by extension, and report sizes.
■ Source Code
1 import os
2 import sys
3 from collections import defaultdict # Dict that auto-initialises missing keys
4
5 def human_readable(size_bytes: int) -> str:
6 """Convert bytes to a human-readable string (KB, MB, GB)."""
7 # Iterate through units; each step is 1024x larger
8 for unit in ["B", "KB", "MB", "GB", "TB"]:
9 if size_bytes < 1024.0:
10 return f"{size_bytes:.1f} {unit}"
11 size_bytes /= 1024.0
12 return f"{size_bytes:.1f} PB"
13
14 def scan_directory(root_path: str) -> dict:
15 """Walk the directory tree and collect stats per file extension."""
16 stats = defaultdict(lambda: {"count": 0, "total_size": 0})
17
18 # [Link]() is a generator that yields (dirpath, dirnames, filenames)
19 # It traverses the entire tree depth-first by default
20 for dirpath, dirnames, filenames in [Link](root_path):
21 # Skip hidden directories (like .git) to avoid noise
22 # Modifying dirnames IN-PLACE prunes the walk — [Link] won't descend
23 dirnames[:] = [d for d in dirnames if not [Link](".")]
24
25 for filename in filenames:
26 # [Link] builds the full path correctly on any OS
27 full_path = [Link](dirpath, filename)
28 # [Link] splits "[Link]" into ("[Link]", ".gz")
29 _, ext = [Link](filename)
30 ext = [Link]() if ext else "(no extension)"
31
32 try:
33 # [Link] returns file size in bytes
34 size = [Link](full_path)
35 stats[ext]["count"] += 1
36 stats[ext]["total_size"] += size
37 except (PermissionError, OSError):
38 # Files might be unreadable (sockets, broken symlinks)
39 pass
40
41 return dict(stats)
42
43 def report(stats: dict, root_path: str):
44 """Print a formatted summary table."""
45 print(f"\nDirectory Scan Report: {root_path}")
46 print("-" * 50)
47 print(f"{'Extension':<20} {'Files':>6} {'Total Size':>12}")
48 print("-" * 50)
49
50 # Sort by total size descending to see largest categories first
51 for ext, data in sorted([Link](), key=lambda x: -x[1]["total_size"]):
52 print(f"{ext:<20} {data['count']:>6} {human_readable(data['total_size']):>12}")
53
54 if __name__ == "__main__":
55 path = [Link][1] if len([Link]) > 1 else "."
56 if not [Link](path):
57 print(f"Error: '{path}' is not a directory.")
58 [Link](1)
59 stats = scan_directory(path)
60 report(stats, path)
■ Line-by-Line Explanation
1 defaultdict(lambda: {...})
A dict that auto-creates a new {'count':0,'total_size':0} entry for any unseen key. Avoids key-not-found errors.
2 [Link]() generator
Memory-efficient recursive traversal. It yields one tuple per directory without loading the whole tree into memory.
3 dirnames[:] = [...]
Modifying the list IN-PLACE (slice assignment) is the only way to prune [Link]'s traversal. Assigning a new list
(dirnames = [...]) has no effect.
4 [Link]
Always use this instead of splitting on '.' yourself. It correctly handles files like '.bashrc' (no extension) and
'[Link]'.
5 [Link][1]
argv[0] is the script name; argv[1] is the first argument. Defaulting to '.' means the script works with no arguments.
Parse an NGINX/Apache log file, count status codes, and find top IPs.
■ Source Code
1 import re # Regular expressions
2 import sys
3 from collections import Counter # Efficient frequency counting
4
5 # NGINX combined log format pattern
6 # Each named group (?P<name>...) creates a key in the match dictionary
7 LOG_PATTERN = [Link](
8 r'(?P<ip>\S+)\s+' # Client IP address
9 r'\S+\s+\S+\s+' # ident and auth fields (usually -)
10 r'\[(?P<time>[^\]]+)\]\s+' # Timestamp inside square brackets
11 r'"(?P<method>\S+)\s+' # HTTP method (GET, POST, etc.)
12 r'(?P<path>\S+)\s+\S+"\s+'# Request path + HTTP version
13 r'(?P<status>\d{3})\s+' # 3-digit HTTP status code
14 r'(?P<size>\d+|-)' # Response size in bytes (or - if 0)
15 )
16
17 def parse_log(filepath: str):
18 """Parse a log file and return lists of parsed entries."""
19 status_counts = Counter() # {200: 1500, 404: 23, ...}
20 ip_counts = Counter() # {'[Link]': 450, ...}
21 errors = []
22
23 with open(filepath, "r", encoding="utf-8", errors="replace") as f:
24 for line_num, line in enumerate(f, 1):
25 # [Link]() tries to match at the START of the string
26 match = LOG_PATTERN.match([Link]())
27 if match:
28 data = [Link]() # Returns dict of named groups
29 status_counts[data["status"]] += 1
30 ip_counts[data["ip"]] += 1
31 else:
32 [Link](f"Line {line_num}: {line[:60]}")
33
34 return status_counts, ip_counts, errors
35
36 def report(status_counts, ip_counts, errors):
37 """Print analysis report."""
38 total = sum(status_counts.values())
39 print(f"\nTotal Requests: {total:,}")
40
41 print("\nStatus Code Breakdown:")
■ Line-by-Line Explanation
1 [Link]()
Pre-compiles the regex pattern once. Much faster than [Link](pattern, line) in a loop which recompiles every
iteration.
3 Counter()
A dict subclass optimised for counting. counter[key] returns 0 for missing keys (no KeyError). most_common(n)
returns top-n items sorted by frequency.
4 errors='replace' in open()
Replaces undecodable bytes with the Unicode replacement character instead of crashing. Essential for real-world
log files.
5 enumerate(f, 1)
Iterates with a counter starting at 1, giving you the actual line number for error reporting.
■ Source Code
1 import os
2 import sys
3 import time
4
5 def get_cpu_usage():
6 """Read CPU usage from /proc/stat (Linux only)."""
7 try:
8 with open("/proc/stat") as f:
9 # First line: cpu user nice system idle iowait irq softirq
10 fields = [Link]().split()
11 # Total time = sum of all CPU time fields
12 total = sum(int(x) for x in fields[1:])
13 # Idle time is the 4th field (index 4)
14 idle = int(fields[4])
15 # Usage % = (total - idle) / total * 100
16 return (total - idle) / total * 100
17 except FileNotFoundError:
18 # /proc/stat doesn't exist on macOS/Windows
19 return -1.0
20
21 def get_memory_usage():
22 """Parse /proc/meminfo to get memory statistics."""
23 mem = {}
24 try:
25 with open("/proc/meminfo") as f:
26 for line in f:
27 # Each line: "MemTotal: 16384000 kB"
28 key, value = [Link](":", 1)
29 # Convert kB value to bytes
30 mem[[Link]()] = int([Link]().split()[0]) * 1024
31 total = [Link]("MemTotal", 0)
32 available = [Link]("MemAvailable", 0)
33 used = total - available
34 pct = used / total * 100 if total > 0 else 0
35 return total, used, pct
36 except FileNotFoundError:
37 return 0, 0, 0.0
38
39 def get_disk_usage(path="/"):
40 """Use [Link] to get disk statistics."""
41 # statvfs returns filesystem statistics for the given path
42 st = [Link](path)
43 # f_blocks * f_frsize = total size in bytes
44 total = st.f_blocks * st.f_frsize
45 # f_bavail = blocks available to non-root users
46 free = st.f_bavail * st.f_frsize
47 used = total - free
48 pct = used / total * 100 if total > 0 else 0
49 return total, used, pct
50
51 def human_readable(b):
52 for unit in ["B","KB","MB","GB","TB"]:
53 if b < 1024: return f"{b:.1f}{unit}"
54 b /= 1024
55 return f"{b:.1f}PB"
56
57 def check_thresholds(cpu_pct, mem_pct, disk_pct,
58 cpu_warn=80, mem_warn=85, disk_warn=90):
59 """Emit WARNING or CRITICAL alerts based on thresholds."""
60 alerts = []
61 if cpu_pct > cpu_warn:
62 [Link](f" [WARN] CPU usage {cpu_pct:.1f}% exceeds {cpu_warn}%")
63 if mem_pct > mem_warn:
64 [Link](f" [WARN] Memory usage {mem_pct:.1f}% exceeds {mem_warn}%")
65 if disk_pct > disk_warn:
66 [Link](f" [CRIT] Disk usage {disk_pct:.1f}% exceeds {disk_warn}%")
67 return alerts
68
69 def monitor(interval=5, iterations=3):
70 for i in range(iterations):
71 print(f"\n=== Health Check #{i+1} @ {[Link]('%H:%M:%S')} ===")
72 cpu = get_cpu_usage()
73 mt, mu, mp = get_memory_usage()
74 dt, du, dp = get_disk_usage("/")
75
76 print(f" CPU : {cpu:5.1f}%")
77 print(f" Memory: {mp:5.1f}% ({human_readable(mu)} / {human_readable(mt)})")
78 print(f" Disk : {dp:5.1f}% ({human_readable(du)} / {human_readable(dt)})")
79
80 alerts = check_thresholds(cpu, mp, dp)
81 if alerts:
82 print(" ALERTS:")
83 for a in alerts: print(a)
84 else:
85 print(" [OK] All metrics within thresholds.")
86
87 if i < iterations - 1:
88 [Link](interval)
89
90 if __name__ == "__main__":
91 monitor()
■ Line-by-Line Explanation
1 /proc/stat
Linux virtual filesystem that exposes kernel data. Reading it is zero-cost; the kernel generates the content
on-the-fly.
2 [Link]()
POSIX system call for filesystem statistics. f_bavail (not f_bfree) gives the blocks available to regular users (root
has a reserved margin).
3 Threshold-based alerting
The pattern of parameterised thresholds with warn/crit levels mirrors Nagios, Prometheus Alertmanager, and
CloudWatch Alarms.
4 [Link](interval)
Pauses the process for the specified number of seconds. Essential for polling-based monitors to avoid 100% CPU.
5 [Link]('%H:%M:%S')
Formats current local time as HH:MM:SS. Use [Link]() or [Link] for UTC timestamps in
production.
Each project in this part is a complete, runnable Python script. Read the description,
3 study the code, then check the line-by-line explanation below it. Type the code
yourself for best retention.
Projects
INTERMEDIA
DAY 031
TE
■ Source Code
1 import yaml # pip install pyyaml
2 import sys
3 import os
4 from typing import Any
5
6 # Define expected schema as a dict of {key: expected_type}
7 SCHEMA = {
8 "app_name": str,
9 "version": str,
10 "replicas": int,
11 "port": int,
12 "image": str,
13 "env": dict,
14 }
15
16 def load_yaml(filepath: str) -> dict:
17 """Load and parse a YAML file safely."""
18 if not [Link](filepath):
19 raise FileNotFoundError(f"Config file not found: {filepath}")
20
21 with open(filepath, "r") as f:
22 # yaml.safe_load() prevents arbitrary Python object instantiation
23 # NEVER use [Link]() without Loader= - it is a security risk
24 data = yaml.safe_load(f)
25
26 # safe_load returns None for empty files
27 if data is None:
28 return {}
29 return data
30
31 def validate(config: dict, schema: dict) -> list:
32 """Validate config against schema. Returns list of error strings."""
33 errors = []
34 for key, expected_type in [Link]():
35 if key not in config:
36 [Link](f"Missing required key: '{key}'")
37 elif not isinstance(config[key], expected_type):
38 actual = type(config[key]).__name__
39 [Link](
40 f"'{key}': expected {expected_type.__name__}, got {actual}"
41 )
42 return errors
43
44 def merge_configs(base: dict, override: dict) -> dict:
45 """Deep-merge two configs. Override values win on conflict."""
46 result = [Link]()
47 for key, value in [Link]():
48 # If both are dicts, recurse for deep merge
49 if key in result and isinstance(result[key], dict) and isinstance(value, dict):
50 result[key] = merge_configs(result[key], value)
51 else:
52 # Otherwise override value replaces base value
53 result[key] = value
54 return result
55
56 def main():
57 base_cfg = load_yaml("[Link]")
58 env = [Link]("APP_ENV", "dev")
59 env_cfg = load_yaml(f"config.{env}.yaml") if [Link](f"config.{env}.yaml") else {}
60
61 # Merge: env-specific values override base values
62 final_cfg = merge_configs(base_cfg, env_cfg)
63
64 print(f"Loaded config for environment: {env}")
65 errors = validate(final_cfg, SCHEMA)
66
67 if errors:
68 print("\n[VALIDATION FAILED]")
69 for e in errors: print(f" - {e}")
70 [Link](1)
71
72 print("[VALID] Configuration passed all checks.")
73 print("\nEffective Configuration:")
74 # [Link]() serialises a Python dict back to YAML format
75 print([Link](final_cfg, default_flow_style=False))
76
77 if __name__ == "__main__":
78 main()
■ Line-by-Line Explanation
1 yaml.safe_load()
The ONLY safe way to load YAML. [Link]() without a Loader can execute arbitrary Python code embedded in
the YAML file — a critical security vulnerability.
4 Environment layering
base → dev/staging/prod is the 12-factor app config pattern. It avoids duplicating common settings while allowing
per-env overrides.
5 [Link](default_flow_style=False)
Produces multi-line human-readable YAML instead of the compact {key: val} inline style.
Flatten nested JSON API responses into CSV for reporting and analysis.
■ Source Code
1 import json
2 import csv
3 import sys
4 import os
5 from typing import Any
6
7 def flatten_dict(d: dict, parent_key: str = "", sep: str = ".") -> dict:
8 """Flatten nested dicts: {a: {b: 1}} -> {'a.b': 1}"""
9 items = []
10 for k, v in [Link]():
11 # Build the dotted key path
12 new_key = f"{parent_key}{sep}{k}" if parent_key else k
13 if isinstance(v, dict):
14 # Recurse into nested dicts
15 [Link](flatten_dict(v, new_key, sep).items())
16 elif isinstance(v, list):
17 # Convert lists to a comma-separated string
18 # (full list flattening would explode row count)
19 [Link]((new_key, [Link](v)))
20 else:
21 [Link]((new_key, v))
22 return dict(items)
23
24 def json_to_csv(json_filepath: str, csv_filepath: str):
25 """Convert a JSON file (list of objects) to CSV."""
26 with open(json_filepath, "r") as f:
27 data = [Link](f) # [Link]() reads from file object
28
29 # Handle both a list of records and a single object
30 if isinstance(data, dict):
31 data = [data]
32
33 if not data:
34 print("Empty JSON — no CSV written.")
35 return
36
37 # Flatten each record
38 flat_records = [flatten_dict(record) for record in data]
39
40 # Collect ALL keys across all records to build a complete header
41 # Some records may have keys others don't (sparse JSON)
42 all_keys = []
43 seen = set()
44 for record in flat_records:
45 for key in [Link]():
46 if key not in seen:
47 all_keys.append(key)
48 [Link](key)
49
50 with open(csv_filepath, "w", newline="", encoding="utf-8") as f:
51 # DictWriter writes dicts as CSV rows using field names as headers
52 writer = [Link](f, fieldnames=all_keys,
53 extrasaction="ignore",
54 restval="") # restval fills missing keys
55 [Link]()
56 [Link](flat_records)
57
58 print(f"Converted {len(flat_records)} records -> {csv_filepath}")
59 print(f"Columns: {len(all_keys)}")
60
61 if __name__ == "__main__":
62 src = [Link][1] if len([Link]) > 1 else "[Link]"
63 dst = [Link][2] if len([Link]) > 2 else "[Link]"
64 json_to_csv(src, dst)
■ Line-by-Line Explanation
1 Recursive flatten
Nested JSON is the norm with API responses. Flattening with dotted paths ([Link]) preserves meaning
while making data tabular.
4 restval=''
When a record doesn't have a particular key, DictWriter fills the cell with this value. Without it, DictWriter raises a
ValueError for sparse records.
5 newline='' in open()
Required by the csv module documentation to prevent extra blank lines on Windows (the csv module handles its
own line termination).
INTERMEDIA
DAY 033
TE
■ Source Code
1 import os
2 import sys
3 import shutil
4 import tarfile
5 import time
6 import glob
7 from pathlib import Path
8
9 class BackupManager:
10 def __init__(self, source_dir: str, backup_dir: str, retain_count: int = 7):
11 [Link] = Path(source_dir) # [Link] for cleaner path ops
12 self.backup_dir = Path(backup_dir)
13 self.retain_count = retain_count # How many backups to keep
14
15 # Create backup directory if it doesn't exist
16 # parents=True creates intermediate directories; exist_ok=True is idempotent
17 self.backup_dir.mkdir(parents=True, exist_ok=True)
18
19 def create_backup(self) -> Path:
20 """Create a gzip-compressed tar archive of the source directory."""
21 # Timestamp format: YYYYMMDD_HHMMSS — lexicographic = chronological order
22 timestamp = [Link]("%Y%m%d_%H%M%S")
23 archive_name = f"backup_{[Link]}_{timestamp}.[Link]"
24 archive_path = self.backup_dir / archive_name # Path / operator joins paths
25
26 print(f"Creating backup: {archive_name}")
27
28 # [Link] with 'w:gz' mode creates a gzip-compressed archive
29 # 'w:bz2' for bzip2 (smaller but slower), 'w:xz' for xz (smallest)
30 with [Link](archive_path, "w:gz") as tar:
31 # arcname sets the root directory name inside the archive
32 [Link](str([Link]), arcname=[Link])
33
34 size = archive_path.stat().st_size
35 print(f" Backup created: {size / 1024 / 1024:.1f} MB")
36 return archive_path
37
38 def apply_retention(self):
39 """Delete old backups, keeping only the N most recent."""
40 # glob to find all backup archives, sorted by name (= by time)
■ Line-by-Line Explanation
1 [Link]
The modern way to handle paths in Python 3. Path('/tmp') / 'backups' / '[Link]' is safer and clearer than
[Link]().
2 [Link]('w:gz')
Creates a new gzip-compressed archive. The 'w:gz' mode string is more explicit than relying on filename
extension.
Each project in this part is a complete, runnable Python script. Read the description,
2 study the code, then check the line-by-line explanation below it. Type the code
yourself for best retention.
Projects
INTERMEDIA
DAY 081
TE
Ping a list of HTTP endpoints and report their status, latency, and SSL expiry.
■ Source Code
1 import [Link] # Built-in HTTP client (no extra install needed)
2 import [Link]
3 import ssl
4 import socket
5 import time
6 import json
7 from datetime import datetime
8
9 ENDPOINTS = [
10 "[Link]
11 "[Link]
12 "[Link]
13 ]
14
15 def check_ssl_expiry(hostname: str, port: int = 443) -> int:
16 """Return days until SSL certificate expires. -1 if error."""
17 ctx = ssl.create_default_context()
18 try:
19 with ctx.wrap_socket([Link](), server_hostname=hostname) as s:
20 # Set a 5 second timeout to avoid hanging
21 [Link](5)
22 [Link]((hostname, port))
23 cert = [Link]() # Returns dict of certificate fields
24 # notAfter format: "Oct 1 00:00:00 2025 GMT"
25 expiry = [Link](cert["notAfter"], "%b %d %H:%M:%S %Y %Z")
26 return (expiry - [Link]()).days
27 except Exception:
28 return -1
29
30 def check_endpoint(url: str, timeout: int = 10) -> dict:
31 """Check a single endpoint and return a result dict."""
32 result = {
33 "url": url,
34 "status": None,
35 "latency_ms": None,
36 "ssl_days": None,
37 "error": None,
38 }
39
40 start = [Link]() # monotonic clock is immune to system time changes
41
42 try:
43 # [Link] opens the URL and returns a response object
44 req = [Link](url, headers={"User-Agent": "DevOps-Checker/1.0"})
45 with [Link](req, timeout=timeout) as resp:
46 result["status"] = [Link] # HTTP status code (200, 404, etc.)
47 result["latency_ms"] = round(([Link]() - start) * 1000, 1)
48
49 except [Link] as e:
50 # HTTPError is raised for 4xx/5xx responses (still a valid response)
51 result["status"] = [Link]
52 result["latency_ms"] = round(([Link]() - start) * 1000, 1)
53
54 except [Link] as e:
55 # URLError covers DNS failures, connection refused, timeouts
56 result["error"] = str([Link])
57
58 # Check SSL for https URLs
59 if [Link]("[Link]
60 # Extract hostname from URL (crude but effective)
61 hostname = [Link]("//")[1].split("/")[0]
62 result["ssl_days"] = check_ssl_expiry(hostname)
63
64 return result
65
66 def report(results: list):
67 print(f"\n{'URL':<40} {'Status':>7} {'Latency':>10} {'SSL Days':>10} {'Note'}")
68 print("-" * 80)
69 for r in results:
70 ssl_str = str(r["ssl_days"]) if r["ssl_days"] is not None else "N/A"
71 note = r["error"] or ("WARNING: cert expires soon" if r["ssl_days"] and r["ssl_days"] < 30 else
"OK")
72 lat = f"{r['latency_ms']}ms" if r["latency_ms"] else "N/A"
73 print(f"{r['url']:<40} {str(r['status']):>7} {lat:>10} {ssl_str:>10} {note}")
74
75 if __name__ == "__main__":
76 results = [check_endpoint(url) for url in ENDPOINTS]
77 report(results)
78 # Exit non-zero if any endpoint is not 200
79 if any(r["status"] != 200 for r in results):
80 import sys; [Link](1)
■ Line-by-Line Explanation
1 [Link]()
Unlike [Link](), the monotonic clock never goes backwards (immune to NTP adjustments). Always use it for
measuring durations.
2 [Link]
Allows setting custom headers. User-Agent is important: some servers block requests without it.
3 [Link] vs URLError
HTTPError is a subclass of URLError. Catch HTTPError first to handle 4xx/5xx responses (you still get the status
code). URLError catches network-level failures.
4 ssl.create_default_context() + wrap_socket()
The standard way to establish a TLS connection and inspect the certificate without making a full HTTP request.
INTERMEDIA
DAY 082
TE
A robust API client with exponential backoff, rate-limit handling, and response caching.
■ Source Code
1 import [Link]
2 import [Link]
3 import [Link]
4 import json
5 import time
6 import os
7 import hashlib
8
9 class APIClient:
10 """Reusable REST API client with retry, backoff, and caching."""
11
12 def __init__(self, base_url: str, api_key: str = None,
13 max_retries: int = 3, cache_dir: str = None):
14 self.base_url = base_url.rstrip("/")
15 self.api_key = api_key
16 self.max_retries = max_retries
17 self.cache_dir = cache_dir
18
19 if cache_dir:
20 [Link](cache_dir, exist_ok=True)
21
22 def _cache_key(self, url: str, params: dict) -> str:
23 """Generate a unique cache key from URL + params."""
24 cache_str = url + [Link](params or {}, sort_keys=True)
25 # MD5 is fast; we're not using it for security here
26 return hashlib.md5(cache_str.encode()).hexdigest()
27
28 def _get_cached(self, key: str, ttl_seconds: int = 300):
29 """Return cached data if fresh, else None."""
30 if not self.cache_dir: return None
31 path = [Link](self.cache_dir, f"{key}.json")
32 if not [Link](path): return None
33 age = [Link]() - [Link](path)
34 if age > ttl_seconds: return None
35 with open(path) as f:
36 return [Link](f)
37
38 def _set_cached(self, key: str, data):
39 """Write data to the cache."""
40 if not self.cache_dir: return
91 raise
92
93 raise RuntimeError(f"All {self.max_retries} attempts failed for {endpoint}")
94
95 if __name__ == "__main__":
96 client = APIClient("[Link] cache_dir="/tmp/api_cache")
97 # Fetch GitHub user info
98 user = [Link]("/users/torvalds")
99 print(f"Name : {[Link]('name')}")
100 print(f"Public Repos: {[Link]('public_repos')}")
101 print(f"Followers : {[Link]('followers')}")
■ Line-by-Line Explanation
Each project in this part is a complete, runnable Python script. Read the description,
2 study the code, then check the line-by-line explanation below it. Type the code
yourself for best retention.
Projects
INTERMEDIA
DAY 131
TE
Query the Docker daemon via its Unix socket API to list and inspect containers.
■ Source Code
1 import socket
2 import json
3 import os
4 import sys
5
6 class DockerClient:
7 """Minimal Docker API client using the Unix socket directly."""
8
9 SOCKET_PATH = "/var/run/[Link]" # Default Docker daemon socket
10
11 def __init__(self):
12 if not [Link](self.SOCKET_PATH):
13 raise RuntimeError(
14 f"Docker socket not found at {self.SOCKET_PATH}\n"
15 "Is Docker running? Are you in the docker group?"
16 )
17
18 def _request(self, method: str, path: str) -> dict:
19 """Send an HTTP request over the Unix domain socket."""
20 # Unix domain socket is like TCP but uses a file instead of IP:port
21 sock = [Link](socket.AF_UNIX, socket.SOCK_STREAM)
22 [Link](10)
23
24 try:
25 [Link](self.SOCKET_PATH)
26
27 # HTTP/1.0 request format: METHOD /path HTTP/1.0\r\nHost: localhost\r\n\r\n
28 request = (
29 f"{method} {path} HTTP/1.0\r\n"
30 f"Host: localhost\r\n"
31 f"Accept: application/json\r\n"
32 f"\r\n"
33 )
34 [Link]([Link]())
35
36 # Read the full response into memory
37 response = b""
38 while True:
39 chunk = [Link](4096)
40 if not chunk:
41 break
42 response += chunk
43 finally:
44 [Link]() # Always close the socket
45
46 # Split HTTP headers from body at \r\n\r\n boundary
47 header_end = [Link](b"\r\n\r\n")
48 body = response[header_end + 4:]
49
50 return [Link]([Link]("utf-8"))
51
52 def list_containers(self, all_containers: bool = False) -> list:
53 """List containers (running only by default)."""
54 params = "?all=true" if all_containers else ""
55 return self._request("GET", f"/containers/json{params}")
56
57 def inspect_container(self, container_id: str) -> dict:
58 """Get full details of a specific container."""
59 return self._request("GET", f"/containers/{container_id}/json")
60
61 def get_stats(self, container_id: str) -> dict:
62 """Get real-time resource stats for a container (single snapshot)."""
63 return self._request("GET", f"/containers/{container_id}/stats?stream=false")
64
65 def format_bytes(b):
66 for unit in ["B","KB","MB","GB"]:
67 if b < 1024: return f"{b:.0f}{unit}"
68 b /= 1024
69 return f"{b:.0f}TB"
70
71 def main():
72 client = DockerClient()
73 containers = client.list_containers(all_containers=True)
74
75 if not containers:
76 print("No containers found.")
77 return
78
79 print(f"{'ID':<14} {'Name':<30} {'Image':<25} {'Status':<15} {'CPU%':>7} {'Memory':>10}")
80 print("-" * 105)
81
82 for c in containers:
83 cid = c["Id"][:12]
84 name = c["Names"][0].lstrip("/") if c["Names"] else "unknown"
85 image = c["Image"][:24]
86 status = c["Status"][:14]
87
88 # Only get stats for running containers
89 cpu_pct = "-"
90 mem_str = "-"
91 if c["State"] == "running":
92 try:
93 stats = client.get_stats(c["Id"])
94 # CPU % calculation from Docker stats API
95 cpu_delta = stats["cpu_stats"]["cpu_usage"]["total_usage"] - \
96 stats["precpu_stats"]["cpu_usage"]["total_usage"]
97 sys_delta = stats["cpu_stats"]["system_cpu_usage"] - \
98 stats["precpu_stats"]["system_cpu_usage"]
99 num_cpus = stats["cpu_stats"].get("online_cpus", 1)
100 if sys_delta > 0:
101 cpu_pct = f"{(cpu_delta / sys_delta) * num_cpus * 100:.1f}%"
102 mem_used = stats["memory_stats"].get("usage", 0)
103 mem_str = format_bytes(mem_used)
104 except Exception:
105 pass
106
107 print(f"{cid:<14} {name:<30} {image:<25} {status:<15} {cpu_pct:>7} {mem_str:>10}")
108
109 if __name__ == "__main__":
110 main()
■ Line-by-Line Explanation
3 CPU % calculation
Docker provides raw nanosecond CPU counters. The delta between two samples divided by the system delta gives
the CPU percentage. This mirrors how docker stats computes it.
5 [Link](10)
Prevents the script from hanging indefinitely if the Docker daemon is slow or unresponsive.
INTERMEDIA
DAY 132
TE
Dockerfile Linter
■ Source Code
1 import sys
2 import re
3 from dataclasses import dataclass, field
4 from typing import List, Optional
5
6 @dataclass
7 class LintResult:
8 line_num: int
9 severity: str # ERROR, WARN, INFO
10 rule_id: str
11 message: str
12 line_text: str
13
14 class DockerfileLinter:
15 """Static analyzer for Dockerfiles."""
16
17 # Rules: list of (regex_pattern, severity, rule_id, message_template)
18 RULES = [
19 # Security rules
20 (r"FROM\s+.*:latest", "WARN", "DL3007",
21 "Using 'latest' tag. Pin image to a specific version for reproducibility."),
22 (r"FROM\s+ubuntu(?::.*)?$|FROM\s+debian(?::.*)?$", "INFO", "DL3006",
23 "Consider using a minimal base image (alpine, distroless) to reduce attack surface."),
24 (r"RUN\s+.*sudo\s", "WARN", "DL3004",
25 "Do not use sudo in RUN; the container runs as the given USER already."),
26 (r"RUN\s+.*apt-get\s+install(?!.*-y)", "ERROR", "DL3015",
27 "apt-get install without -y flag may hang waiting for user input."),
28 (r"RUN\s+.*apt-get\s+update[^&\n]*$", "WARN", "DL3009",
29 "apt-get update without apt-get install in same RUN creates stale cache."),
30 (r"ADD\s+https?://", "WARN", "DL3020",
31 "Use COPY instead of ADD for local files. Use curl/wget in RUN for URLs."),
32 (r"(PASSWORD|SECRET|API_KEY|TOKEN)\s*=\s*\S+", "ERROR", "DL4006",
33 "Potential hardcoded secret. Use --build-arg or multi-stage builds."),
34 # Best practice rules
35 (r"^RUN\s", "INFO", "DL3059",
36 "Multiple consecutive RUN instructions. Combine to reduce layers."),
37 (r"MAINTAINER\s", "WARN", "DL4000",
38 "MAINTAINER is deprecated. Use LABEL maintainer= instead."),
39 ]
40
91 ))
92 if not has_healthcheck:
93 [Link](LintResult(
94 line_num=0, severity="INFO", rule_id="DL3029",
95 message="No HEALTHCHECK instruction defined.",
96 line_text=""
97 ))
98
99 return sorted(results, key=lambda r: r.line_num)
100
101 def report(self, results: List[LintResult]) -> bool:
102 """Print results and return True if no errors."""
103 icons = {"ERROR": "[E]", "WARN": "[W]", "INFO": "[I]"}
104 errors = 0
105 for r in results:
106 loc = f"Line {r.line_num:>3}" if r.line_num > 0 else " (file)"
107 print(f"{icons[[Link]]} {loc} [{r.rule_id}] {[Link]}")
108 if r.line_text:
109 print(f" > {r.line_text}")
110 if [Link] == "ERROR":
111 errors += 1
112
113 print(f"\nTotal: {len(results)} findings ({errors} errors)")
114 return errors == 0
115
116 if __name__ == "__main__":
117 path = [Link][1] if len([Link]) > 1 else "Dockerfile"
118 linter = DockerfileLinter()
119 results = [Link](path)
120 ok = [Link](results)
121 [Link](0 if ok else 1)
■ Line-by-Line Explanation
1 @dataclass
Automatically generates __init__, __repr__, and __eq__ methods. Cleaner than writing boilerplate classes for data
containers.
2 [Link] vs [Link]
search() finds the pattern anywhere in the string; match() only looks at the start. Use search() for line scanning.
5 [Link](0 if ok else 1)
Zero means success, non-zero means failure. This makes the linter usable in CI/CD pipelines as a quality gate.
Each project in this part is a complete, runnable Python script. Read the description,
2 study the code, then check the line-by-line explanation below it. Type the code
yourself for best retention.
Projects
INTERMEDIA
DAY 181
TE
■ Source Code
1 import boto3 # pip install boto3
2 import os
3 import sys
4 import time
5 from [Link] import ClientError, NoCredentialsError
6
7 class S3Manager:
8 """High-level S3 operations wrapper with progress reporting."""
9
10 def __init__(self, region: str = None):
11 # [Link]() reads credentials from:
12 # 1. Environment: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
13 # 2. ~/.aws/credentials file
14 # 3. IAM role (when running on EC2/ECS/Lambda)
15 try:
16 self.s3 = [Link]("s3", region_name=region)
17 # Test credentials by making a cheap API call
18 self.s3.list_buckets()
19 except NoCredentialsError:
20 print("ERROR: No AWS credentials found.")
21 print("Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or run aws configure")
22 [Link](1)
23
24 def list_buckets(self) -> list:
25 """Return list of all S3 buckets in the account."""
26 response = self.s3.list_buckets()
27 # Response is a dict; 'Buckets' key contains the list
28 return [Link]("Buckets", [])
29
30 def list_objects(self, bucket: str, prefix: str = "") -> list:
31 """List objects in a bucket, handling pagination automatically."""
32 objects = []
33 paginator = self.s3.get_paginator("list_objects_v2")
34 # Paginators automatically handle NextContinuationToken
35 # without them you'd need a while loop checking IsTruncated
36 for page in [Link](Bucket=bucket, Prefix=prefix):
37 [Link]([Link]("Contents", []))
38 return objects
39
40 def upload_file(self, local_path: str, bucket: str, s3_key: str = None):
91 if buckets:
92 bucket_name = buckets[0]["Name"]
93 count, size = manager.get_bucket_size(bucket_name)
94 print(f"\n Bucket '{bucket_name}': {count} objects, {size/1024/1024:.1f} MB")
95
96 if __name__ == "__main__":
97 main()
■ Line-by-Line Explanation
2 Paginators
AWS APIs return a maximum of 1000 items per call. Paginators handle the NextContinuationToken loop
automatically. Always use paginators for list operations.
3 Progress callback
S3 upload_file accepts a Callback parameter called for each chunk. The closure captures the uploaded list
(mutable) to track cumulative progress.
4 ClientError wrapping
All AWS API errors are ClientError exceptions. [Link]['Error']['Code'] gives the specific error code (e.g.,
'AccessDenied', 'NoSuchBucket').
5 Multipart upload
upload_file automatically uses multipart upload for files > 8MB, enabling resumable uploads and parallel part
transfers.
INTERMEDIA
DAY 182
TE
■ Source Code
1 import boto3
2 import sys
3 import time
4 from [Link] import ClientError
5
6 class EC2Manager:
7 def __init__(self, region: str = "us-east-1"):
8 # ec2 resource provides object-oriented access to instances
9 self.ec2 = [Link]("ec2", region_name=region)
10 # ec2 client provides direct API access for operations not on resource
11 [Link] = [Link]("ec2", region_name=region)
12 [Link] = region
13
14 def list_instances(self, filters: list = None) -> list:
15 """List instances with optional filters."""
16 if filters is None:
17 filters = []
18 # [Link]() translates to DescribeInstances API call
19 instances = list([Link](Filters=filters))
20 return instances
21
22 def get_instance_name(self, instance) -> str:
23 """Extract the Name tag from an instance."""
24 if [Link]:
25 for tag in [Link]:
26 if tag["Key"] == "Name":
27 return tag["Value"]
28 return "(no name)"
29
30 def print_instances(self, instances: list):
31 """Print a formatted table of instances."""
32 print(f"\n{'ID':<20} {'Name':<25} {'Type':<14} {'State':<12} {'IP':<16}")
33 print("-" * 90)
34 for inst in instances:
35 name = self.get_instance_name(inst)
36 ip = inst.public_ip_address or inst.private_ip_address or "N/A"
37 print(f"{[Link]:<20} {name:<25} {inst.instance_type:<14} "
38 f"{[Link]['name']:<12} {ip:<16}")
39
40 def start_instance(self, instance_id: str, wait: bool = True):
91 if untagged:
92 print(f"\nWARNING: {len(untagged)} untagged instance(s) found:")
93 for inst in untagged:
94 print(f" {[Link]} ({inst.instance_type}) - {[Link]['name']}")
■ Line-by-Line Explanation
1 [Link] vs [Link]
resource gives object-oriented access ([Link]()), while client is a thin wrapper over raw API calls. Use
resource for simplicity, client when you need full API control.
2 [Link] iteration
Tags in AWS are always a list of Key/Value dicts, never a plain dict. This is because the same key can technically
appear multiple times.
3 Waiters
Built-in polling loops that wait for a resource to reach a desired state. Use them instead of writing your own
sleep/retry loops.
4 [Link]()
After state changes, the instance object's cached attributes may be stale. reload() fetches fresh data from the API.
Each project in this part is a complete, runnable Python script. Read the description,
1 study the code, then check the line-by-line explanation below it. Type the code
yourself for best retention.
Projects
Parse and validate GitHub Actions YAML workflow files for correctness.
■ Source Code
1 import yaml
2 import os
3 import sys
4 from pathlib import Path
5 from typing import List, Dict, Any
6
7 class WorkflowValidator:
8 """Validates GitHub Actions workflow YAML files."""
9
10 VALID_EVENTS = {
11 "push", "pull_request", "workflow_dispatch", "schedule",
12 "release", "issues", "issue_comment", "create", "delete",
13 "deployment", "fork", "gollum", "label", "milestone",
14 "page_build", "project", "public", "registry_package",
15 "repository_dispatch", "status", "watch", "workflow_call",
16 "workflow_run",
17 }
18
19 VALID_PERMISSIONS = {
20 "actions", "checks", "contents", "deployments", "id-token",
21 "issues", "discussions", "packages", "pages", "pull-requests",
22 "repository-projects", "security-events", "statuses",
23 }
24
25 def __init__(self):
26 [Link]: List[str] = []
27 [Link]: List[str] = []
28
29 def _err(self, msg: str): [Link](f"[ERROR] {msg}")
30 def _warn(self, msg: str): [Link](f"[WARN] {msg}")
31
32 def validate(self, filepath: str) -> bool:
33 """Main validation entry point. Returns True if valid."""
34 [Link] = []
35 [Link] = []
36
37 try:
38 with open(filepath) as f:
39 workflow = yaml.safe_load(f)
40 except [Link] as e:
41 self._err(f"YAML parse error: {e}")
42 return False
43
44 if not isinstance(workflow, dict):
45 self._err("Workflow must be a YAML mapping (dict).")
46 return False
47
48 self._validate_name(workflow)
49 self._validate_on(workflow)
50 self._validate_jobs(workflow)
51 self._validate_permissions(workflow)
52
53 return len([Link]) == 0
54
55 def _validate_name(self, wf: dict):
56 if "name" not in wf:
57 self._warn("No 'name' key. Add a descriptive workflow name.")
58
59 def _validate_on(self, wf: dict):
60 """Validate the 'on' trigger section."""
61 on = [Link]("on") or [Link](True) # YAML parses 'on' as True!
62 if on is None:
63 self._err("Missing 'on:' trigger. Workflow will never run.")
64 return
65
66 if isinstance(on, str):
67 on = {on: None}
68 elif isinstance(on, list):
69 on = {event: None for event in on}
70
71 if isinstance(on, dict):
72 for event in [Link]():
73 if str(event) not in self.VALID_EVENTS:
74 self._err(f"Unknown trigger event: '{event}'")
75
76 def _validate_jobs(self, wf: dict):
77 """Validate the jobs section."""
78 jobs = [Link]("jobs")
79 if not jobs:
80 self._err("No 'jobs' defined. Workflow does nothing.")
81 return
82
83 if not isinstance(jobs, dict):
84 self._err("'jobs' must be a mapping.")
85 return
86
87 all_job_ids = set([Link]())
88
89 for job_id, job in [Link]():
90 prefix = f"Job '{job_id}'"
91
■ Line-by-Line Explanation
Each project in this part is a complete, runnable Python script. Read the description,
1 study the code, then check the line-by-line explanation below it. Type the code
yourself for best retention.
Projects
■ Source Code
1 import yaml
2 import sys
3 import os
4 from typing import List, Dict
5
6 class K8sValidator:
7 """Validates Kubernetes YAML manifests for correctness and security."""
8
9 VALID_KINDS = {
10 "Pod", "Deployment", "StatefulSet", "DaemonSet", "Job", "CronJob",
11 "Service", "Ingress", "ConfigMap", "Secret", "ServiceAccount",
12 "PersistentVolumeClaim", "Namespace", "NetworkPolicy", "HorizontalPodAutoscaler",
13 }
14
15 def __init__(self):
16 [Link] = []
17 [Link] = []
18
19 def validate_file(self, filepath: str) -> bool:
20 [Link] = []
21 [Link] = []
22
23 with open(filepath) as f:
24 # yaml.safe_load_all handles multi-document YAML (separated by ---)
25 documents = list(yaml.safe_load_all(f))
26
27 for i, doc in enumerate(documents):
28 if doc is None: continue
29 prefix = f"Document {i+1}"
30 self._validate_document(doc, prefix)
31
32 return len([Link]) == 0
33
34 def _validate_document(self, doc: dict, prefix: str):
35 """Validate a single Kubernetes manifest document."""
36 if not isinstance(doc, dict):
37 [Link](f"{prefix}: must be a YAML mapping.")
38 return
39
40 # Every K8s manifest needs apiVersion, kind, and metadata
41 for required_field in ["apiVersion", "kind", "metadata"]:
92 [Link](
93 f"{prefix}: [Link].{key}={value} doesn't match template labels."
94 )
95
96 # Validate pod template spec
97 pod_spec = [Link]("template", {}).get("spec", {})
98 self._validate_pod_spec(pod_spec, f"{prefix}/template")
99
100 def _validate_pod_spec(self, spec: dict, prefix: str):
101 if not isinstance(spec, dict): return
102
103 containers = [Link]("containers", [])
104 if not containers:
105 [Link](f"{prefix}: [Link] is empty.")
106 return
107
108 for i, container in enumerate(containers):
109 cpfx = f"{prefix}/containers[{i}]"
110
111 # Image tag pinning
112 image = [Link]("image", "")
113 if [Link](":latest") or ":" not in image:
114 [Link](f"{cpfx}: image '{image}' uses latest tag. Pin to specific version.")
115
116 # Resource limits
117 resources = [Link]("resources", {})
118 if not resources:
119 [Link](f"{cpfx}: no resource limits defined. Risk of OOM kills.")
120 else:
121 if "limits" not in resources:
122 [Link](f"{cpfx}: [Link] not set.")
123 if "requests" not in resources:
124 [Link](f"{cpfx}: [Link] not set.")
125
126 # Security context
127 sc = [Link]("securityContext", {})
128 if not sc:
129 [Link](f"{cpfx}: no securityContext defined.")
130 else:
131 if [Link]("privileged") is True:
132 [Link](f"{cpfx}: privileged=true is a critical security risk.")
133 if [Link]("runAsRoot") is True or [Link]("runAsUser") == 0:
134 [Link](f"{cpfx}: running as root. Set runAsNonRoot: true.")
135
136 def _validate_service(self, doc: dict, prefix: str):
137 spec = [Link]("spec", {})
138 svc_type = [Link]("type", "ClusterIP")
139 if svc_type == "NodePort":
140 [Link](f"{prefix}: NodePort exposes service on all nodes. Use LoadBalancer or
Ingress.")
■ Line-by-Line Explanation
1 yaml.safe_load_all()
Handles multi-document YAML files (documents separated by ---). Returns a generator; wrap in list() to iterate
multiple times.
5 stringData warning
Secrets in stringData appear in plaintext in YAML files and git history. Use data (base64) and seal secrets with
tools like Sealed Secrets or Vault.
Each project in this part is a complete, runnable Python script. Read the description,
2 study the code, then check the line-by-line explanation below it. Type the code
yourself for best retention.
Projects
Build a custom Prometheus exporter that exposes system metrics in the /metrics format.
■ Source Code
1 import [Link] # Built-in HTTP server
2 import time
3 import os
4 import threading
5 import socket
6
7 class MetricsRegistry:
8 """Simple Prometheus-compatible metrics registry."""
9
10 def __init__(self):
11 self._gauges = {} # Current value metrics
12 self._counters = {} # Monotonically increasing metrics
13 self._lock = [Link]() # Thread-safe writes
14
15 def set_gauge(self, name: str, value: float, labels: dict = None, help_text: str = ""):
16 """Set a gauge metric (e.g., current CPU usage)."""
17 with self._lock:
18 key = (name, tuple(sorted((labels or {}).items())))
19 self._gauges[key] = (value, labels or {}, help_text)
20
21 def inc_counter(self, name: str, amount: float = 1.0,
22 labels: dict = None, help_text: str = ""):
23 """Increment a counter metric (e.g., total HTTP requests)."""
24 with self._lock:
25 key = (name, tuple(sorted((labels or {}).items())))
26 current = self._counters.get(key, (0.0, labels or {}, help_text))
27 self._counters[key] = (current[0] + amount, labels or {}, help_text)
28
29 def render(self) -> str:
30 """Render all metrics in Prometheus text exposition format."""
31 lines = []
32
33 with self._lock:
34 # Output HELP and TYPE lines, then metric lines
35 seen_names = set()
36 for (name, label_items), (value, labels, help_text) in self._gauges.items():
37 if name not in seen_names:
38 if help_text:
39 [Link](f"# HELP {name} {help_text}")
40 [Link](f"# TYPE {name} gauge")
41 seen_names.add(name)
42 label_str = self._format_labels(labels)
43 [Link](f"{name}{label_str} {value}")
44
45 seen_names = set()
46 for (name, label_items), (value, labels, help_text) in self._counters.items():
47 if name not in seen_names:
48 if help_text:
49 [Link](f"# HELP {name} {help_text}")
50 [Link](f"# TYPE {name} counter")
51 seen_names.add(name)
52 label_str = self._format_labels(labels)
53 [Link](f"{name}_total{label_str} {value}")
54
55 return "\n".join(lines) + "\n"
56
57 @staticmethod
58 def _format_labels(labels: dict) -> str:
59 """Format labels as {key="val",key2="val2"}."""
60 if not labels: return ""
61 parts = [f'{k}="{v}"' for k, v in sorted([Link]())]
62 return "{" + ",".join(parts) + "}"
63
64 class SystemCollector:
65 """Collects system metrics and populates the registry."""
66
67 def __init__(self, registry: MetricsRegistry):
68 [Link] = registry
69 [Link] = [Link]()
70
71 def collect(self):
72 """Collect all system metrics."""
73 self._collect_cpu()
74 self._collect_memory()
75 self._collect_disk()
76 self._collect_load()
77
78 def _collect_cpu(self):
79 try:
80 with open("/proc/stat") as f:
81 fields = [Link]().split()
82 total = sum(int(x) for x in fields[1:])
83 idle = int(fields[4])
84 cpu_pct = (total - idle) / total * 100
85 [Link].set_gauge(
86 "node_cpu_usage_percent", round(cpu_pct, 2),
87 labels={"host": [Link]},
88 help_text="CPU usage percentage"
89 )
90 except Exception: pass
91
92 def _collect_memory(self):
93 try:
94 mem = {}
95 with open("/proc/meminfo") as f:
96 for line in f:
97 k, v = [Link](":", 1)
98 mem[[Link]()] = int([Link]().split()[0]) * 1024
99
100 total = [Link]("MemTotal", 0)
101 available = [Link]("MemAvailable", 0)
102 used = total - available
103
104 [Link].set_gauge("node_memory_total_bytes", total,
105 labels={"host": [Link]}, help_text="Total memory bytes")
106 [Link].set_gauge("node_memory_used_bytes", used,
107 labels={"host": [Link]}, help_text="Used memory bytes")
108 [Link].set_gauge("node_memory_available_bytes", available,
109 labels={"host": [Link]}, help_text="Available memory bytes")
110 except Exception: pass
111
112 def _collect_disk(self):
113 try:
114 st = [Link]("/")
115 total = st.f_blocks * st.f_frsize
116 free = st.f_bavail * st.f_frsize
117 used = total - free
118 [Link].set_gauge("node_disk_total_bytes", total,
119 labels={"host": [Link], "mount": "/"},
120 help_text="Disk total bytes")
121 [Link].set_gauge("node_disk_used_bytes", used,
122 labels={"host": [Link], "mount": "/"},
123 help_text="Disk used bytes")
124 except Exception: pass
125
126 def _collect_load(self):
127 try:
128 load1, load5, load15 = [Link]()
129 [Link].set_gauge("node_load1", load1,
130 labels={"host": [Link]}, help_text="1-minute load average")
131 [Link].set_gauge("node_load5", load5,
132 labels={"host": [Link]}, help_text="5-minute load average")
133 except Exception: pass
134
135 registry = MetricsRegistry()
136 collector = SystemCollector(registry)
137
138 class MetricsHandler([Link]):
139 """HTTP handler that serves Prometheus metrics."""
140
141 def do_GET(self):
■ Line-by-Line Explanation
2 [Link]()
The HTTP server and collector run in the same process. Lock prevents race conditions when the HTTP handler
reads metrics while the collector is writing.
3 Labels in Prometheus
Labels add dimensions to metrics. node_cpu_usage_percent{host='web1'} and
node_cpu_usage_percent{host='web2'} are distinct time series.
4 Collect on scrape
Rather than pushing metrics on a timer, Prometheus pulls metrics by HTTP request. Collecting on demand ensures
freshness without wasted work.
5 Content-Type header
Prometheus requires the specific header text/plain; version=0.0.4 to parse the exposition format correctly. Wrong
Content-Type = parsing failure.
A complete pipeline runner: lint → test → build → deploy → verify with rollback on failure.
■ Source Code
1 import subprocess
2 import sys
3 import time
4 import os
5 import json
6 from dataclasses import dataclass, field
7 from typing import List, Callable, Optional
8 from enum import Enum
9
10 class StageStatus(Enum):
11 PENDING = "PENDING"
12 RUNNING = "RUNNING"
13 PASSED = "PASSED"
14 FAILED = "FAILED"
15 SKIPPED = "SKIPPED"
16
17 @dataclass
18 class Stage:
19 name: str
20 command: List[str] # Command to run as a list (subprocess-safe)
21 depends_on: List[str] = field(default_factory=list)
22 on_failure: str = "halt" # "halt" | "continue" | "rollback"
23 timeout: int = 300 # Seconds before kill
24 env: dict = field(default_factory=dict) # Extra env vars for this stage
25 status: StageStatus = [Link]
26 duration: float = 0.0
27 output: str = ""
28
29 class Pipeline:
30 """DAG-based pipeline runner with dependency resolution."""
31
32 def __init__(self, name: str):
33 [Link] = name
34 [Link]: List[Stage] = []
35 self.rollback_stages: List[Stage] = []
36 self.start_time = None
37
38 def add_stage(self, stage: Stage) -> "Pipeline":
39 """Add a stage (fluent builder pattern)."""
40 [Link](stage)
41 return self
42
43 def add_rollback(self, stage: Stage) -> "Pipeline":
44 """Add a rollback stage (run on failure)."""
45 self.rollback_stages.append(stage)
46 return self
47
48 def _resolve_order(self) -> List[Stage]:
49 """Topological sort of stages based on depends_on."""
50 completed = set()
51 ordered = []
52 remaining = list([Link])
53 max_iterations = len(remaining) ** 2 # Detect cycles
54
55 iteration = 0
56 while remaining:
57 iteration += 1
58 if iteration > max_iterations:
59 raise RuntimeError("Circular dependency detected in pipeline stages.")
60
61 for stage in remaining[:]:
62 # A stage is ready if all its dependencies are completed
63 if all(dep in completed for dep in stage.depends_on):
64 [Link](stage)
65 [Link]([Link])
66 [Link](stage)
67
68 return ordered
69
70 def _run_stage(self, stage: Stage) -> bool:
71 """Execute a single stage. Returns True on success."""
72 [Link] = [Link]
73 print(f" ■■ [{[Link]}] Starting...", flush=True)
74
75 start = [Link]()
76
77 # Merge base environment with stage-specific vars
78 env = {**[Link], **[Link]}
79
80 try:
81 result = [Link](
82 [Link],
83 capture_output=True, # Capture stdout and stderr
84 text=True, # Decode bytes to str automatically
85 timeout=[Link],
86 env=env,
87 )
88 [Link] = [Link]() - start
89 [Link] = [Link] + [Link]
90
91 if [Link] == 0:
92 [Link] = [Link]
93 print(f" ■ [{[Link]}] PASSED ({[Link]:.1f}s)")
94 return True
95 else:
96 [Link] = [Link]
97 print(f" ■ [{[Link]}] FAILED (exit code {[Link]}, {[Link]:.1f}s)")
98 if [Link]():
99 # Print last 5 lines of output for context
100 last_lines = "\n".join([Link]().splitlines()[-5:])
101 print(f" ■ Output (last 5 lines):\n{last_lines}")
102 return False
103
104 except [Link]:
105 [Link] = [Link]
106 [Link] = [Link]
107 print(f" ■ [{[Link]}] TIMEOUT after {[Link]}s")
108 return False
109
110 except FileNotFoundError:
111 [Link] = [Link]
112 print(f" ■ [{[Link]}] FAILED: command not found: {[Link][0]}")
113 return False
114
115 def _run_rollback(self):
116 """Execute rollback stages in reverse order."""
117 if not self.rollback_stages:
118 print(" [No rollback stages defined]")
119 return
120 print("\n === EXECUTING ROLLBACK ===")
121 for stage in reversed(self.rollback_stages):
122 self._run_stage(stage)
123
124 def run(self) -> bool:
125 """Execute the full pipeline. Returns True if all stages pass."""
126 self.start_time = [Link]()
127 print(f"\n■■■■ Pipeline: {[Link]} ■■■")
128 print(f"■ Started: {[Link]('%Y-%m-%d %H:%M:%S')}")
129 print(f"■{'■'*50}")
130
131 try:
132 ordered = self._resolve_order()
133 except RuntimeError as e:
134 print(f"■ ERROR: {e}")
135 return False
136
137 pipeline_success = True
138
139 for stage in ordered:
140 # Skip stages whose dependencies failed
141 if any(
192
193 pipeline.add_stage(Stage("lint", ["python3", "-m", "py_compile", "[Link]"],
194 on_failure="halt"))
195 pipeline.add_stage(Stage("test", ["python3", "-m", "pytest", "tests/", "-q"],
196 depends_on=["lint"], on_failure="halt"))
197 pipeline.add_stage(Stage("build", ["docker", "build", "-t", "my-app:latest", "."],
198 depends_on=["test"], on_failure="halt"))
199 pipeline.add_stage(Stage("deploy", ["kubectl", "apply", "-f", "k8s/"],
200 depends_on=["build"], on_failure="rollback", timeout=120))
201 pipeline.add_stage(Stage("verify", ["curl", "-f", "[Link]
202 depends_on=["deploy"], on_failure="rollback"))
203
204 # Rollback: remove the deployment if verify fails
205 pipeline.add_rollback(Stage("rollback-deploy",
206 ["kubectl", "rollout", "undo", "deployment/my-app"]))
207
208 ok = [Link]()
209 [Link](0 if ok else 1)
■ Line-by-Line Explanation
2 [Link](capture_output=True, text=True)
capture_output=True is shorthand for stdout=PIPE, stderr=PIPE. text=True auto-decodes bytes to str. Always
prefer this over [Link]().
3 [Link]
If a stage exceeds its timeout, the process is killed and this exception is raised. Essential for CI/CD to prevent stuck
pipelines.
6 on_failure strategies
'halt' stops immediately (fail-fast), 'continue' runs all stages regardless, 'rollback' triggers the undo sequence.
These mirror real CD pipeline semantics.
6 Build a reusable CLI parser using argparse — the foundation of every DevOps script.
7 Grep-like tool: search files for patterns using [Link]() with context lines.
8 Read raw data CSVs and produce formatted summary reports with totals and averages.
5 Read and write .ini configuration files using the configparser module.
6 Serve files from a directory using [Link] with custom MIME types.
7 Scan a host for open ports using socket with threading for speed.
1 Collect and display comprehensive system info: CPU, RAM, disk, network.
6 Parse ~/.ssh/config files and extract host aliases, keys, and options.
9 Rotate, compress, and archive log files when they exceed a size threshold.
0 Read, add, and remove cron jobs from crontab using subprocess.
6 Compare two YAML/JSON configs and report added, removed, changed keys.
7 Scan source code files for accidentally committed secrets using regex patterns.
2 Interactive SQLite query runner with output formatting and CSV export.
4 Verify that all source files have the required license header.
3 Simple job queue using Redis lists for worker-based task processing.
0 Check gRPC service health using the standard health checking protocol.
1 Build a minimal API gateway with routing, auth, and rate limiting.
4 Stream CloudWatch log groups in real time using the Boto3 API.
8 Track AWS service quota usage and alert before limits are hit.
4 Score deployment risk based on code change size and test coverage.
6 Calculate error budgets from SLI metrics and alert on burn rate.
[Link]/akashfrancis3211 | [Link]/in/akash-francis91