Automation Relevant Coding Problems Questions
Practice problems involving: parsing and transforming log data, working with strings and regular expressions, counting and aggregating data, basic system operations like file handling and simple data processing, sorting and searching through configuration data. These often appear in SRE interviews to test automation-relevant thinking and real-world problem application.
HardTechnical
57 practiced
Design and implement a deduplicating backup manifest generator: given a filesystem snapshot, produce a manifest that groups identical file contents by hash and outputs an efficient restore plan that avoids re-transmitting duplicate content. Requirements: support incremental snapshots, reasonably efficient scanning, and provide core code to compute file hashes and group duplicates.
Sample Answer
Approach (brief): walk a snapshot, compute content hashes (SHA-256) in streaming chunks, reuse previous snapshot metadata (size+mtime->hash) to avoid rehashing unchanged files, group identical contents by hash, and emit a manifest with unique blobs + per-path mapping (restore plan points files to blob ids). Use parallel workers for I/O-bound hashing.Code (core hashing + grouping, Python):Key points:- Incremental optimization: reuse previous hash if size+mtime match (fast-path). For stronger guarantees use inode/ETag or user-provided fingerprint.- Deduplication: manifest groups by hash; restore plan sends unique blobs once and maps file paths to blob IDs.- Efficiency: stream reads in CHUNK, parallelize IO-bound hashing, use os.stat to avoid hashing unchanged files.Time/space:- Time: O(total_bytes_read) for files needing hashing; worst-case O(N) file metadata walking cost. Parallelism improves wall-clock.- Space: O(number_of_files) metadata in manifest.Edge cases:- Hard links/symlinks: record link targets or inode to avoid double-transmit.- Partial reads/IO errors: skip/flag for retry.- Hash collisions: SHA-256 practically negligible; for extra safety verify sizes and optionally byte-by-byte compare on collision.Alternatives/trade-offs:- Content-defined chunking (variable-size chunks) for block-level dedupe (better for large file edits) but more complex.- Use file system snapshot APIs (ZFS/Btrfs) to list changed files cheaply when available.
python
import os, hashlib
from concurrent.futures import ThreadPoolExecutor, as_completed
CHUNK = 4 * 1024 * 1024
def hash_file(path):
"""Stream file and return hex sha256 and size."""
h = hashlib.sha256()
size = 0
with open(path, "rb") as f:
while True:
b = f.read(CHUNK)
if not b:
break
size += len(b)
h.update(b)
return h.hexdigest(), size
def generate_manifest(root, prev_meta=None, workers=8):
"""
prev_meta: dict path -> {hash, size, mtime} from prior manifest
Returns: manifest { "blobs": {hash: {"size": size}}, "files": {path: hash} }
"""
prev_meta = prev_meta or {}
files = []
for dirpath, _, filenames in os.walk(root):
for fn in filenames:
p = os.path.join(dirpath, fn)
try:
st = os.stat(p)
except OSError:
continue
files.append((p, st.st_size, int(st.st_mtime)))
manifest_files = {}
blobs = {}
with ThreadPoolExecutor(max_workers=workers) as ex:
futs = {}
for p, size, mtime in files:
prev = prev_meta.get(os.path.relpath(p, root))
if prev and prev["size"] == size and prev["mtime"] == mtime:
# assume unchanged; reuse hash
manifest_files[os.path.relpath(p, root)] = prev["hash"]
blobs.setdefault(prev["hash"], {"size": size})
else:
futs[ex.submit(hash_file, p)] = (p, size)
for fut in as_completed(futs):
p, size = futs[fut]
try:
h, real_size = fut.result()
except Exception:
continue
rel = os.path.relpath(p, root)
manifest_files[rel] = h
blobs.setdefault(h, {"size": real_size})
return {"blobs": blobs, "files": manifest_files}MediumTechnical
47 practiced
Write a program to compute a minimal human-readable diff between two text configuration files: given old.conf and new.conf, output added/removed/changed lines in a concise summary rather than a full unified diff. Requirements: good performance on large files and intelligible summaries for operators.
Sample Answer
Approach:- Treat files as sequences of meaningful lines (strip whitespace/comments optionally).- Use a hashed index (line -> list of positions) to detect exact additions/removals quickly.- For changed lines (same key but different value in config formats like key=val), parse simple "key/value" pairs and show semantic diffs.- Produce a concise summary: counts, top N added/removed, and per-key changes for parsed configs.Code (Python):Key points:- Uses Counters for O(n) passes and low memory overhead relative to file size.- Semantic parsing for key=value lets operators see meaningful config changes instead of line noise.Time/space:- Time O(n + m) for n/m lines. Space O(unique lines).Edge cases:- Multi-line values, JSON/YAML configs — extend parser accordingly.- Ordered changes (moved blocks) show as add+remove; detecting moves needs LCS (costly).Alternatives:- Use difflib.SequenceMatcher for context-aware diffs (more expensive) or specialized parsers (YAML/INI) for richer semantics.
python
#!/usr/bin/env python3
import sys
from collections import Counter, defaultdict
def canonicalize(lines, strip_comments=True):
out = []
for l in lines:
s = l.rstrip("\n")
if strip_comments:
s = s.split("#",1)[0].strip()
if s: out.append(s)
return out
def parse_kv(lines):
kv = {}
for l in lines:
if "=" in l:
k,v = l.split("=",1)
kv[k.strip()] = v.strip()
return kv
def summarize(old_path, new_path, top=10):
with open(old_path) as f: old = canonicalize(f)
with open(new_path) as f: new = canonicalize(f)
old_set, new_set = Counter(old), Counter(new)
added = list((new_set - old_set).elements())
removed = list((old_set - new_set).elements())
summary = []
summary.append(f"Lines added: {len(added)}, removed: {len(removed)}")
def top_list(lst):
c = Counter(lst).most_common(top)
return [f"{n} x {line}" for line,n in c]
if added:
summary.append("Top additions:")
summary += top_list(added)
if removed:
summary.append("Top removals:")
summary += top_list(removed)
# Try semantic kv diff
old_kv, new_kv = parse_kv(old), parse_kv(new)
changed_keys = []
for k in set(old_kv)|set(new_kv):
a, b = old_kv.get(k), new_kv.get(k)
if a is None:
changed_keys.append(f"+ {k}={b}")
elif b is None:
changed_keys.append(f"- {k}={a}")
elif a != b:
changed_keys.append(f"~ {k}: {a} -> {b}")
if changed_keys:
summary.append("Key-level changes:")
summary += changed_keys[:top]
return "\n".join(summary)
if __name__ == "__main__":
if len(sys.argv) < 3:
print("usage: diffsum.py old.conf new.conf")
sys.exit(2)
print(summarize(sys.argv[1], sys.argv[2]))MediumTechnical
54 practiced
Implement an alert deduplicator: given a stream of alert events (each with a fingerprint key, timestamp, and payload), collapse alerts with the same fingerprint that arrive within a dedupe window (e.g., 5 minutes) into a single aggregated alert containing count, first_timestamp, last_timestamp, and a representative payload. Requirements: streamable, memory-bounded, and able to expire old fingerprints.
Sample Answer
Approach:Keep an in-memory map of active aggregates keyed by fingerprint and a min-heap to expire old fingerprints. For each incoming event, update or create the aggregate (count, first_ts, last_ts, representative payload). An aggregate is emitted when the dedupe window has passed since last event (via expiration). This is streamable (processes events one-by-one), memory-bounded (evicts by expiry), and supports expiration via the heap.Key points:- Use dict for O(1) updates and a min-heap to expire by earliest expiry.- Representative payload policy is adjustable (first/last/merge).- Streamable: process events one at a time and yield aggregates as they expire.- Memory-bounded: fingerprints are removed after dedupe window; heap and dict size limited by number of unique active fingerprints within window.Complexity:- Per event: average O(log n) due to heap push; dict ops O(1). n = active fingerprints.- Space: O(n) where n is active fingerprints within dedupe window.Edge cases:- Out-of-order timestamps: current implementation uses event timestamp for update/expiry; if heavy skew expected, normalize or use event ingestion time.- Duplicate events with identical timestamps: count handles duplicates.- Clock skew between producers: prefer ingestion-time or vector clocks if necessary.Alternatives:- Use capped LRU cache with TTL (e.g., cachetools TTLCache) instead of explicit heap.- Use windowed stream processing frameworks (Kafka Streams, Flink) for distributed scaling and stronger guarantees.
python
import heapq
import time
from typing import Dict, Any, Tuple, Optional, Iterator
DEDUPE_WINDOW = 300 # seconds (5 minutes)
class Aggregate:
def __init__(self, fingerprint: str, ts: float, payload: Any):
self.fingerprint = fingerprint
self.count = 1
self.first_ts = ts
self.last_ts = ts
self.payload = payload # representative; choose e.g., latest
self.expiry = ts + DEDUPE_WINDOW
def update(self, ts: float, payload: Any):
self.count += 1
self.last_ts = max(self.last_ts, ts)
self.payload = payload # choose representative policy
self.expiry = ts + DEDUPE_WINDOW
def process_stream(events: Iterator[Dict[str, Any]], now_func: Optional[callable]=None) -> Iterator[Dict[str, Any]]:
"""
events: iterator of dicts with keys: fingerprint, timestamp, payload
yields completed aggregates as dicts when they expire
"""
if now_func is None:
now_func = time.time
active: Dict[str, Aggregate] = {}
heap: list[Tuple[float, str]] = [] # (expiry_ts, fingerprint)
def expire_up_to(now: float):
# pop expired aggregates and yield them
while heap and heap[0][0] <= now:
expiry, fp = heapq.heappop(heap)
agg = active.get(fp)
# heap may have stale entries; ensure expiry matches current
if agg is None or agg.expiry != expiry:
continue
# emit aggregate and remove from active
yield {
"fingerprint": agg.fingerprint,
"count": agg.count,
"first_ts": agg.first_ts,
"last_ts": agg.last_ts,
"payload": agg.payload
}
del active[fp]
for ev in events:
now = now_func()
# first, expire any aggregates due before processing this event
for out in expire_up_to(now):
yield out
fp = ev["fingerprint"]
ts = ev["timestamp"]
payload = ev.get("payload")
agg = active.get(fp)
if agg is None:
agg = Aggregate(fp, ts, payload)
active[fp] = agg
heapq.heappush(heap, (agg.expiry, fp))
else:
# if event arrives after expiry (rare if expirations not yet processed),
# we treat it as new aggregate
if ts > agg.expiry:
# emit existing, start new
yield {
"fingerprint": agg.fingerprint,
"count": agg.count,
"first_ts": agg.first_ts,
"last_ts": agg.last_ts,
"payload": agg.payload
}
agg = Aggregate(fp, ts, payload)
active[fp] = agg
heapq.heappush(heap, (agg.expiry, fp))
else:
agg.update(ts, payload)
heapq.heappush(heap, (agg.expiry, fp))
# when stream ends, drain all remaining aggregates immediately
for out in expire_up_to(float('inf')):
yield out
# any remaining (if heap empty logic) - emit rest
for fp, agg in list(active.items()):
yield {
"fingerprint": agg.fingerprint,
"count": agg.count,
"first_ts": agg.first_ts,
"last_ts": agg.last_ts,
"payload": agg.payload
}EasyTechnical
61 practiced
Write a function in Go that extracts all IPv4 addresses from a given log line and returns a deduplicated list of addresses in the order they first appear. Requirements: use a compiled regex, avoid converting every match into an IP object unless necessary, and return results as strings. Show example input and expected output.
Sample Answer
Approach: compile a regex for IPv4-like dotted quads, iterate matches, validate each quad numerically only when needed (avoid converting every match to net.IP). Use a map to deduplicate while preserving first-seen order.Key points:- Compiled regex (ipv4Re) for performance.- Deduplication via map preserves first-seen order by only appending unseen matches.- Avoids creating net.IP objects; uses simple string checks and light numeric validation.Time: O(n) over line length; Space: O(k) for k unique IPs.Example input:"Accepted connection from 192.168.1.10, rejected 10.0.0.256, retry from 192.168.1.10 and 8.8.8.8"Expected output:[ "192.168.1.10", "8.8.8.8" ]
go
package main
import (
"fmt"
"regexp"
"strconv"
)
var ipv4Re = regexp.MustCompile(`\b(?:25[0-5]|2[0-4]\d|1?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|1?\d{1,2})){3}\b`)
// ExtractIPv4 extracts deduplicated IPv4 addresses in the order they appear.
func ExtractIPv4(line string) []string {
seen := make(map[string]struct{})
result := make([]string, 0)
for _, m := range ipv4Re.FindAllString(line, -1) {
if _, ok := seen[m]; ok {
continue
}
// Lightweight numeric validation: ensure each octet <= 255 (regex already enforces this,
// but this demonstrates avoiding heavy conversions; can be skipped).
valid := true
for _, part := range regexp.MustCompile(`\.`).Split(m, -1) {
if n, err := strconv.Atoi(part); err != nil || n < 0 || n > 255 {
valid = false
break
}
}
if !valid {
continue
}
seen[m] = struct{}{}
result = append(result, m)
}
return result
}
func main() {
line := "Accepted connection from 192.168.1.10, rejected 10.0.0.256, retry from 192.168.1.10 and 8.8.8.8"
fmt.Println(ExtractIPv4(line))
}EasyTechnical
60 practiced
Implement a validator for an /etc/hosts-style file that detects duplicate hostnames mapped to different IP addresses and duplicate IPs mapped to multiple hostnames. Input lines are 'IP hostname [aliases]'. Requirements: ignore comments and whitespace, report filename and line numbers for conflicts, and support IPv4 and IPv6 entries.
Sample Answer
Approach:- Parse file line-by-line, ignore blank lines and comments (lines starting with '#').- For each valid line, split first token as IP, rest as hostname + aliases.- Validate IP using Python's ipaddress module (supports IPv4 & IPv6).- Track mappings: - hostname_to_ips: map hostname -> set of (ip, line) - ip_to_hostnames: map ip -> set of (hostname, line)- Report conflicts when a hostname maps to more than one IP or an IP maps to multiple hostnames, including filename and line numbers.Key points:- Uses ipaddress for correct IPv4/IPv6 validation and normalization.- Reports include filename and line numbers for each conflicting mapping.- Time complexity O(n*m) where n = lines and m = average names per line (effectively linear overall). Space O(k) for stored mappings.Edge cases:- Duplicate identical mappings are allowed (same hostname->same IP); not reported.- Lines with only IP or malformed tokens are reported.Alternative:- Normalize hostnames (lowercase) if desired, or ignore local-only patterns. Add option to treat aliases differently.
python
import ipaddress
from collections import defaultdict
def validate_hosts_file(path):
hostname_to_ips = defaultdict(list) # hostname -> list of (ip, line)
ip_to_hostnames = defaultdict(list) # ip -> list of (hostname, line)
reports = []
with open(path, 'r', encoding='utf-8') as f:
for lineno, raw in enumerate(f, start=1):
line = raw.split('#', 1)[0].strip() # remove comments and trim
if not line:
continue
parts = line.split()
if len(parts) < 2:
reports.append(f"{path}:{lineno}: malformed line (needs IP and hostname)")
continue
ip_str, *names = parts
try:
ip = str(ipaddress.ip_address(ip_str))
except ValueError:
reports.append(f"{path}:{lineno}: invalid IP '{ip_str}'")
continue
for name in names:
hostname_to_ips[name].append((ip, lineno))
ip_to_hostnames[ip].append((name, lineno))
# Detect hostname -> multiple IPs
for host, entries in hostname_to_ips.items():
ips = {ip for ip, _ in entries}
if len(ips) > 1:
lines = ", ".join(f"{ip} (line {ln})" for ip, ln in entries)
reports.append(f"{path}: hostname '{host}' mapped to multiple IPs: {lines}")
# Detect ip -> multiple hostnames
for ip, entries in ip_to_hostnames.items():
hosts = {h for h, _ in entries}
if len(hosts) > 1:
lines = ", ".join(f"'{h}' (line {ln})" for h, ln in entries)
reports.append(f"{path}: IP '{ip}' mapped to multiple hostnames: {lines}")
return reports
# Example usage:
# for r in validate_hosts_file('/etc/hosts'):
# print(r)Unlock Full Question Bank
Get access to hundreds of Automation Relevant Coding Problems interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.