From 35e6b139fc86cbec4334580bcd707905e025e067 Mon Sep 17 00:00:00 2001 From: Mateusz Suski Date: Mon, 4 May 2026 17:37:24 +0000 Subject: [PATCH] Initial CV-aligned infrastructure portfolio Rework portfolio around Linux operations, Zabbix monitoring, migration validation, and ELK/Grafana log observability. Add AAP-style LVM resize workflow, Zabbix server/proxy/agent automation assets, Linux/AIX monitoring templates, and updated validation CI. --- .gitea/workflows/ci.yml | 47 ++ .gitignore | 24 + AI_CONTEXT.md | 61 +++ CHANGELOG.md | 39 ++ Mateusz Suski - Linux Engineer.pdf | Bin 0 -> 275955 bytes README.md | 31 ++ docs/architecture.md | 64 +++ docs/runbooks.md | 92 ++++ .../linux-operations-automation/.ansible-lint | 14 + .../linux-operations-automation/Makefile | 95 ++++ .../linux-operations-automation/README.md | 92 ++++ .../VAULT_GUIDE.md | 43 ++ .../linux-operations-automation/ansible.cfg | 5 + .../docker-compose.yml | 28 + .../docs/aap_lvm_resize_workflow.md | 45 ++ .../docs/architecture.md | 30 ++ .../examples/failure-simulation.txt | 8 + .../examples/lvm-resize-output.txt | 19 + .../examples/patch-output.txt | 33 ++ .../group_vars/all.yml | 20 + .../group_vars/databases.yml | 9 + .../group_vars/loadbalancers.yml | 10 + .../group_vars/monitoring.yml | 10 + .../group_vars/vault.example.yml | 8 + .../group_vars/webservers.yml | 11 + .../inventory/hosts.ini | 35 ++ .../molecule/default/converge.yml | 24 + .../molecule/default/destroy.yml | 15 + .../molecule/default/molecule.yml | 31 ++ .../molecule/default/verify.yml | 32 ++ .../playbooks/decommission.yml | 34 ++ .../playbooks/hardening.yml | 124 +++++ .../playbooks/lvm_resize.yml | 149 ++++++ .../playbooks/patch.yml | 31 ++ .../playbooks/provision.yml | 33 ++ .../roles/base_provision/README.md | 48 ++ .../roles/base_provision/defaults/main.yml | 44 ++ .../roles/base_provision/handlers/main.yml | 11 + .../roles/base_provision/tasks/main.yml | 138 +++++ .../base_provision/templates/jail.local.j2 | 14 + .../roles/decommission/README.md | 62 +++ .../roles/decommission/defaults/main.yml | 34 ++ .../roles/decommission/tasks/main.yml | 177 +++++++ .../templates/decommission_report.j2 | 13 + .../roles/hardening/README.md | 58 ++ .../roles/hardening/defaults/main.yml | 35 ++ .../roles/hardening/handlers/main.yml | 5 + .../roles/hardening/tasks/cis_hardening.yml | 7 + .../roles/hardening/tasks/main.yml | 95 ++++ .../roles/patching/README.md | 45 ++ .../roles/patching/defaults/main.yml | 20 + .../roles/patching/handlers/main.yml | 6 + .../roles/patching/tasks/main.yml | 105 ++++ .../roles/patching/templates/patch_report.j2 | 10 + .../scenarios/failure_patch.md | 21 + .../scenarios/scaling_event.yml | 116 ++++ .../scripts/simulate_failure.sh | 388 ++++++++++++++ .../scripts/simulate_scaling.sh | 229 ++++++++ .../log-observability-elk-grafana/Makefile | 20 + .../log-observability-elk-grafana/README.md | 98 ++++ .../alerting/alert_rules.yml | 326 ++++++++++++ .../docker-compose.yml | 120 +++++ .../docs/architecture.md | 30 ++ .../elasticsearch/config/elasticsearch.yml | 4 + .../examples/alert-output.txt | 4 + .../examples/sample-log.txt | 5 + .../filebeat/config/filebeat.yml | 11 + .../grafana/dashboards/README.md | 3 + .../provisioning/datasources/datasources.yml | 14 + .../kibana/config/kibana.yml | 4 + .../logstash/config/logstash.yml | 2 + .../logstash/pipeline/logstash.conf | 24 + .../scenarios/incident_simulation.md | 21 + .../scenarios/incident_simulation.sh | 318 +++++++++++ .../migration-validation-framework/Makefile | 11 + .../migration-validation-framework/README.md | 87 +++ .../migration-validation-framework/cli.py | 323 +++++++++++ .../collectors/disk_usage.py | 206 +++++++ .../collectors/mounts.py | 172 ++++++ .../collectors/services.py | 223 ++++++++ .../docs/architecture.md | 30 ++ .../examples/after.json | 40 ++ .../examples/before.json | 39 ++ .../examples/diff.json | 211 ++++++++ .../scenarios/before_after_comparison.md | 19 + .../tests/test_framework.py | 67 +++ .../validators/compare.py | 501 ++++++++++++++++++ .../.ansible-lint | 8 + .../Makefile | 19 + .../README.md | 63 +++ .../ansible.cfg | 5 + .../docs/incident-response-runbook.md | 30 ++ .../docs/maintenance-runbook.md | 29 + .../docs/proxy-design.md | 27 + .../examples/alert-evidence.txt | 4 + .../inventory/hosts.ini | 12 + .../playbooks/zabbix_agent.yml | 8 + .../playbooks/zabbix_proxy.yml | 8 + .../playbooks/zabbix_server.yml | 8 + .../roles/zabbix_agent/defaults/main.yml | 7 + .../roles/zabbix_agent/tasks/main.yml | 38 ++ .../zabbix_agent/templates/os_checks.conf.j2 | 4 + .../templates/zabbix_agentd.conf.j2 | 5 + .../roles/zabbix_proxy/defaults/main.yml | 7 + .../roles/zabbix_proxy/tasks/main.yml | 31 ++ .../templates/zabbix_proxy.conf.j2 | 6 + .../roles/zabbix_server/defaults/main.yml | 7 + .../roles/zabbix_server/tasks/main.yml | 25 + .../templates/zabbix_server.conf.j2 | 5 + .../samples/aix-os-checks.json | 13 + .../samples/linux-os-checks.json | 12 + .../scripts/validate_assets.py | 49 ++ .../templates/aix-os-template.json | 16 + .../templates/linux-os-template.json | 16 + 114 files changed, 6422 insertions(+) create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitignore create mode 100644 AI_CONTEXT.md create mode 100644 CHANGELOG.md create mode 100644 Mateusz Suski - Linux Engineer.pdf create mode 100644 README.md create mode 100644 docs/architecture.md create mode 100644 docs/runbooks.md create mode 100644 professional-infra/linux-operations-automation/.ansible-lint create mode 100644 professional-infra/linux-operations-automation/Makefile create mode 100644 professional-infra/linux-operations-automation/README.md create mode 100644 professional-infra/linux-operations-automation/VAULT_GUIDE.md create mode 100644 professional-infra/linux-operations-automation/ansible.cfg create mode 100644 professional-infra/linux-operations-automation/docker-compose.yml create mode 100644 professional-infra/linux-operations-automation/docs/aap_lvm_resize_workflow.md create mode 100644 professional-infra/linux-operations-automation/docs/architecture.md create mode 100644 professional-infra/linux-operations-automation/examples/failure-simulation.txt create mode 100644 professional-infra/linux-operations-automation/examples/lvm-resize-output.txt create mode 100644 professional-infra/linux-operations-automation/examples/patch-output.txt create mode 100644 professional-infra/linux-operations-automation/group_vars/all.yml create mode 100644 professional-infra/linux-operations-automation/group_vars/databases.yml create mode 100644 professional-infra/linux-operations-automation/group_vars/loadbalancers.yml create mode 100644 professional-infra/linux-operations-automation/group_vars/monitoring.yml create mode 100644 professional-infra/linux-operations-automation/group_vars/vault.example.yml create mode 100644 professional-infra/linux-operations-automation/group_vars/webservers.yml create mode 100644 professional-infra/linux-operations-automation/inventory/hosts.ini create mode 100644 professional-infra/linux-operations-automation/molecule/default/converge.yml create mode 100644 professional-infra/linux-operations-automation/molecule/default/destroy.yml create mode 100644 professional-infra/linux-operations-automation/molecule/default/molecule.yml create mode 100644 professional-infra/linux-operations-automation/molecule/default/verify.yml create mode 100644 professional-infra/linux-operations-automation/playbooks/decommission.yml create mode 100644 professional-infra/linux-operations-automation/playbooks/hardening.yml create mode 100644 professional-infra/linux-operations-automation/playbooks/lvm_resize.yml create mode 100644 professional-infra/linux-operations-automation/playbooks/patch.yml create mode 100644 professional-infra/linux-operations-automation/playbooks/provision.yml create mode 100644 professional-infra/linux-operations-automation/roles/base_provision/README.md create mode 100644 professional-infra/linux-operations-automation/roles/base_provision/defaults/main.yml create mode 100644 professional-infra/linux-operations-automation/roles/base_provision/handlers/main.yml create mode 100644 professional-infra/linux-operations-automation/roles/base_provision/tasks/main.yml create mode 100644 professional-infra/linux-operations-automation/roles/base_provision/templates/jail.local.j2 create mode 100644 professional-infra/linux-operations-automation/roles/decommission/README.md create mode 100644 professional-infra/linux-operations-automation/roles/decommission/defaults/main.yml create mode 100644 professional-infra/linux-operations-automation/roles/decommission/tasks/main.yml create mode 100644 professional-infra/linux-operations-automation/roles/decommission/templates/decommission_report.j2 create mode 100644 professional-infra/linux-operations-automation/roles/hardening/README.md create mode 100644 professional-infra/linux-operations-automation/roles/hardening/defaults/main.yml create mode 100644 professional-infra/linux-operations-automation/roles/hardening/handlers/main.yml create mode 100644 professional-infra/linux-operations-automation/roles/hardening/tasks/cis_hardening.yml create mode 100644 professional-infra/linux-operations-automation/roles/hardening/tasks/main.yml create mode 100644 professional-infra/linux-operations-automation/roles/patching/README.md create mode 100644 professional-infra/linux-operations-automation/roles/patching/defaults/main.yml create mode 100644 professional-infra/linux-operations-automation/roles/patching/handlers/main.yml create mode 100644 professional-infra/linux-operations-automation/roles/patching/tasks/main.yml create mode 100644 professional-infra/linux-operations-automation/roles/patching/templates/patch_report.j2 create mode 100644 professional-infra/linux-operations-automation/scenarios/failure_patch.md create mode 100644 professional-infra/linux-operations-automation/scenarios/scaling_event.yml create mode 100644 professional-infra/linux-operations-automation/scripts/simulate_failure.sh create mode 100644 professional-infra/linux-operations-automation/scripts/simulate_scaling.sh create mode 100644 professional-infra/log-observability-elk-grafana/Makefile create mode 100644 professional-infra/log-observability-elk-grafana/README.md create mode 100644 professional-infra/log-observability-elk-grafana/alerting/alert_rules.yml create mode 100644 professional-infra/log-observability-elk-grafana/docker-compose.yml create mode 100644 professional-infra/log-observability-elk-grafana/docs/architecture.md create mode 100644 professional-infra/log-observability-elk-grafana/elasticsearch/config/elasticsearch.yml create mode 100644 professional-infra/log-observability-elk-grafana/examples/alert-output.txt create mode 100644 professional-infra/log-observability-elk-grafana/examples/sample-log.txt create mode 100644 professional-infra/log-observability-elk-grafana/filebeat/config/filebeat.yml create mode 100644 professional-infra/log-observability-elk-grafana/grafana/dashboards/README.md create mode 100644 professional-infra/log-observability-elk-grafana/grafana/provisioning/datasources/datasources.yml create mode 100644 professional-infra/log-observability-elk-grafana/kibana/config/kibana.yml create mode 100644 professional-infra/log-observability-elk-grafana/logstash/config/logstash.yml create mode 100644 professional-infra/log-observability-elk-grafana/logstash/pipeline/logstash.conf create mode 100644 professional-infra/log-observability-elk-grafana/scenarios/incident_simulation.md create mode 100755 professional-infra/log-observability-elk-grafana/scenarios/incident_simulation.sh create mode 100644 professional-infra/migration-validation-framework/Makefile create mode 100644 professional-infra/migration-validation-framework/README.md create mode 100644 professional-infra/migration-validation-framework/cli.py create mode 100644 professional-infra/migration-validation-framework/collectors/disk_usage.py create mode 100644 professional-infra/migration-validation-framework/collectors/mounts.py create mode 100644 professional-infra/migration-validation-framework/collectors/services.py create mode 100644 professional-infra/migration-validation-framework/docs/architecture.md create mode 100644 professional-infra/migration-validation-framework/examples/after.json create mode 100644 professional-infra/migration-validation-framework/examples/before.json create mode 100644 professional-infra/migration-validation-framework/examples/diff.json create mode 100644 professional-infra/migration-validation-framework/scenarios/before_after_comparison.md create mode 100644 professional-infra/migration-validation-framework/tests/test_framework.py create mode 100644 professional-infra/migration-validation-framework/validators/compare.py create mode 100644 professional-infra/zabbix-monitoring-incident-response/.ansible-lint create mode 100644 professional-infra/zabbix-monitoring-incident-response/Makefile create mode 100644 professional-infra/zabbix-monitoring-incident-response/README.md create mode 100644 professional-infra/zabbix-monitoring-incident-response/ansible.cfg create mode 100644 professional-infra/zabbix-monitoring-incident-response/docs/incident-response-runbook.md create mode 100644 professional-infra/zabbix-monitoring-incident-response/docs/maintenance-runbook.md create mode 100644 professional-infra/zabbix-monitoring-incident-response/docs/proxy-design.md create mode 100644 professional-infra/zabbix-monitoring-incident-response/examples/alert-evidence.txt create mode 100644 professional-infra/zabbix-monitoring-incident-response/inventory/hosts.ini create mode 100644 professional-infra/zabbix-monitoring-incident-response/playbooks/zabbix_agent.yml create mode 100644 professional-infra/zabbix-monitoring-incident-response/playbooks/zabbix_proxy.yml create mode 100644 professional-infra/zabbix-monitoring-incident-response/playbooks/zabbix_server.yml create mode 100644 professional-infra/zabbix-monitoring-incident-response/roles/zabbix_agent/defaults/main.yml create mode 100644 professional-infra/zabbix-monitoring-incident-response/roles/zabbix_agent/tasks/main.yml create mode 100644 professional-infra/zabbix-monitoring-incident-response/roles/zabbix_agent/templates/os_checks.conf.j2 create mode 100644 professional-infra/zabbix-monitoring-incident-response/roles/zabbix_agent/templates/zabbix_agentd.conf.j2 create mode 100644 professional-infra/zabbix-monitoring-incident-response/roles/zabbix_proxy/defaults/main.yml create mode 100644 professional-infra/zabbix-monitoring-incident-response/roles/zabbix_proxy/tasks/main.yml create mode 100644 professional-infra/zabbix-monitoring-incident-response/roles/zabbix_proxy/templates/zabbix_proxy.conf.j2 create mode 100644 professional-infra/zabbix-monitoring-incident-response/roles/zabbix_server/defaults/main.yml create mode 100644 professional-infra/zabbix-monitoring-incident-response/roles/zabbix_server/tasks/main.yml create mode 100644 professional-infra/zabbix-monitoring-incident-response/roles/zabbix_server/templates/zabbix_server.conf.j2 create mode 100644 professional-infra/zabbix-monitoring-incident-response/samples/aix-os-checks.json create mode 100644 professional-infra/zabbix-monitoring-incident-response/samples/linux-os-checks.json create mode 100644 professional-infra/zabbix-monitoring-incident-response/scripts/validate_assets.py create mode 100644 professional-infra/zabbix-monitoring-incident-response/templates/aix-os-template.json create mode 100644 professional-infra/zabbix-monitoring-incident-response/templates/linux-os-template.json diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..52e040d --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,47 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install validation tools + run: | + python3 -m venv .venv + . .venv/bin/activate + pip install --upgrade pip + pip install ansible ansible-lint + + - name: Migration validation framework + run: | + cd professional-infra/migration-validation-framework + make test + make demo + + - name: Linux operations automation + run: | + . .venv/bin/activate + cd professional-infra/linux-operations-automation + ansible-playbook --syntax-check playbooks/*.yml + ansible-lint + make test + + - name: Zabbix monitoring and incident response + run: | + . .venv/bin/activate + cd professional-infra/zabbix-monitoring-incident-response + make test + + - name: Log observability ELK Grafana + run: | + cd professional-infra/log-observability-elk-grafana + make test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d413056 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +__pycache__/ +**/__pycache__/ +*.pyc +*.pyo + +# Runtime logs and generated evidence +*.log +logs/ +snapshots/ +reports/ +*.html +migration_report_*.json +migration_report_*.html + +# Local environment files +.env +.env.* +!.env.example +.venv/ +venv/ + +# OS/editor noise +.DS_Store +*.swp diff --git a/AI_CONTEXT.md b/AI_CONTEXT.md new file mode 100644 index 0000000..c52f21b --- /dev/null +++ b/AI_CONTEXT.md @@ -0,0 +1,61 @@ +# AI Context File - CV-Aligned Portfolio Guide + +## Positioning + +This repository should support a Linux/Unix Infrastructure Engineer CV. The main story is operational infrastructure work: Linux operations, Zabbix monitoring, migration validation, incident response, and log observability. + +Do not reposition the repo as a generic cloud-native platform portfolio. DevOps side labs can be mentioned, but the main `professional-infra/` section should stay grounded in real operational work. + +## Current Professional Projects + +### Linux Operations Automation + +Technology stack: Ansible, Bash, Docker Compose. + +Focus: + +- Linux provisioning, patching, hardening, and decommissioning, +- service and failure simulation, +- AAP-style LVM filesystem resize workflow, +- before/after operational evidence. + +### Zabbix Monitoring + Incident Response + +Technology stack: Ansible, Zabbix template assets, JSON/YAML-style operational docs. + +Focus: + +- Zabbix server, proxy, and agent automation structure, +- active/passive proxy design, +- Linux and AIX OS monitoring templates, +- maintenance and incident response runbooks, +- sample check and alert evidence. + +### Migration Validation Framework + +Technology stack: Python, JSON, HTML reports. + +Focus: + +- pre/post migration snapshots, +- drift detection, +- risk assessment, +- migration evidence reports. + +### Log Observability ELK/Grafana + +Technology stack: Docker Compose, Elasticsearch, Logstash, Kibana, Grafana, Filebeat. + +Focus: + +- log ingestion and parsing, +- incident log evidence, +- local demo observability stack. + +## Standards + +- Every project should have `make test`. +- Documentation should include CV relevance and interview talking points. +- Runtime logs, caches, snapshots, and generated reports should stay out of git. +- AIX should be represented by templates, samples, and runbooks; do not claim local AIX runtime. +- AAP should be represented as workflow/job-template documentation plus playbook variables; do not add AWX/AAP runtime unless explicitly requested. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4b57bb5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,39 @@ +# Portfolio Changelog + +## [1.2.0] - 2026-05-04 - CV-Aligned Infrastructure Portfolio Rework + +### Changed +- Reorganized the repository around `professional-infra/` projects aligned with the Linux Engineer CV. +- Repositioned the root README around Linux/Unix operations, Zabbix monitoring, migration validation, and log observability. +- Moved the former infrastructure simulator into Linux Operations Automation. +- Moved the former observability stack into Log Observability ELK/Grafana. + +### Added +- Zabbix Monitoring + Incident Response project with Ansible-first server/proxy/agent structure. +- Linux and AIX Zabbix OS monitoring template assets and sample check payloads. +- Zabbix proxy design, maintenance, and incident response runbooks. +- AAP-style LVM filesystem resize workflow in Linux Operations Automation. +- LVM resize workflow documentation and sample evidence output. + +### Fixed +- Updated CI paths for the new `professional-infra/` structure. +- Updated runbooks and architecture notes to match the CV-aligned portfolio structure. +- Kept runtime logs, caches, and generated reports out of tracked project evidence. + +## [1.1.0] - 2026-05-04 - Portfolio Reliability Pass + +### Changed +- Restored lightweight CI for Python validation, Ansible syntax/lint checks, and Docker Compose validation. +- Separated versioned examples from runtime logs, caches, snapshots, and generated reports. + +### Fixed +- Hardened migration report generation by escaping untrusted snapshot content. +- Added missing local configuration scaffolding for the ELK/Grafana stack. + +## [1.0.0] - 2026-04-29 - Initial Portfolio Release + +### Added +- Ansible lifecycle automation examples. +- Migration validation Python CLI. +- ELK/Grafana local observability scaffold. +- Per-project documentation, architecture notes, examples, and scenarios. diff --git a/Mateusz Suski - Linux Engineer.pdf b/Mateusz Suski - Linux Engineer.pdf new file mode 100644 index 0000000000000000000000000000000000000000..be3b127894a11173337a85537c67b5e12211c664 GIT binary patch literal 275955 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

LypiMnrsl_f36Ws5#BK&J- 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 Wna`38=x)qi5H}XbaRkC&j{h&q4nem7 literal 0 HcmV?d00001 diff --git a/README.md b/README.md new file mode 100644 index 0000000..a8a8d2d --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# CV-Aligned Linux / Unix Infrastructure Portfolio + +This repository maps my Linux/Unix infrastructure experience into practical, reviewable projects: operations automation, monitoring, incident response, migration validation, and log observability. It is intentionally grounded in the kind of work described in my CV: large Linux/Unix estates, Zabbix monitoring, storage and migration work, provisioning, patching, troubleshooting, and operational evidence. + +The repository also leaves room for DevOps side labs, but the main section is professional infrastructure work rather than cloud-native/platform fantasy. + +## What To Review First + +| Order | Project | CV relevance | Technologies | Validation | +| --- | --- | --- | --- | --- | +| 1 | [Linux Operations Automation](professional-infra/linux-operations-automation/) | Linux server deployment, patching, hardening, LVM resize operations, AAP-style automation | Ansible, Bash, Docker Compose | `cd professional-infra/linux-operations-automation && make test` | +| 2 | [Zabbix Monitoring + Incident Response](professional-infra/zabbix-monitoring-incident-response/) | Zabbix maintenance, proxy topology, active/passive checks, Linux/AIX OS monitoring | Ansible, Zabbix templates, YAML | `cd professional-infra/zabbix-monitoring-incident-response && make test` | +| 3 | [Migration Validation Framework](professional-infra/migration-validation-framework/) | Pre/post migration validation, reporting, drift detection, evidence generation | Python, JSON, HTML | `cd professional-infra/migration-validation-framework && make test && make demo` | +| 4 | [Log Observability ELK/Grafana](professional-infra/log-observability-elk-grafana/) | Log ingestion, incident evidence, environment observability | Docker, ELK, Grafana, Filebeat | `cd professional-infra/log-observability-elk-grafana && make test` | + +## CV Skills To Repo Map + +- **Linux/Unix operations:** Linux Operations Automation, LVM resize workflow, patching, hardening, service checks. +- **Automation:** Ansible playbooks/roles, Bash simulation scripts, Python validation tooling. +- **Monitoring:** Zabbix project for OS checks and proxy operations; ELK/Grafana project for log monitoring. +- **Migration work:** Migration Validation Framework for before/after evidence and drift reports. +- **Incident response:** Zabbix runbooks, ELK incident simulation, failure simulation examples. +- **DevOps practices:** lightweight CI, Git workflows, repeatable `make test` targets, containerized lab components. + +## Professional Infrastructure Projects + +The `professional-infra/` directory contains the projects that should be read as direct support for the CV. Each project includes a reviewer-focused README, validation command, examples, and interview talking points. + +## DevOps Side Labs + +Future side projects can live under `devops-labs/` when they are ready. Good candidates are K3s, CI/CD workflow demos, cloud experiments, Wazuh, or other after-hours labs. Empty placeholder project directories are intentionally avoided. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..1a47cf1 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,64 @@ +# Architecture Overview + +## Portfolio Shape + +This repository is organized around professional infrastructure work that maps to the CV: + +```text +professional-infra/ + linux-operations-automation/ + zabbix-monitoring-incident-response/ + migration-validation-framework/ + log-observability-elk-grafana/ + +devops-labs/ + future side projects only when ready +``` + +## Project Roles + +### Linux Operations Automation + +Operational automation for Linux server work: + +- provisioning and baseline configuration, +- patching and hardening, +- service/failure simulation, +- AAP-style LVM filesystem resize workflow with before/after evidence. + +### Zabbix Monitoring + Incident Response + +Simple checks and OS monitoring: + +- Zabbix server/proxy/agent automation structure, +- active and passive proxy design, +- Linux and AIX monitoring templates, +- maintenance and incident response runbooks. + +### Migration Validation Framework + +Evidence tooling for platform/storage migrations: + +- before/after snapshot collection, +- drift detection, +- risk assessment, +- JSON and HTML reports. + +### Log Observability ELK/Grafana + +Log monitoring and incident evidence: + +- Filebeat ingestion, +- Logstash parsing, +- Elasticsearch storage, +- Kibana/Grafana review surfaces, +- incident log simulation. + +## Design Principles + +- Keep implemented work separate from roadmap ideas. +- Prefer reviewable automation and evidence over overbuilt local labs. +- Make every project independently validatable. +- Treat Zabbix and ELK/Grafana as complementary monitoring tools: + - Zabbix for simple checks and OS health, + - ELK/Grafana for logs and observability evidence. diff --git a/docs/runbooks.md b/docs/runbooks.md new file mode 100644 index 0000000..79c45fc --- /dev/null +++ b/docs/runbooks.md @@ -0,0 +1,92 @@ +# Portfolio Runbooks + +These runbooks are scoped to the portfolio version of the projects. They favor fast validation and reviewable evidence over full production operations. + +## Linux Operations Automation + +Validate the implemented Ansible core: + +```bash +cd professional-infra/linux-operations-automation +make test +``` + +Run a safe failure simulation without live SSH hosts: + +```bash +make demo +``` + +Review the AAP-style LVM workflow: + +```bash +ansible-playbook -i inventory/hosts.ini playbooks/lvm_resize.yml --syntax-check +cat docs/aap_lvm_resize_workflow.md +``` + +Run playbooks against your own lab hosts after updating `inventory/hosts.ini`: + +```bash +make run +make patch +make harden +make decommission +``` + +## Zabbix Monitoring + Incident Response + +Validate Zabbix playbooks and templates: + +```bash +cd professional-infra/zabbix-monitoring-incident-response +make test +``` + +Review proxy and OS monitoring operations: + +```bash +cat docs/proxy-design.md +cat docs/maintenance-runbook.md +cat docs/incident-response-runbook.md +``` + +Linux and AIX checks are represented as templates and sample data. AIX is not run locally. + +## Migration Validation Framework + +Validate code and parser/report behavior: + +```bash +cd professional-infra/migration-validation-framework +make test +``` + +Run the included before/after comparison: + +```bash +make demo +``` + +The demo intentionally reports `FAIL` to show a high-risk migration finding. + +## Log Observability ELK/Grafana + +Validate Docker Compose and required bind-mounted configs: + +```bash +cd professional-infra/log-observability-elk-grafana +make test +``` + +Generate sample incident logs without starting the full stack: + +```bash +make demo +``` + +Start the full local demo stack with Docker: + +```bash +make run +make down +``` diff --git a/professional-infra/linux-operations-automation/.ansible-lint b/professional-infra/linux-operations-automation/.ansible-lint new file mode 100644 index 0000000..96f5b9d --- /dev/null +++ b/professional-infra/linux-operations-automation/.ansible-lint @@ -0,0 +1,14 @@ +--- +# Ansible-lint configuration + +skip_list: + - 'role-name' + - 'name[casing]' + - 'line-too-long' + +exclude_paths: + - .git + - .gitea + - molecule/ + - molecule/default/tests/ + - scenarios/ diff --git a/professional-infra/linux-operations-automation/Makefile b/professional-infra/linux-operations-automation/Makefile new file mode 100644 index 0000000..0f8ee90 --- /dev/null +++ b/professional-infra/linux-operations-automation/Makefile @@ -0,0 +1,95 @@ +# Linux Operations Automation Makefile + +.PHONY: help test run demo patch harden decommission lvm-check up down status logs validate clean lint scale-up-web scale-up-db scale-down-web scale-down-db fail-network fail-disk fail-service fail-node scenario-scaling help-scaling help-failure + +help: ## Show this help message + @echo "Linux Operations Automation" + @echo "" + @echo "Available commands:" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " %-18s %s\n", $$1, $$2}' + +test: ## Run offline validation checks + ansible-playbook -i inventory/hosts.ini --syntax-check playbooks/*.yml + ansible-lint + +run: ## Run provisioning against the configured inventory + ansible-playbook -i inventory/hosts.ini playbooks/provision.yml + +demo: ## Run a safe local demonstration without requiring live SSH hosts + SIMULATION_MODE=true bash ./scripts/simulate_failure.sh service 5 web + +patch: ## Apply patching workflow against the configured inventory + ansible-playbook -i inventory/hosts.ini playbooks/patch.yml + +harden: ## Apply hardening workflow against the configured inventory + ansible-playbook -i inventory/hosts.ini playbooks/hardening.yml + +decommission: ## Run decommissioning workflow against the configured inventory + ansible-playbook -i inventory/hosts.ini playbooks/decommission.yml + +lvm-check: ## Validate the AAP-style LVM resize workflow + ansible-playbook -i inventory/hosts.ini --syntax-check playbooks/lvm_resize.yml + +up: ## Start the optional local container scaffold + docker compose up -d + +down: ## Stop the optional local container scaffold + docker compose down + +status: ## Show local scaffold status and inventory hosts + docker compose ps + ansible -i inventory/hosts.ini --list-hosts all || echo "Inventory check failed" + +logs: ## Show local scaffold logs + docker compose logs -f --tail=100 + +validate: ## Run all offline validation checks + $(MAKE) test + docker compose config --quiet + +clean: ## Clean up generated local logs and reports + rm -f logs/*.log reports/*.txt + +lint: ## Lint Ansible content + ansible-lint + +scale-up-web: ## Scale up web servers in simulation mode (usage: make scale-up-web COUNT=2) + SIMULATION_MODE=true bash ./scripts/simulate_scaling.sh up $(or $(COUNT),1) web + +scale-up-db: ## Scale up database servers in simulation mode (usage: make scale-up-db COUNT=1) + SIMULATION_MODE=true bash ./scripts/simulate_scaling.sh up $(or $(COUNT),1) db + +scale-down-web: ## Scale down web servers in simulation mode (usage: make scale-down-web COUNT=1) + SIMULATION_MODE=true bash ./scripts/simulate_scaling.sh down $(or $(COUNT),1) web + +scale-down-db: ## Scale down database servers in simulation mode (usage: make scale-down-db COUNT=1) + SIMULATION_MODE=true bash ./scripts/simulate_scaling.sh down $(or $(COUNT),1) db + +fail-network: ## Simulate network failure safely (usage: make fail-network DURATION=60) + SIMULATION_MODE=true bash ./scripts/simulate_failure.sh network $(or $(DURATION),60) + +fail-disk: ## Simulate disk pressure safely (usage: make fail-disk DURATION=120) + SIMULATION_MODE=true bash ./scripts/simulate_failure.sh disk $(or $(DURATION),120) + +fail-service: ## Simulate service failures safely (usage: make fail-service DURATION=30) + SIMULATION_MODE=true bash ./scripts/simulate_failure.sh service $(or $(DURATION),30) + +fail-node: ## Simulate node failure safely (usage: make fail-node DURATION=300) + SIMULATION_MODE=true bash ./scripts/simulate_failure.sh node $(or $(DURATION),300) + +scenario-scaling: ## Run scaling event syntax validation + ansible-playbook -i inventory/hosts.ini --syntax-check scenarios/scaling_event.yml + +help-scaling: ## Show scaling-related commands + @echo "Scaling Commands:" + @echo " make scale-up-web COUNT=2" + @echo " make scale-up-db COUNT=1" + @echo " make scale-down-web COUNT=1" + @echo " make scale-down-db COUNT=1" + +help-failure: ## Show failure simulation commands + @echo "Failure Simulation Commands:" + @echo " make fail-network DURATION=60" + @echo " make fail-disk DURATION=120" + @echo " make fail-service DURATION=30" + @echo " make fail-node DURATION=300" diff --git a/professional-infra/linux-operations-automation/README.md b/professional-infra/linux-operations-automation/README.md new file mode 100644 index 0000000..fa64ad4 --- /dev/null +++ b/professional-infra/linux-operations-automation/README.md @@ -0,0 +1,92 @@ +# Linux Operations Automation + +## Problem + +Linux infrastructure work often starts as ticket-driven operations: deploy a server, patch it, harden SSH, check a failed service, expand a filesystem, and leave evidence that the change was safe. These tasks need automation that is readable, repeatable, and cautious enough for production-style environments. + +## CV Relevance + +This project maps directly to Linux/Unix operations, server deployment, patching, troubleshooting, and storage/LVM work from enterprise infrastructure environments. The LVM resize workflow is written in an AAP-style shape: explicit survey variables, dry-run defaults, pre-checks, resize actions, and before/after evidence. + +## What This Project Demonstrates + +- Ansible playbooks for common Linux node lifecycle operations. +- Role-based task organization with clear defaults and handlers. +- LVM filesystem expansion workflow suitable for Ansible Automation Platform job templates. +- Safe simulation scripts for failure, service, and scaling exercises. +- Reviewer-friendly evidence in `examples/` without relying on a live enterprise lab. + +## Architecture + +```text +Operator -> Make targets -> Ansible inventory -> Playbooks/Roles -> Linux nodes + -> Simulation scripts -> Example evidence + -> AAP-style LVM workflow -> Before/after report +``` + +Core components: + +- `inventory/hosts.ini` defines realistic host groups. +- `playbooks/` contains provision, patch, harden, and decommission workflows. +- `playbooks/lvm_resize.yml` contains the storage expansion workflow. +- `roles/` contains the implemented Ansible roles. +- `scripts/` provides safe simulation helpers. +- `docker-compose.yml` is a lightweight local scaffold, not a production lab. + +## Quickstart + +```bash +cd professional-infra/linux-operations-automation +make test +make demo +``` + +`make test` runs offline syntax and lint checks. `make demo` runs a safe simulation with `SIMULATION_MODE=true` and does not require reachable SSH hosts. + +To run playbooks against real or lab hosts, update `inventory/hosts.ini` and run: + +```bash +make run +make patch +make harden +make decommission +``` + +Review the LVM workflow: + +```bash +ansible-playbook -i inventory/hosts.ini playbooks/lvm_resize.yml --syntax-check +cat docs/aap_lvm_resize_workflow.md +``` + +## Validation + +```bash +make test +docker compose config --quiet +``` + +The optional compose scaffold can be started with: + +```bash +make up +make down +``` + +## Example Output + +Sample evidence is available in [examples/patch-output.txt](examples/patch-output.txt), [examples/failure-simulation.txt](examples/failure-simulation.txt), and [examples/lvm-resize-output.txt](examples/lvm-resize-output.txt). + +## Interview Talking Points + +- How to make LVM resize automation safe with dry-run defaults and explicit approval. +- Why before/after evidence matters for storage and filesystem changes. +- How Ansible roles keep Linux baseline operations repeatable. +- Where AAP surveys and job templates reduce ticket handling errors. + +## Roadmap + +- Add complete service roles for application deployment examples. +- Add backup, security scan, and disaster recovery playbooks. +- Add a richer local lab with SSH-ready containers. +- Add cloud or Kubernetes deployment variants. diff --git a/professional-infra/linux-operations-automation/VAULT_GUIDE.md b/professional-infra/linux-operations-automation/VAULT_GUIDE.md new file mode 100644 index 0000000..1833f96 --- /dev/null +++ b/professional-infra/linux-operations-automation/VAULT_GUIDE.md @@ -0,0 +1,43 @@ +# Vault Configuration Guide + +## Overview + +The current portfolio demo does not require Ansible Vault for `make test` or `make demo`. Secrets are intentionally kept out of the main validation path so reviewers can run the project offline. + +Use Vault only when extending the simulator to manage real hosts or credentials. + +## Recommended Pattern + +1. Start from the example file: + +```bash +cp group_vars/vault.example.yml group_vars/vault.yml +``` + +2. Replace placeholder values locally. + +3. Encrypt the file before using it with real systems: + +```bash +ansible-vault encrypt group_vars/vault.yml +``` + +4. Do not commit real secret values. Keep `group_vars/vault.example.yml` as the committed reference. + +## Running With Vault + +```bash +ansible-playbook -i inventory/hosts.ini playbooks/provision.yml --ask-vault-pass +``` + +or: + +```bash +ansible-playbook -i inventory/hosts.ini playbooks/provision.yml --vault-password-file ~/.vault_pass.txt +``` + +## Notes + +- The delivered playbooks do not import a vault file by default. +- Add `vars_files` only in an environment-specific branch or private overlay. +- Prefer a secret manager or automation controller for production use. diff --git a/professional-infra/linux-operations-automation/ansible.cfg b/professional-infra/linux-operations-automation/ansible.cfg new file mode 100644 index 0000000..f295265 --- /dev/null +++ b/professional-infra/linux-operations-automation/ansible.cfg @@ -0,0 +1,5 @@ +[defaults] +roles_path = ./roles +inventory = ./inventory/hosts.ini +host_key_checking = False +retry_files_enabled = False diff --git a/professional-infra/linux-operations-automation/docker-compose.yml b/professional-infra/linux-operations-automation/docker-compose.yml new file mode 100644 index 0000000..65d9fa8 --- /dev/null +++ b/professional-infra/linux-operations-automation/docker-compose.yml @@ -0,0 +1,28 @@ +services: + web: + image: debian:12-slim + command: ["sleep", "infinity"] + networks: + infra_sim: + ipv4_address: 172.20.0.11 + + db: + image: debian:12-slim + command: ["sleep", "infinity"] + networks: + infra_sim: + ipv4_address: 172.20.0.21 + + lb: + image: debian:12-slim + command: ["sleep", "infinity"] + networks: + infra_sim: + ipv4_address: 172.20.0.31 + +networks: + infra_sim: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/24 diff --git a/professional-infra/linux-operations-automation/docs/aap_lvm_resize_workflow.md b/professional-infra/linux-operations-automation/docs/aap_lvm_resize_workflow.md new file mode 100644 index 0000000..6446014 --- /dev/null +++ b/professional-infra/linux-operations-automation/docs/aap_lvm_resize_workflow.md @@ -0,0 +1,45 @@ +# AAP-Style LVM Resize Workflow + +## Purpose + +This workflow shows how a routine storage ticket can be converted into a controlled Ansible Automation Platform job. It is intentionally conservative: dry-run is the default, required variables are explicit, and every run produces before/after evidence. + +## Suggested Job Template + +- Name: `Linux - LVM Filesystem Resize` +- Inventory: Linux production or pre-production inventory +- Playbook: `playbooks/lvm_resize.yml` +- Credentials: privileged Linux automation credential +- Privilege escalation: enabled +- Default extra vars: + +```yaml +lvm_dry_run: true +lvm_resize_filesystem: true +``` + +## Suggested Survey Variables + +| Variable | Example | Required | Notes | +| --- | --- | --- | --- | +| `lvm_vg_name` | `vg_app` | yes | Target volume group. | +| `lvm_lv_name` | `lv_data` | yes | Target logical volume. | +| `lvm_mountpoint` | `/data` | yes | Filesystem mountpoint to validate before/after. | +| `lvm_size_request` | `+20G` | yes | Passed to `lvextend -L`; use explicit growth syntax for tickets. | +| `lvm_dry_run` | `true` | yes | Start with `true`; switch to `false` after evidence review. | + +## Safety Notes + +- Run with `lvm_dry_run=true` first and attach output to the ticket. +- Confirm backup/snapshot status before actual resize. +- Confirm filesystem type; this workflow supports XFS and ext filesystems. +- Keep requested size aligned with the ticket approval. +- Use maintenance windows for critical systems. + +## Evidence Captured + +- `lsblk --fs` +- `pvs`, `vgs`, `lvs` +- `df -hT ` before and after +- target LV path and filesystem type +- dry-run flag and requested size diff --git a/professional-infra/linux-operations-automation/docs/architecture.md b/professional-infra/linux-operations-automation/docs/architecture.md new file mode 100644 index 0000000..51fa69e --- /dev/null +++ b/professional-infra/linux-operations-automation/docs/architecture.md @@ -0,0 +1,30 @@ +# Linux Operations Automation Architecture + +## Components + +- Operator interface: `make` targets and direct Ansible commands. +- Inventory: static host groups in `inventory/hosts.ini`. +- Automation: lifecycle playbooks in `playbooks/`. +- Simulation scripts: controlled failure and scaling events in `scripts/`. +- Evidence: logs, reports, scenario notes, and examples. + +## Data Flow + +``` +Operator + -> Make target or shell script + -> Ansible inventory + -> lifecycle playbook + -> managed Linux node + -> log/report artifact +``` + +Failure drills follow a parallel flow: + +``` +Operator -> simulate_failure.sh -> target node/service -> health check -> patch/hardening playbook -> evidence +``` + +## Notes + +The project favors explicit playbooks over hidden orchestration so the operational intent is visible during review. In a production implementation, the same workflows would typically run from a CI runner or automation controller with credentials supplied by a secret manager. diff --git a/professional-infra/linux-operations-automation/examples/failure-simulation.txt b/professional-infra/linux-operations-automation/examples/failure-simulation.txt new file mode 100644 index 0000000..78a4c70 --- /dev/null +++ b/professional-infra/linux-operations-automation/examples/failure-simulation.txt @@ -0,0 +1,8 @@ +2026-04-29 02:13:41 - Starting failure simulation: service 30 web +2026-04-29 02:13:41 - Simulating service failures on containers: web +2026-04-29 02:13:42 - Stopping services in container enterprise-web-1 +2026-04-29 02:13:44 - Health probe failed: http://web01/health returned 503 +2026-04-29 02:14:12 - Cleaning up failure simulation +2026-04-29 02:14:13 - Restarted nginx in enterprise-web-1 +2026-04-29 02:14:18 - Health probe recovered: http://web01/health returned 200 +2026-04-29 02:14:18 - Failure simulation completed successfully diff --git a/professional-infra/linux-operations-automation/examples/lvm-resize-output.txt b/professional-infra/linux-operations-automation/examples/lvm-resize-output.txt new file mode 100644 index 0000000..757698b --- /dev/null +++ b/professional-infra/linux-operations-automation/examples/lvm-resize-output.txt @@ -0,0 +1,19 @@ +TASK [Report LVM resize evidence] ********************************************** +ok: [app01] => { + "msg": { + "host": "app01", + "dry_run": true, + "target": "/dev/vg_app/lv_data", + "mountpoint": "/data", + "requested_size": "+20G", + "filesystem_type": "xfs", + "before_df": [ + "Filesystem Type Size Used Avail Use% Mounted on", + "/dev/mapper/vg_app-lv_data xfs 100G 83G 17G 84% /data" + ], + "after_df": [ + "Filesystem Type Size Used Avail Use% Mounted on", + "/dev/mapper/vg_app-lv_data xfs 100G 83G 17G 84% /data" + ] + } +} diff --git a/professional-infra/linux-operations-automation/examples/patch-output.txt b/professional-infra/linux-operations-automation/examples/patch-output.txt new file mode 100644 index 0000000..481d500 --- /dev/null +++ b/professional-infra/linux-operations-automation/examples/patch-output.txt @@ -0,0 +1,33 @@ +PLAY [Apply Security Patches and Updates] ************************************** + +TASK [Update package cache] ***************************************************** +changed: [web01] +changed: [db01] +ok: [lb01] + +TASK [Check for available updates] ********************************************** +ok: [web01] => {"stdout": "9"} +ok: [db01] => {"stdout": "4"} +ok: [lb01] => {"stdout": "0"} + +TASK [Apply security updates only] ********************************************** +changed: [web01] +changed: [db01] +ok: [lb01] + +TASK [Verify critical services] ************************************************* +ok: [web01] => (item=systemd-journald) +ok: [web01] => (item=cron) +ok: [db01] => (item=systemd-journald) +ok: [lb01] => (item=cron) + +PLAY RECAP ********************************************************************* +web01 : ok=19 changed=6 unreachable=0 failed=0 skipped=2 rescued=0 ignored=1 +db01 : ok=18 changed=5 unreachable=0 failed=0 skipped=2 rescued=0 ignored=1 +lb01 : ok=15 changed=1 unreachable=0 failed=0 skipped=4 rescued=0 ignored=0 + +Patch report +Status: SUCCESS +Window: 02:00-04:00 UTC +Reboot required: false +Notification: infra-team@example.com diff --git a/professional-infra/linux-operations-automation/group_vars/all.yml b/professional-infra/linux-operations-automation/group_vars/all.yml new file mode 100644 index 0000000..00c7285 --- /dev/null +++ b/professional-infra/linux-operations-automation/group_vars/all.yml @@ -0,0 +1,20 @@ +--- +# Group variables for all hosts + +# SSH Configuration +ssh_config: + port: 22 + max_auth_tries: 3 + alive_interval: 300 + +# Firewall defaults +firewall_enabled: true +firewall_default_policy: deny + +# Patching defaults +patch_enabled: true +enforce_patch_window: true + +# Services monitoring +enable_monitoring: false +enable_health_checks: true diff --git a/professional-infra/linux-operations-automation/group_vars/databases.yml b/professional-infra/linux-operations-automation/group_vars/databases.yml new file mode 100644 index 0000000..5c4b3b3 --- /dev/null +++ b/professional-infra/linux-operations-automation/group_vars/databases.yml @@ -0,0 +1,9 @@ +--- +# Database servers group configuration +db_type: postgresql +db_port: 5432 +db_backup_enabled: true +db_backup_path: /var/backups/database + +# Database user (use vault for production) +db_admin_user: postgres diff --git a/professional-infra/linux-operations-automation/group_vars/loadbalancers.yml b/professional-infra/linux-operations-automation/group_vars/loadbalancers.yml new file mode 100644 index 0000000..419e372 --- /dev/null +++ b/professional-infra/linux-operations-automation/group_vars/loadbalancers.yml @@ -0,0 +1,10 @@ +--- +# Load balancers group configuration +lb_type: haproxy +lb_port: 443 +lb_stats_port: 8404 +lb_stats_enabled: true + +# Frontend configuration +frontend_host: "0.0.0.0" +frontend_port: 80 diff --git a/professional-infra/linux-operations-automation/group_vars/monitoring.yml b/professional-infra/linux-operations-automation/group_vars/monitoring.yml new file mode 100644 index 0000000..405f3f4 --- /dev/null +++ b/professional-infra/linux-operations-automation/group_vars/monitoring.yml @@ -0,0 +1,10 @@ +--- +# Monitoring servers group configuration +monitoring_type: prometheus +monitoring_port: 9090 +monitoring_retention: 30d +monitoring_scrape_interval: 15s + +# Grafana configuration +grafana_port: 3000 +grafana_admin_password: "{{ vault_grafana_password }}" diff --git a/professional-infra/linux-operations-automation/group_vars/vault.example.yml b/professional-infra/linux-operations-automation/group_vars/vault.example.yml new file mode 100644 index 0000000..fb6da38 --- /dev/null +++ b/professional-infra/linux-operations-automation/group_vars/vault.example.yml @@ -0,0 +1,8 @@ +--- +# Example variables for secret values. +# Copy these keys into an Ansible Vault encrypted file when real secrets are needed. + +admin_password: "replace-with-vault-managed-value" +db_root_password: "replace-with-vault-managed-value" +grafana_admin_password: "replace-with-vault-managed-value" +ssh_key_passphrase: "replace-with-vault-managed-value" diff --git a/professional-infra/linux-operations-automation/group_vars/webservers.yml b/professional-infra/linux-operations-automation/group_vars/webservers.yml new file mode 100644 index 0000000..d7852a2 --- /dev/null +++ b/professional-infra/linux-operations-automation/group_vars/webservers.yml @@ -0,0 +1,11 @@ +--- +# Webservers group configuration +webserver_type: nginx +http_port: 80 +https_port: 443 +health_check_path: /health + +# Application configuration +app_name: "{{ group_names[0] | default('app') }}" +app_user: "{{ admin_user }}" +app_group: "{{ admin_user }}" diff --git a/professional-infra/linux-operations-automation/inventory/hosts.ini b/professional-infra/linux-operations-automation/inventory/hosts.ini new file mode 100644 index 0000000..c41d2fa --- /dev/null +++ b/professional-infra/linux-operations-automation/inventory/hosts.ini @@ -0,0 +1,35 @@ +[webservers] +web01 ansible_host=172.20.0.11 ansible_user=root ansible_ssh_private_key_file=/root/.ssh/id_rsa +web02 ansible_host=172.20.0.12 ansible_user=root ansible_ssh_private_key_file=/root/.ssh/id_rsa +web03 ansible_host=172.20.0.13 ansible_user=root ansible_ssh_private_key_file=/root/.ssh/id_rsa + +[databases] +db01 ansible_host=172.20.0.21 ansible_user=root ansible_ssh_private_key_file=/root/.ssh/id_rsa +db02 ansible_host=172.20.0.22 ansible_user=root ansible_ssh_private_key_file=/root/.ssh/id_rsa + +[loadbalancers] +lb01 ansible_host=172.20.0.31 ansible_user=root ansible_ssh_private_key_file=/root/.ssh/id_rsa + +[monitoring] +mon01 ansible_host=172.20.0.41 ansible_user=root ansible_ssh_private_key_file=/root/.ssh/id_rsa + +[all:vars] +ansible_python_interpreter=/usr/bin/python3 +ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' +ansible_connection=ssh + +[webservers:vars] +node_type=web +environment=production + +[databases:vars] +node_type=database +environment=production + +[loadbalancers:vars] +node_type=loadbalancer +environment=production + +[monitoring:vars] +node_type=monitoring +environment=production \ No newline at end of file diff --git a/professional-infra/linux-operations-automation/molecule/default/converge.yml b/professional-infra/linux-operations-automation/molecule/default/converge.yml new file mode 100644 index 0000000..b1f91bd --- /dev/null +++ b/professional-infra/linux-operations-automation/molecule/default/converge.yml @@ -0,0 +1,24 @@ +--- +# Molecule converge playbook - applies roles to test them + +- name: Converge + hosts: all + become: true + gather_facts: true + + pre_tasks: + - name: Update apt cache + apt: + update_cache: yes + cache_valid_time: 3600 + when: ansible_os_family == "Debian" + + roles: + - role: base_provision + - role: hardening + - role: patching + + post_tasks: + - name: Print Ansible facts + debug: + var: ansible_facts diff --git a/professional-infra/linux-operations-automation/molecule/default/destroy.yml b/professional-infra/linux-operations-automation/molecule/default/destroy.yml new file mode 100644 index 0000000..80d35d7 --- /dev/null +++ b/professional-infra/linux-operations-automation/molecule/default/destroy.yml @@ -0,0 +1,15 @@ +--- +# Molecule destroy playbook + +- name: Destroy + hosts: localhost + gather_facts: false + tasks: + - name: Destroy molecule containers + docker_container: + name: "{{ item }}" + state: absent + force_kill: yes + loop: "{{ molecule_yml.platforms | map(attribute='name') | list }}" + register: destroy_result + ignore_errors: yes diff --git a/professional-infra/linux-operations-automation/molecule/default/molecule.yml b/professional-infra/linux-operations-automation/molecule/default/molecule.yml new file mode 100644 index 0000000..ead4557 --- /dev/null +++ b/professional-infra/linux-operations-automation/molecule/default/molecule.yml @@ -0,0 +1,31 @@ +--- +# Molecule configuration for Ansible role testing + +driver: + name: docker + +platforms: + - name: ubuntu-22.04 + image: geerlingguy/docker-ubuntu2204-ansible:latest + pre_build_image: true + privileged: true + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + +provisioner: + name: ansible + config_options: + defaults: + gathering: smart + fact_caching: jsonfile + fact_caching_connection: /tmp/ansible_facts + fact_caching_timeout: 3600 + deprecation_warnings: false + +verifier: + name: ansible + directory: molecule/default/tests + +lint: | + yamllint . + ansible-lint diff --git a/professional-infra/linux-operations-automation/molecule/default/verify.yml b/professional-infra/linux-operations-automation/molecule/default/verify.yml new file mode 100644 index 0000000..7981bdc --- /dev/null +++ b/professional-infra/linux-operations-automation/molecule/default/verify.yml @@ -0,0 +1,32 @@ +--- +# Molecule verify playbook - runs tests to verify roles + +- name: Verify + hosts: all + gather_facts: false + tasks: + - name: Check if base OS packages are installed + shell: dpkg -l | grep -E '(curl|wget|vim|htop)' + register: package_check + failed_when: package_check.rc not in [0, 1] + + - name: Check SSH configuration + stat: + path: /etc/ssh/sshd_config + register: ssh_config_stat + failed_when: not ssh_config_stat.stat.exists + + - name: Check firewall status + shell: ufw status | grep -q active + register: firewall_check + failed_when: false + + - name: Verify admin user exists + getent: + database: passwd + key: infra-admin + failed_when: false + + - name: Print verification results + debug: + msg: "Role verification completed" diff --git a/professional-infra/linux-operations-automation/playbooks/decommission.yml b/professional-infra/linux-operations-automation/playbooks/decommission.yml new file mode 100644 index 0000000..f85cf83 --- /dev/null +++ b/professional-infra/linux-operations-automation/playbooks/decommission.yml @@ -0,0 +1,34 @@ +--- +- name: Decommission Enterprise Infrastructure Nodes + hosts: all + become: true + gather_facts: true + + pre_tasks: + - name: Confirm decommissioning + ansible.builtin.pause: + prompt: | + WARNING: This will decommission {{ inventory_hostname }} + Backup Data: {{ backup_data }} + Export Config: {{ export_config }} + + Press ENTER to continue or Ctrl+C to cancel + + - name: Display decommissioning information + ansible.builtin.debug: + msg: | + Decommissioning {{ inventory_hostname }} + Auto Shutdown: {{ auto_shutdown }} + Backup Enabled: {{ backup_data }} + + roles: + - role: decommission + tags: ['decommission', 'cleanup'] + + post_tasks: + - name: Display decommissioning summary + ansible.builtin.debug: + msg: | + Decommissioning completed! + Host: {{ inventory_hostname }} + Backup Location: /var/backups/decommission-{{ ansible_date_time.iso8601 }}/ diff --git a/professional-infra/linux-operations-automation/playbooks/hardening.yml b/professional-infra/linux-operations-automation/playbooks/hardening.yml new file mode 100644 index 0000000..020eeaa --- /dev/null +++ b/professional-infra/linux-operations-automation/playbooks/hardening.yml @@ -0,0 +1,124 @@ +--- +- name: Harden Enterprise Infrastructure Nodes + hosts: all + become: true + gather_facts: true + + pre_tasks: + - name: Validate hardening prerequisites + ansible.builtin.assert: + that: + - ansible_os_family == "Debian" + - cis_level in [1, 2] + fail_msg: "Invalid hardening configuration" + + - name: Display hardening information + ansible.builtin.debug: + msg: | + Hardening {{ inventory_hostname }} + CIS Level: {{ cis_level }} + Disable Root Login: {{ disable_root_login }} + + roles: + - role: hardening + tags: ['hardening', 'security'] + + post_tasks: + - name: Display hardening summary + ansible.builtin.debug: + msg: | + Hardening completed successfully! + Host: {{ inventory_hostname }} + + when: ansible_os_family == "Debian" + + - name: Configure auditd + when: auditd_enabled + block: + - name: Install auditd + ansible.builtin.apt: + name: auditd + state: present + when: ansible_os_family == "Debian" + + - name: Configure audit rules + ansible.builtin.template: + src: templates/audit.rules.j2 + dest: /etc/audit/rules.d/hardening.rules + mode: '0644' + + - name: Enable auditd service + ansible.builtin.service: + name: auditd + state: started + enabled: true + + - name: Configure AppArmor + when: apparmor_enabled and ansible_os_family == "Debian" + block: + - name: Install apparmor + ansible.builtin.apt: + name: apparmor + state: present + when: ansible_os_family == "Debian" + + - name: Enable apparmor service + ansible.builtin.service: + name: apparmor + state: started + enabled: true + + - name: Configure sysctl hardening + ansible.posix.sysctl: + name: "{{ item.key }}" + value: "{{ item.value }}" + state: present + reload: true + loop: + - { key: 'net.ipv4.ip_forward', value: '0' } + - { key: 'net.ipv4.conf.all.send_redirects', value: '0' } + - { key: 'net.ipv4.conf.default.send_redirects', value: '0' } + - { key: 'net.ipv4.tcp_syncookies', value: '1' } + - { key: 'net.ipv4.icmp_echo_ignore_broadcasts', value: '1' } + + - name: Set secure file permissions + ansible.builtin.file: + path: "{{ item }}" + mode: '0644' + owner: root + group: root + loop: + - /etc/passwd + - /etc/group + - /etc/shadow + - /etc/gshadow + + - name: Lock inactive user accounts + ansible.builtin.command: usermod -L "{{ item }}" + loop: "{{ inactive_users | default([]) }}" + changed_when: false + + - name: Configure password policies + community.general.pam_limits: + domain: '*' + limit_type: hard + limit_item: nofile + value: 1024 + + - name: Generate hardening report + ansible.builtin.template: + src: templates/hardening_report.j2 + dest: "/var/log/hardening_report_{{ ansible_date_time.iso8601 }}.log" + mode: '0644' + + handlers: + - name: restart sshd + ansible.builtin.service: + name: ssh + state: restarted + + - name: restart auditd + ansible.builtin.service: + name: auditd + state: restarted + when: auditd_enabled diff --git a/professional-infra/linux-operations-automation/playbooks/lvm_resize.yml b/professional-infra/linux-operations-automation/playbooks/lvm_resize.yml new file mode 100644 index 0000000..3f39a28 --- /dev/null +++ b/professional-infra/linux-operations-automation/playbooks/lvm_resize.yml @@ -0,0 +1,149 @@ +--- +- name: AAP-style LVM filesystem resize workflow + hosts: all + become: true + gather_facts: true + + vars: + lvm_dry_run: true + lvm_vg_name: "" + lvm_lv_name: "" + lvm_mountpoint: "" + lvm_size_request: "+10G" + lvm_resize_filesystem: true + + pre_tasks: + - name: Validate required survey variables + ansible.builtin.assert: + that: + - lvm_vg_name | length > 0 + - lvm_lv_name | length > 0 + - lvm_mountpoint | length > 0 + - lvm_size_request | length > 0 + fail_msg: "Required variables: lvm_vg_name, lvm_lv_name, lvm_mountpoint, lvm_size_request" + + tasks: + - name: Capture block device layout before resize + ansible.builtin.command: + argv: + - lsblk + - --fs + register: lvm_lsblk_before + changed_when: false + + - name: Capture physical volumes before resize + ansible.builtin.command: + argv: + - pvs + - --noheadings + - --units + - g + register: lvm_pvs_before + changed_when: false + + - name: Capture volume groups before resize + ansible.builtin.command: + argv: + - vgs + - --noheadings + - --units + - g + register: lvm_vgs_before + changed_when: false + + - name: Capture logical volumes before resize + ansible.builtin.command: + argv: + - lvs + - --noheadings + - --units + - g + register: lvm_lvs_before + changed_when: false + + - name: Capture filesystem usage before resize + ansible.builtin.command: + argv: + - df + - -hT + - "{{ lvm_mountpoint }}" + register: lvm_df_before + changed_when: false + + - name: Validate target logical volume exists + ansible.builtin.command: + argv: + - lvs + - "/dev/{{ lvm_vg_name }}/{{ lvm_lv_name }}" + register: lvm_target_check + changed_when: false + + - name: Show dry-run resize command + ansible.builtin.debug: + msg: "DRY RUN: would run lvextend -L {{ lvm_size_request }} /dev/{{ lvm_vg_name }}/{{ lvm_lv_name }}" + when: lvm_dry_run | bool + + - name: Extend logical volume + ansible.builtin.command: + argv: + - lvextend + - -L + - "{{ lvm_size_request }}" + - "/dev/{{ lvm_vg_name }}/{{ lvm_lv_name }}" + register: lvm_lvextend_result + changed_when: true + when: not (lvm_dry_run | bool) + + - name: Detect filesystem type + ansible.builtin.command: + argv: + - findmnt + - -n + - -o + - FSTYPE + - "{{ lvm_mountpoint }}" + register: lvm_fstype + changed_when: false + + - name: Resize XFS filesystem + ansible.builtin.command: + argv: + - xfs_growfs + - "{{ lvm_mountpoint }}" + changed_when: true + when: + - not (lvm_dry_run | bool) + - lvm_resize_filesystem | bool + - lvm_fstype.stdout == "xfs" + + - name: Resize ext filesystem + ansible.builtin.command: + argv: + - resize2fs + - "/dev/{{ lvm_vg_name }}/{{ lvm_lv_name }}" + changed_when: true + when: + - not (lvm_dry_run | bool) + - lvm_resize_filesystem | bool + - lvm_fstype.stdout in ["ext2", "ext3", "ext4"] + + - name: Capture filesystem usage after resize + ansible.builtin.command: + argv: + - df + - -hT + - "{{ lvm_mountpoint }}" + register: lvm_df_after + changed_when: false + + - name: Report LVM resize evidence + ansible.builtin.debug: + msg: + host: "{{ inventory_hostname }}" + dry_run: "{{ lvm_dry_run }}" + target: "/dev/{{ lvm_vg_name }}/{{ lvm_lv_name }}" + mountpoint: "{{ lvm_mountpoint }}" + requested_size: "{{ lvm_size_request }}" + filesystem_type: "{{ lvm_fstype.stdout | default('unknown') }}" + before_df: "{{ lvm_df_before.stdout_lines }}" + after_df: "{{ lvm_df_after.stdout_lines }}" diff --git a/professional-infra/linux-operations-automation/playbooks/patch.yml b/professional-infra/linux-operations-automation/playbooks/patch.yml new file mode 100644 index 0000000..f630847 --- /dev/null +++ b/professional-infra/linux-operations-automation/playbooks/patch.yml @@ -0,0 +1,31 @@ +--- +- name: Apply Security Patches and Updates + hosts: all + become: true + gather_facts: true + + pre_tasks: + - name: Validate patch prerequisites + ansible.builtin.assert: + that: + - ansible_os_family == "Debian" + fail_msg: "Patching supported only on Debian-based systems" + + - name: Display patch information + ansible.builtin.debug: + msg: | + Patching {{ inventory_hostname }} + Patch Window: {{ patch_window_start }} - {{ patch_window_end }} + Security Only: {{ patch_security_only }} + + roles: + - role: patching + tags: ['patch', 'updates'] + + post_tasks: + - name: Display patching summary + ansible.builtin.debug: + msg: | + Patching completed! + Host: {{ inventory_hostname }} + Reboot Required: {{ reboot_required | default(false) }} diff --git a/professional-infra/linux-operations-automation/playbooks/provision.yml b/professional-infra/linux-operations-automation/playbooks/provision.yml new file mode 100644 index 0000000..5684d6f --- /dev/null +++ b/professional-infra/linux-operations-automation/playbooks/provision.yml @@ -0,0 +1,33 @@ +--- +- name: Provision Enterprise Infrastructure Nodes + hosts: all + become: true + gather_facts: true + + pre_tasks: + - name: Validate Ansible version + ansible.builtin.assert: + that: + - ansible_version.major >= 2 + - ansible_version.minor >= 9 + fail_msg: "Ansible 2.9+ is required" + + - name: Display provisioning information + ansible.builtin.debug: + msg: | + Provisioning {{ inventory_hostname }} + OS: {{ ansible_os_family }} + Python: {{ ansible_python_version }} + + roles: + - role: base_provision + tags: ['provision', 'base'] + + post_tasks: + - name: Generate provisioning summary + ansible.builtin.debug: + msg: | + Provisioning completed successfully! + Host: {{ inventory_hostname }} + IP: {{ ansible_default_ipv4.address }} + OS: {{ ansible_os_family }} {{ ansible_os_version }} diff --git a/professional-infra/linux-operations-automation/roles/base_provision/README.md b/professional-infra/linux-operations-automation/roles/base_provision/README.md new file mode 100644 index 0000000..2b1be9e --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/base_provision/README.md @@ -0,0 +1,48 @@ +# Base Provision Role + +Provision basic infrastructure on enterprise nodes with security hardening. + +## Features + +- **Idempotent**: All tasks use proper idempotency markers (`changed_when`, `failed_when`) +- **Handlers**: SSH and fail2ban restarts use handlers instead of direct service calls +- **Variables**: All configuration in `defaults/main.yml` - no hardcoding +- **Validation**: Pre-flight checks for system requirements +- **Firewall**: UFW firewall configuration with configurable rules +- **SSH Security**: Root login disabled, password auth disabled, key-based auth only + +## Role Variables + +See `defaults/main.yml` for all available variables. + +### Key Variables + +- `node_timezone`: System timezone (default: UTC) +- `admin_user`: Admin username for infrastructure access +- `ssh_port`: SSH service port (default: 22) +- `base_packages`: List of base packages to install +- `firewall_enabled`: Enable UFW firewall (default: true) +- `firewall_allowed_tcp_ports`: Allowed TCP ports for firewall + +## Secret Variables + +This portfolio demo does not require secrets for offline validation. If you extend it with real passwords or keys, copy `group_vars/vault.example.yml` into an encrypted Ansible Vault file and keep real values out of normal git history. + +## Usage + +```yaml +- role: base_provision + vars: + node_timezone: "Europe/Warsaw" + firewall_enabled: true +``` + +## Handlers + +- `restart sshd`: Restarts SSH service (triggered by config changes) +- `restart fail2ban`: Restarts fail2ban service (triggered by config changes) + +## Tags + +- `provision`: All provisioning tasks +- `base`: Base provision role tasks diff --git a/professional-infra/linux-operations-automation/roles/base_provision/defaults/main.yml b/professional-infra/linux-operations-automation/roles/base_provision/defaults/main.yml new file mode 100644 index 0000000..2511cb9 --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/base_provision/defaults/main.yml @@ -0,0 +1,44 @@ +--- +# Base provisioning configuration +node_timezone: "UTC" +admin_user: "infra-admin" +ssh_port: 22 +ssh_disabled_root_login: true +ssh_disable_password_auth: true + +# Packages to install +base_packages: + - curl + - wget + - vim + - htop + - net-tools + - iptables + - fail2ban + - unattended-upgrades + +# Firewall rules +firewall_enabled: true +firewall_default_policy: deny +firewall_allowed_tcp_ports: + - 22 + - 80 + - 443 + +# Application directories +app_directories: + - path: /opt/application + owner: "{{ admin_user }}" + group: "{{ admin_user }}" + mode: '0755' + - path: /var/log/application + owner: "{{ admin_user }}" + group: "{{ admin_user }}" + mode: '0755' + - path: /etc/application + owner: root + group: root + mode: '0755' + +# Service verification +services_to_verify: [] diff --git a/professional-infra/linux-operations-automation/roles/base_provision/handlers/main.yml b/professional-infra/linux-operations-automation/roles/base_provision/handlers/main.yml new file mode 100644 index 0000000..c7fbc01 --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/base_provision/handlers/main.yml @@ -0,0 +1,11 @@ +--- +- name: restart sshd + ansible.builtin.service: + name: sshd + state: restarted + +- name: restart fail2ban + ansible.builtin.service: + name: fail2ban + state: restarted + enabled: true diff --git a/professional-infra/linux-operations-automation/roles/base_provision/tasks/main.yml b/professional-infra/linux-operations-automation/roles/base_provision/tasks/main.yml new file mode 100644 index 0000000..4a7feca --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/base_provision/tasks/main.yml @@ -0,0 +1,138 @@ +--- +- name: Validate system requirements + ansible.builtin.assert: + that: + - ansible_os_family == "Debian" + - ansible_python_version is version('3.6', '>=') + fail_msg: "Unsupported system - requires Debian and Python 3.6+" + +- name: Update package cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 + changed_when: false + +- name: Install base packages + ansible.builtin.apt: + name: "{{ base_packages }}" + state: present + update_cache: true + +- name: Check if admin user exists + ansible.builtin.getent: + database: passwd + key: "{{ admin_user }}" + register: admin_check + failed_when: false + changed_when: false + +- name: Create admin user + ansible.builtin.user: + name: "{{ admin_user }}" + groups: sudo + append: true + create_home: true + shell: /bin/bash + when: admin_check.failed + +- name: Configure timezone + community.general.timezone: + name: "{{ node_timezone }}" + +- name: Configure SSH security + block: + - name: Disable root SSH login + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^PermitRootLogin' + line: 'PermitRootLogin no' + state: present + when: ssh_disabled_root_login + notify: restart sshd + + - name: Set SSH port + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^Port' + line: "Port {{ ssh_port }}" + state: present + notify: restart sshd + + - name: Disable password authentication + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^PasswordAuthentication' + line: 'PasswordAuthentication no' + state: present + when: ssh_disable_password_auth + notify: restart sshd + +- name: Configure firewall + block: + - name: Enable UFW firewall + community.general.ufw: + state: enabled + policy: "{{ firewall_default_policy }}" + when: firewall_enabled + + - name: Allow SSH access + community.general.ufw: + rule: allow + port: "{{ ssh_port }}" + proto: tcp + when: firewall_enabled + + - name: Allow HTTP/HTTPS + community.general.ufw: + rule: allow + port: "{{ item }}" + proto: tcp + loop: "{{ firewall_allowed_tcp_ports }}" + when: firewall_enabled and item not in [ssh_port] + +- name: Configure fail2ban + ansible.builtin.template: + src: jail.local.j2 + dest: /etc/fail2ban/jail.local + backup: true + mode: '0644' + notify: restart fail2ban + +- name: Enable unattended upgrades + ansible.builtin.lineinfile: + path: /etc/apt/apt.conf.d/20auto-upgrades + regexp: '^APT::Periodic::Unattended-Upgrade' + line: 'APT::Periodic::Unattended-Upgrade "1";' + state: present + +- name: Create application directories + ansible.builtin.file: + path: "{{ item.path }}" + state: directory + owner: "{{ item.owner }}" + group: "{{ item.group }}" + mode: "{{ item.mode }}" + loop: "{{ app_directories }}" + +- name: Record role-specific service intent + ansible.builtin.debug: + msg: "Would configure {{ node_type | default('generic') }} service components in a full lab deployment" + +- name: Verify services are running + ansible.builtin.service: + name: "{{ item }}" + state: started + enabled: true + loop: "{{ services_to_verify }}" + when: services_to_verify | length > 0 + failed_when: false + +- name: Run health checks + ansible.builtin.uri: + url: http://localhost/health + method: GET + status_code: 200 + register: health_check + failed_when: false + ignore_errors: true + when: "'webservers' in group_names" diff --git a/professional-infra/linux-operations-automation/roles/base_provision/templates/jail.local.j2 b/professional-infra/linux-operations-automation/roles/base_provision/templates/jail.local.j2 new file mode 100644 index 0000000..fbe2e13 --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/base_provision/templates/jail.local.j2 @@ -0,0 +1,14 @@ +# fail2ban configuration +[DEFAULT] +bantime = 3600 +findtime = 600 +maxretry = 5 + +[sshd] +enabled = true +port = {{ ssh_port }} +logpath = /var/log/auth.log +maxretry = 3 + +[recidive] +enabled = true diff --git a/professional-infra/linux-operations-automation/roles/decommission/README.md b/professional-infra/linux-operations-automation/roles/decommission/README.md new file mode 100644 index 0000000..93e521f --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/decommission/README.md @@ -0,0 +1,62 @@ +# Decommission Role + +Gracefully decommission enterprise infrastructure nodes with comprehensive backup and cleanup. + +## Features + +- **Confirmation Prompt**: Interactive confirmation before decommissioning +- **Graceful Shutdown**: Stop services gracefully with connection drain time +- **Comprehensive Backup**: Archive configurations and data before cleanup +- **Selective Cleanup**: Only remove items that were deployed +- **Logging**: Detailed decommissioning logs for audit trail +- **Notifications**: Optional email notifications on completion + +## Role Variables + +See `defaults/main.yml` for all available variables. + +### Key Variables + +- `backup_data`: Backup application data (default: true) +- `export_config`: Export system configuration (default: true) +- `graceful_shutdown`: Graceful service shutdown (default: true) +- `auto_shutdown`: Auto shutdown after decommissioning (default: false) +- `application_services`: Services to stop +- `application_packages`: Packages to remove +- `decommission_notification_email`: Email for notifications (optional) + +## Usage + +```yaml +- role: decommission + vars: + backup_data: true + export_config: true + auto_shutdown: false + decommission_notification_email: "ops@company.com" +``` + +## Backup Locations + +- Configuration: `/var/backups/decommission-/config/` +- Data: `/var/backups/decommission-/data/` +- Report: `/var/log/decommission_report_.log` + +## Supported Groups + +- `webservers`: Backs up /var/www/html +- `databases`: Backs up PostgreSQL data +- `monitoring`: Backs up Prometheus data +- `loadbalancers`: Loadbalancer cleanup + +## Safety Features + +- Interactive confirmation before execution +- Connection drain time before shutdown (30 seconds) +- Errors are logged but don't stop the process +- Comprehensive audit log + +## Tags + +- `decommission`: All decommissioning tasks +- `cleanup`: Cleanup-related tasks diff --git a/professional-infra/linux-operations-automation/roles/decommission/defaults/main.yml b/professional-infra/linux-operations-automation/roles/decommission/defaults/main.yml new file mode 100644 index 0000000..da15e60 --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/decommission/defaults/main.yml @@ -0,0 +1,34 @@ +--- +# Decommissioning configuration +backup_data: true +export_config: true +graceful_shutdown: true +cleanup_inventory: true +auto_shutdown: false +shutdown_delay: 10 + +# Services to stop gracefully +application_services: + - nginx + - postgresql + - haproxy + +# Packages to remove +application_packages: + - nginx + - postgresql + - haproxy + - prometheus + +# Directories to archive +config_paths: + - /etc/ + - /opt/application/ + +data_paths: + - /var/www/html + - /var/lib/postgresql + - /var/lib/prometheus + +# Notification settings +decommission_notification_email: null diff --git a/professional-infra/linux-operations-automation/roles/decommission/tasks/main.yml b/professional-infra/linux-operations-automation/roles/decommission/tasks/main.yml new file mode 100644 index 0000000..f5604d4 --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/decommission/tasks/main.yml @@ -0,0 +1,177 @@ +--- +- name: Validate decommissioning requirements + ansible.builtin.assert: + that: + - backup_data or not backup_data + fail_msg: "Invalid decommissioning configuration" + +- name: Pre-decommissioning checks + block: + - name: Check node health + ansible.builtin.uri: + url: http://localhost/health + method: GET + status_code: 200 + register: health_check + failed_when: false + ignore_errors: true + when: "'webservers' in group_names" + + - name: Create decommissioning backup directory + ansible.builtin.file: + path: "/var/backups/decommission-{{ ansible_date_time.iso8601 }}" + state: directory + mode: '0755' + + - name: Initialize decommissioning log + ansible.builtin.file: + path: "/var/log/decommission.log" + state: touch + mode: '0644' + modification_time: now + access_time: now + + - name: Log decommissioning start + ansible.builtin.lineinfile: + path: "/var/log/decommission.log" + line: "{{ ansible_date_time.iso8601 }} - Starting decommissioning of {{ inventory_hostname }}" + state: present + +- name: Graceful application shutdown + block: + - name: Stop application services + ansible.builtin.service: + name: "{{ item }}" + state: stopped + loop: "{{ application_services }}" + failed_when: false + when: graceful_shutdown + + - name: Wait for connections to drain + ansible.builtin.pause: + seconds: 30 + when: graceful_shutdown and ("webservers" in group_names or "loadbalancers" in group_names) + +- name: Export and backup data + block: + - name: Create config export directory + ansible.builtin.file: + path: "/var/backups/decommission-{{ ansible_date_time.iso8601 }}/config" + state: directory + mode: '0755' + + - name: Archive system configuration + community.general.archive: + path: "{{ config_paths }}" + dest: "/var/backups/decommission-{{ ansible_date_time.iso8601 }}/config/system_config.tar.gz" + format: gz + when: export_config + failed_when: false # noqa risky-file-permissions + + - name: Create data backup directory + ansible.builtin.file: + path: "/var/backups/decommission-{{ ansible_date_time.iso8601 }}/data" + state: directory + mode: '0755' + when: backup_data + + - name: Backup individual data paths + community.general.archive: + path: "{{ item }}" + dest: "/var/backups/decommission-{{ ansible_date_time.iso8601 }}/data/{{ item | regex_replace('/', '_') }}.tar.gz" + format: gz + loop: "{{ data_paths }}" + when: backup_data + failed_when: false # noqa risky-file-permissions + +- name: Update monitoring and load balancing + block: + - name: Remove from load balancer + ansible.builtin.debug: + msg: "Would remove {{ inventory_hostname }} from load balancer" + when: "'webservers' in group_names or 'databases' in group_names" + + - name: Update monitoring alerts + ansible.builtin.debug: + msg: "Would update monitoring alerts for {{ inventory_hostname }}" + when: "'monitoring' not in group_names" + +- name: Clean up application + block: + - name: Remove application directories + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - /opt/application + - /var/www/html + - /var/lib/postgresql + - /var/lib/prometheus + failed_when: false + + - name: Remove application packages + ansible.builtin.apt: + name: "{{ item }}" + state: absent + purge: true + loop: "{{ application_packages }}" + failed_when: false + + - name: Clean system logs + ansible.builtin.shell: | + set -o pipefail + find /var/log -name "*.log" -type f -size +0 -exec truncate -s 0 {} \; + changed_when: false + failed_when: false + + - name: Remove SSH credentials + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - /root/.ssh/authorized_keys + - /root/.ssh/known_hosts + - /home/infra-admin/.ssh/authorized_keys + failed_when: false + +- name: Generate decommissioning report + ansible.builtin.template: + src: decommission_report.j2 + dest: "/var/log/decommission_report_{{ ansible_date_time.iso8601 }}.log" + mode: '0644' + vars: + backup_location: "/var/backups/decommission-{{ ansible_date_time.iso8601 }}" + +- name: Send decommissioning notification + community.general.mail: + host: localhost + port: 25 + to: "{{ decommission_notification_email }}" + subject: "Node Decommissioned - {{ inventory_hostname }}" + body: | + Node {{ inventory_hostname }} has been successfully decommissioned. + + Backup location: /var/backups/decommission-{{ ansible_date_time.iso8601 }}/ + Services stopped: {{ application_services | join(', ') }} + Configuration exported: {{ export_config }} + Data backed up: {{ backup_data }} + + See /var/log/decommission_report_{{ ansible_date_time.iso8601 }}.log for details + when: decommission_notification_email is defined + failed_when: false + +- name: Finalize decommissioning + block: + - name: Log decommissioning completion + ansible.builtin.lineinfile: + path: "/var/log/decommission.log" + line: "{{ ansible_date_time.iso8601 }} - Decommissioning completed for {{ inventory_hostname }}" + state: present + + - name: Perform system shutdown + ansible.builtin.reboot: + msg: "System scheduled for shutdown after decommissioning" + delay: "{{ shutdown_delay }}" + when: auto_shutdown | bool + async: 1 + poll: 0 diff --git a/professional-infra/linux-operations-automation/roles/decommission/templates/decommission_report.j2 b/professional-infra/linux-operations-automation/roles/decommission/templates/decommission_report.j2 new file mode 100644 index 0000000..2eba80f --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/decommission/templates/decommission_report.j2 @@ -0,0 +1,13 @@ +Decommissioning Report +====================== +Generated: {{ ansible_date_time.iso8601 }} +Host: {{ inventory_hostname }} + +Status: COMPLETED +Backup Location: {{ backup_location }} + +Configuration Exported: {{ export_config }} +Data Backed Up: {{ backup_data }} +Services Stopped: {{ application_services | join(', ') }} + +Log Location: /var/log/decommission.log diff --git a/professional-infra/linux-operations-automation/roles/hardening/README.md b/professional-infra/linux-operations-automation/roles/hardening/README.md new file mode 100644 index 0000000..02f2b21 --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/hardening/README.md @@ -0,0 +1,58 @@ +# Hardening Role + +Apply security hardening to enterprise infrastructure nodes following CIS benchmarks. + +## Features + +- **CIS Compliance**: Support for CIS hardening levels 1 and 2 +- **SSH Hardening**: Disable root login, password auth, set auth limits +- **Firewall Configuration**: UFW with configurable rules +- **Service Cleanup**: Disable unnecessary services and remove insecure packages +- **Handlers**: SSH restarts via handlers + +## Role Variables + +See `defaults/main.yml` for all available variables. + +### Key Variables + +- `cis_level`: CIS hardening level (1 or 2) +- `disable_root_login`: Disable root SSH login (default: true) +- `secure_ssh_config`: Apply SSH security hardening (default: true) +- `firewall_policy`: Firewall default policy (default: deny) +- `ssh_max_auth_tries`: Maximum SSH authentication attempts (default: 3) +- `ssh_client_alive_interval`: SSH client alive interval in seconds (default: 300) +- `ssh_allowed_networks`: Networks allowed SSH access from + +### SSH Allowed Networks + +Default trusted networks: +- 10.0.0.0/8 (Private Class A) +- 172.16.0.0/12 (Private Class B) +- 192.168.0.0/16 (Private Class C) + +## Usage + +```yaml +- role: hardening + vars: + cis_level: 1 + disable_root_login: true + ssh_allowed_networks: + - 10.0.0.0/8 + - 203.0.113.0/24 +``` + +## SSH Configuration Changes + +- Root login disabled +- Password authentication disabled +- Maximum auth tries: 3 +- Empty passwords prohibited +- Client alive interval: 300 seconds +- Client alive count max: 2 + +## Tags + +- `hardening`: All hardening tasks +- `security`: Security-related tasks diff --git a/professional-infra/linux-operations-automation/roles/hardening/defaults/main.yml b/professional-infra/linux-operations-automation/roles/hardening/defaults/main.yml new file mode 100644 index 0000000..ebafd65 --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/hardening/defaults/main.yml @@ -0,0 +1,35 @@ +--- +# Hardening configuration +cis_level: 1 +disable_root_login: true +secure_ssh_config: true +firewall_policy: deny +auditd_enabled: true +selinux_mode: enforcing +apparmor_enabled: true + +# SSH Hardening +ssh_max_auth_tries: 3 +ssh_client_alive_interval: 300 +ssh_client_alive_count_max: 2 + +# Firewall rules for SSH (trusted networks) +ssh_allowed_networks: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + +# Services to disable +unnecessary_services: + - cups + - avahi-daemon + - bluetooth + - nfs-server + - rpcbind + +# Packages to remove +unnecessary_packages: + - telnet + - rsh-client + - talk + - ntalk diff --git a/professional-infra/linux-operations-automation/roles/hardening/handlers/main.yml b/professional-infra/linux-operations-automation/roles/hardening/handlers/main.yml new file mode 100644 index 0000000..77e6bcc --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/hardening/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart sshd + ansible.builtin.service: + name: sshd + state: restarted diff --git a/professional-infra/linux-operations-automation/roles/hardening/tasks/cis_hardening.yml b/professional-infra/linux-operations-automation/roles/hardening/tasks/cis_hardening.yml new file mode 100644 index 0000000..ca8f30f --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/hardening/tasks/cis_hardening.yml @@ -0,0 +1,7 @@ +--- +# CIS Hardening Level 1 tasks (stub for future expansion) +# https://www.cisecurity.org/cis-benchmarks/ + +- name: Check CIS status + ansible.builtin.debug: + msg: "CIS Hardening Level {{ cis_level }} would be applied here" diff --git a/professional-infra/linux-operations-automation/roles/hardening/tasks/main.yml b/professional-infra/linux-operations-automation/roles/hardening/tasks/main.yml new file mode 100644 index 0000000..9cb7f49 --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/hardening/tasks/main.yml @@ -0,0 +1,95 @@ +--- +- name: Validate hardening requirements + ansible.builtin.assert: + that: + - ansible_os_family == "Debian" + - cis_level in [1, 2] + fail_msg: "Unsupported configuration for hardening" + +- name: Apply CIS hardening tasks + ansible.builtin.include_tasks: cis_hardening.yml + when: cis_level >= 1 + +- name: Configure SSH hardening + block: + - name: Disable root SSH login + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^PermitRootLogin' + line: 'PermitRootLogin no' + state: present + when: disable_root_login + notify: restart sshd + + - name: Disable password authentication + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^PasswordAuthentication' + line: 'PasswordAuthentication no' + state: present + when: secure_ssh_config + notify: restart sshd + + - name: Set MaxAuthTries + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^MaxAuthTries' + line: "MaxAuthTries {{ ssh_max_auth_tries }}" + state: present + notify: restart sshd + + - name: Disable empty passwords + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^PermitEmptyPasswords' + line: 'PermitEmptyPasswords no' + state: present + notify: restart sshd + + - name: Set ClientAliveInterval + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^ClientAliveInterval' + line: "ClientAliveInterval {{ ssh_client_alive_interval }}" + state: present + notify: restart sshd + + - name: Set ClientAliveCountMax + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^ClientAliveCountMax' + line: "ClientAliveCountMax {{ ssh_client_alive_count_max }}" + state: present + notify: restart sshd + +- name: Configure firewall rules + block: + - name: Enable firewall + community.general.ufw: + state: enabled + policy: "{{ firewall_policy }}" + when: firewall_policy is defined + + - name: Allow SSH from trusted networks + community.general.ufw: + rule: allow + port: '22' + proto: tcp + from: "{{ item }}" + loop: "{{ ssh_allowed_networks }}" + +- name: Disable unnecessary services + ansible.builtin.service: + name: "{{ item }}" + state: stopped + enabled: false + loop: "{{ unnecessary_services }}" + failed_when: false + +- name: Remove unnecessary packages + ansible.builtin.apt: + name: "{{ item }}" + state: absent + purge: true + loop: "{{ unnecessary_packages }}" + failed_when: false diff --git a/professional-infra/linux-operations-automation/roles/patching/README.md b/professional-infra/linux-operations-automation/roles/patching/README.md new file mode 100644 index 0000000..ef8d1e6 --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/patching/README.md @@ -0,0 +1,45 @@ +# Patching Role + +Apply security patches and OS updates to enterprise infrastructure nodes. + +## Features + +- **Idempotent**: Properly checks for changes with `changed_when` +- **Patch Window**: Optional enforcement of patch time windows +- **Pre-patch Backup**: Backs up package list before patching +- **Smart Reboot**: Automatically detects if reboot is required +- **Service Restart**: Restarts only necessary services after patching +- **Health Checks**: Verifies services and runs health endpoint checks + +## Role Variables + +See `defaults/main.yml` for all available variables. + +### Key Variables + +- `patch_window_start`: Patch window start time (default: 02:00) +- `patch_window_end`: Patch window end time (default: 04:00) +- `enforce_patch_window`: Enforce patch time window (default: true) +- `patch_security_only`: Apply security updates only (default: true) +- `backup_before_patch`: Create backup before patching (default: true) +- `reboot_if_required`: Auto-reboot if required (default: false) +- `services_to_restart`: Services to restart after patching +- `critical_services`: Critical services to verify after patching + +## Usage + +```yaml +- role: patching + vars: + patch_security_only: true + enforce_patch_window: false + reboot_if_required: true +``` + +## Report + +Patch report is generated at: `/var/log/patch_report_.log` + +## Backup Location + +Pre-patch backups saved to: `/var/backups/pre-patch-/` diff --git a/professional-infra/linux-operations-automation/roles/patching/defaults/main.yml b/professional-infra/linux-operations-automation/roles/patching/defaults/main.yml new file mode 100644 index 0000000..a64c401 --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/patching/defaults/main.yml @@ -0,0 +1,20 @@ +--- +# Patching configuration +patch_window_start: "02:00" +patch_window_end: "04:00" +enforce_patch_window: true +patch_security_only: true +backup_before_patch: true +reboot_if_required: false +reboot_timeout: 300 + +# Services to restart after patching +services_to_restart: + - sshd + - fail2ban + +# Services to verify after patching +critical_services: + - systemd-journald + - systemd-logind + - cron diff --git a/professional-infra/linux-operations-automation/roles/patching/handlers/main.yml b/professional-infra/linux-operations-automation/roles/patching/handlers/main.yml new file mode 100644 index 0000000..5e3b2f3 --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/patching/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart patching services + ansible.builtin.service: + name: "{{ item }}" + state: restarted + loop: "{{ services_to_restart }}" diff --git a/professional-infra/linux-operations-automation/roles/patching/tasks/main.yml b/professional-infra/linux-operations-automation/roles/patching/tasks/main.yml new file mode 100644 index 0000000..34c72a4 --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/patching/tasks/main.yml @@ -0,0 +1,105 @@ +--- +- name: Validate patch window + when: enforce_patch_window | bool + block: + - name: Check current time against patch window + ansible.builtin.assert: + that: + - ansible_date_time.hour | int >= patch_window_start.split(':')[0] | int + - ansible_date_time.hour | int < patch_window_end.split(':')[0] | int + fail_msg: | + Current time {{ ansible_date_time.hour }}:{{ ansible_date_time.minute }} is outside patch window {{ patch_window_start }}-{{ patch_window_end }} + +- name: Create pre-patch backup + when: backup_before_patch | bool + block: + - name: Create backup directory + ansible.builtin.file: + path: "/var/backups/pre-patch-{{ ansible_date_time.iso8601 }}" + state: directory + mode: '0755' + + - name: Capture current package list + ansible.builtin.shell: | + set -o pipefail + dpkg --get-selections > /var/backups/pre-patch-{{ ansible_date_time.iso8601 }}/packages.list + changed_when: false + +- name: Check for available updates + ansible.builtin.shell: | + set -o pipefail + apt list --upgradable 2>/dev/null | grep -v "Listing..." | wc -l + register: updates_available_count + changed_when: false + failed_when: false + +- name: Update package cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 300 + changed_when: false + +- name: Check if reboot required before patching + ansible.builtin.stat: + path: /var/run/reboot-required + register: reboot_required_before + changed_when: false + +- name: Apply security updates + ansible.builtin.apt: + upgrade: dist + update_cache: true + when: patch_security_only | bool + register: apt_update_result + notify: restart patching services + +- name: Apply all available updates + ansible.builtin.apt: + upgrade: full + update_cache: true + when: not (patch_security_only | bool) + register: apt_update_result + notify: restart patching services + +- name: Check if reboot required after patching + ansible.builtin.stat: + path: /var/run/reboot-required + register: reboot_required_after + changed_when: false + +- name: Verify critical services are running + ansible.builtin.service: + name: "{{ item }}" + state: started + enabled: true + loop: "{{ critical_services }}" + failed_when: false + +- name: Run post-patch health checks + ansible.builtin.uri: + url: http://localhost/health + method: GET + status_code: 200 + register: health_check + failed_when: false + ignore_errors: true + when: "'webservers' in group_names" + +- name: Set reboot required flag + ansible.builtin.set_fact: + reboot_required: "{{ reboot_required_after.stat.exists | default(false) }}" + +- name: Perform system reboot if required + ansible.builtin.reboot: + msg: "Rebooting after security patches" + timeout: "{{ reboot_timeout }}" + when: reboot_required and reboot_if_required | bool + +- name: Generate patching report + ansible.builtin.template: + src: patch_report.j2 + dest: /var/log/patch_report_{{ ansible_date_time.iso8601 }}.log + mode: '0644' + vars: + updates_applied_count: "{{ apt_update_result.changed | ternary('Yes', 'No') }}" + reboot_required_flag: "{{ reboot_required }}" diff --git a/professional-infra/linux-operations-automation/roles/patching/templates/patch_report.j2 b/professional-infra/linux-operations-automation/roles/patching/templates/patch_report.j2 new file mode 100644 index 0000000..1b63c67 --- /dev/null +++ b/professional-infra/linux-operations-automation/roles/patching/templates/patch_report.j2 @@ -0,0 +1,10 @@ +Patching Report +=============== +Generated: {{ ansible_date_time.iso8601 }} +Host: {{ inventory_hostname }} + +Updates Applied: {{ updates_applied_count }} +Reboot Required: {{ reboot_required_flag }} +Services Restarted: {{ services_to_restart | join(', ') }} + +Backup Location: /var/backups/pre-patch-{{ ansible_date_time.iso8601 }}/ diff --git a/professional-infra/linux-operations-automation/scenarios/failure_patch.md b/professional-infra/linux-operations-automation/scenarios/failure_patch.md new file mode 100644 index 0000000..0a2b70c --- /dev/null +++ b/professional-infra/linux-operations-automation/scenarios/failure_patch.md @@ -0,0 +1,21 @@ +# Scenario: Simulate Failure and Patch + +## Description + +Validate that a service-level failure can be detected, recovered, and followed by a controlled patch workflow. This mirrors a maintenance window where a degraded node is stabilized before package updates are applied. + +## Commands + +```bash +cd professional-infra/linux-operations-automation +./scripts/simulate_failure.sh service 30 web +ansible-playbook -i inventory/hosts.ini playbooks/patch.yml +ansible-playbook -i inventory/hosts.ini playbooks/hardening.yml --check +``` + +## Expected Result + +- The simulation records a temporary service failure. +- The service is restored after cleanup. +- The patch playbook completes without unreachable hosts. +- Hardening check mode reports no destructive changes. diff --git a/professional-infra/linux-operations-automation/scenarios/scaling_event.yml b/professional-infra/linux-operations-automation/scenarios/scaling_event.yml new file mode 100644 index 0000000..68a23ac --- /dev/null +++ b/professional-infra/linux-operations-automation/scenarios/scaling_event.yml @@ -0,0 +1,116 @@ +--- +- name: Enterprise Scaling Event Scenario + hosts: all + become: yes + gather_facts: yes + vars: + scaling_threshold: 80 + cooldown_period: 300 + max_scale_up: 5 + min_instances: 2 + + pre_tasks: + - name: Log scenario start + lineinfile: + path: "/var/log/scaling_scenario.log" + line: "{{ ansible_date_time.iso8601 }} - Starting scaling event scenario" + create: yes + + - name: Check current load + command: uptime + register: system_load + changed_when: false + + - name: Parse load average + set_fact: + load_1min: "{{ system_load.stdout.split(',')[0].split()[-1] | float }}" + load_5min: "{{ system_load.stdout.split(',')[1] | float }}" + load_15min: "{{ system_load.stdout.split(',')[2] | float }}" + + tasks: + - name: Evaluate scaling conditions + set_fact: + scale_up_needed: "{{ load_5min > scaling_threshold }}" + scale_down_needed: "{{ load_5min < (scaling_threshold * 0.3) }}" + + - name: Scale up web servers + include_role: + name: scale_up + tasks_from: web_servers + vars: + scale_count: "{{ [max_scale_up, (load_5min / 10) | int] | min }}" + when: scale_up_needed and "'webservers' in group_names" + + - name: Scale up database servers + include_role: + name: scale_up + tasks_from: database_servers + vars: + scale_count: "{{ [2, (load_5min / 20) | int] | min }}" + when: scale_up_needed and "'databases' in group_names" + + - name: Update load balancer configuration + include_role: + name: load_balancer + tasks_from: update_backends + when: scale_up_needed + + - name: Scale down web servers + include_role: + name: scale_down + tasks_from: web_servers + vars: + scale_count: "{{ [(inventory_hostname | regex_findall('[0-9]+') | first | int) - min_instances, 1] | max }}" + when: scale_down_needed and "'webservers' in group_names" and (inventory_hostname | regex_findall('[0-9]+') | first | int) > min_instances + + - name: Wait for cooldown period + pause: + seconds: "{{ cooldown_period }}" + when: scale_up_needed or scale_down_needed + + - name: Verify scaling results + uri: + url: http://localhost/health + method: GET + status_code: 200 + register: health_check + until: health_check.status == 200 + retries: 5 + delay: 10 + when: "'webservers' in group_names" + + - name: Update monitoring thresholds + include_role: + name: monitoring + tasks_from: update_alerts + vars: + new_threshold: "{{ scaling_threshold + 10 }}" + + - name: Send scaling notification + mail: + to: "{{ scaling_notification_email | default('infra-team@company.com') }}" + subject: "Infrastructure Scaling Event - {{ inventory_hostname }}" + body: | + Scaling event completed on {{ inventory_hostname }} + + Load averages: {{ load_1min }}, {{ load_5min }}, {{ load_15min }} + Action taken: {{ 'Scale Up' if scale_up_needed else 'Scale Down' if scale_down_needed else 'No Action' }} + Health check: {{ 'PASSED' if health_check.status == 200 else 'FAILED' }} + + See /var/log/scaling_scenario.log for details + when: scaling_notification_email is defined + ignore_errors: yes + + post_tasks: + - name: Generate scaling scenario report + template: + src: templates/scaling_scenario_report.j2 + dest: "/var/log/scaling_scenario_report_{{ ansible_date_time.iso8601 }}.log" + vars: + scenario_outcome: "{{ 'SUCCESS' if health_check.status == 200 else 'WARNING' }}" + load_metrics: "{{ load_1min }}, {{ load_5min }}, {{ load_15min }}" + + - name: Log scenario completion + lineinfile: + path: "/var/log/scaling_scenario.log" + line: "{{ ansible_date_time.iso8601 }} - Scaling event scenario completed" \ No newline at end of file diff --git a/professional-infra/linux-operations-automation/scripts/simulate_failure.sh b/professional-infra/linux-operations-automation/scripts/simulate_failure.sh new file mode 100644 index 0000000..dd11a9d --- /dev/null +++ b/professional-infra/linux-operations-automation/scripts/simulate_failure.sh @@ -0,0 +1,388 @@ +#!/bin/bash + +# Enterprise Infrastructure Failure Simulation Script +# Simulates various types of infrastructure failures for testing + +set -euo pipefail + +# Configuration +DOCKER_COMPOSE_FILE="docker-compose.yml" +INVENTORY_FILE="inventory/hosts.ini" +LOG_FILE="logs/failure_simulation.log" + +# Default values +FAILURE_TYPE="${1:-network}" +DURATION="${2:-60}" +TARGET_NODES="${3:-all}" +INTENSITY="${INTENSITY:-medium}" + +# Logging function +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') - $*" | tee -a "$LOG_FILE" +} + +# Error handling +error_exit() { + log "ERROR: $1" + # Cleanup any active failures + cleanup_failure + exit 1 +} + +# Validate inputs +validate_inputs() { + case "$FAILURE_TYPE" in + network|disk|service|node|cpu|memory) ;; + *) error_exit "Invalid failure type: $FAILURE_TYPE. Must be network, disk, service, node, cpu, or memory" ;; + esac + + if ! [[ "$DURATION" =~ ^[0-9]+$ ]] || [ "$DURATION" -lt 1 ]; then + error_exit "Invalid duration: $DURATION. Must be a positive integer (seconds)" + fi + + case "$INTENSITY" in + low|medium|high|critical) ;; + *) error_exit "Invalid intensity: $INTENSITY. Must be low, medium, high, or critical" ;; + esac +} + +# Get target containers +get_target_containers() { + if [ "${SIMULATION_MODE:-false}" = true ]; then + case "$TARGET_NODES" in + all) echo "web db lb" ;; + *) echo "$TARGET_NODES" ;; + esac + return + fi + + case "$TARGET_NODES" in + all) + docker compose ps --services | grep -v "^NAME$" || true + ;; + web) + echo "web" + ;; + db) + echo "db" + ;; + lb) + echo "lb" + ;; + monitor) + echo "monitor" + ;; + *) + echo "$TARGET_NODES" + ;; + esac +} + +# Network failure simulation +simulate_network_failure() { + local containers=$(get_target_containers) + log "Simulating network failure on containers: $containers" + + if [ "${SIMULATION_MODE:-false}" = true ]; then + log "SIMULATION_MODE=true: skipping Docker network changes" + return + fi + + for container in $containers; do + local container_ids=$(docker compose ps -q "$container" 2>/dev/null || true) + + for cid in $container_ids; do + if [ -n "$cid" ]; then + log "Disconnecting network for container $cid" + + # Disconnect from network + docker network disconnect "$(docker inspect "$cid" --format '{{.HostConfig.NetworkMode}}')" "$cid" 2>/dev/null || true + + # Store original network for restoration + echo "$cid:$(docker inspect "$cid" --format '{{.HostConfig.NetworkMode}}')" >> /tmp/network_failure_state + fi + done + done +} + +# Disk failure simulation +simulate_disk_failure() { + local containers=$(get_target_containers) + log "Simulating disk space exhaustion on containers: $containers" + + if [ "${SIMULATION_MODE:-false}" = true ]; then + log "SIMULATION_MODE=true: skipping container disk writes" + return + fi + + for container in $containers; do + local container_ids=$(docker compose ps -q "$container" 2>/dev/null || true) + + for cid in $container_ids; do + if [ -n "$cid" ]; then + log "Filling disk space in container $cid" + + # Create a large file to consume disk space + local fill_size_mb=100 + case "$INTENSITY" in + low) fill_size_mb=50 ;; + medium) fill_size_mb=100 ;; + high) fill_size_mb=500 ;; + critical) fill_size_mb=1024 ;; + esac + + docker exec "$cid" bash -c "dd if=/dev/zero of=/tmp/disk_fill bs=1M count=${fill_size_mb}" 2>/dev/null || true + echo "$cid:disk_fill" >> /tmp/disk_failure_state + fi + done + done +} + +# Service failure simulation +simulate_service_failure() { + local containers=$(get_target_containers) + log "Simulating service failures on containers: $containers" + + if [ "${SIMULATION_MODE:-false}" = true ]; then + for container in $containers; do + log "SIMULATION_MODE=true: would stop services in $container" + done + return + fi + + for container in $containers; do + local container_ids=$(docker compose ps -q "$container" 2>/dev/null || true) + + for cid in $container_ids; do + if [ -n "$cid" ]; then + log "Stopping services in container $cid" + + # Stop common services + docker exec "$cid" systemctl stop nginx 2>/dev/null || true + docker exec "$cid" systemctl stop postgresql 2>/dev/null || true + docker exec "$cid" systemctl stop haproxy 2>/dev/null || true + + echo "$cid:services" >> /tmp/service_failure_state + fi + done + done +} + +# Node failure simulation +simulate_node_failure() { + local containers=$(get_target_containers) + log "Simulating complete node failures on containers: $containers" + + if [ "${SIMULATION_MODE:-false}" = true ]; then + log "SIMULATION_MODE=true: skipping container pause" + return + fi + + for container in $containers; do + local container_ids=$(docker compose ps -q "$container" 2>/dev/null || true) + + for cid in $container_ids; do + if [ -n "$cid" ]; then + log "Stopping container $cid (node failure)" + docker pause "$cid" + echo "$cid:paused" >> /tmp/node_failure_state + fi + done + done +} + +# CPU stress simulation +simulate_cpu_failure() { + local containers=$(get_target_containers) + log "Simulating CPU stress on containers: $containers" + + if [ "${SIMULATION_MODE:-false}" = true ]; then + log "SIMULATION_MODE=true: skipping CPU stress" + return + fi + + for container in $containers; do + local container_ids=$(docker compose ps -q "$container" 2>/dev/null || true) + + for cid in $container_ids; do + if [ -n "$cid" ]; then + log "Starting CPU stress in container $cid" + + # Start CPU stress process + docker exec -d "$cid" bash -c "while true; do :; done" 2>/dev/null || true + echo "$cid:cpu_stress:$(docker exec "$cid" ps aux | grep "while true" | grep -v grep | awk '{print $2}' | head -1)" >> /tmp/cpu_failure_state + fi + done + done +} + +# Memory stress simulation +simulate_memory_failure() { + local containers=$(get_target_containers) + log "Simulating memory exhaustion on containers: $containers" + + if [ "${SIMULATION_MODE:-false}" = true ]; then + log "SIMULATION_MODE=true: skipping memory stress" + return + fi + + for container in $containers; do + local container_ids=$(docker compose ps -q "$container" 2>/dev/null || true) + + for cid in $container_ids; do + if [ -n "$cid" ]; then + log "Starting memory stress in container $cid" + + # Start memory stress process + docker exec -d "$cid" bash -c "tail /dev/zero" 2>/dev/null || true + echo "$cid:memory_stress:$(docker exec "$cid" ps aux | grep "tail /dev/zero" | grep -v grep | awk '{print $2}' | head -1)" >> /tmp/memory_failure_state + fi + done + done +} + +# Inject failure +inject_failure() { + case "$FAILURE_TYPE" in + network) simulate_network_failure ;; + disk) simulate_disk_failure ;; + service) simulate_service_failure ;; + node) simulate_node_failure ;; + cpu) simulate_cpu_failure ;; + memory) simulate_memory_failure ;; + esac +} + +# Cleanup failure +cleanup_failure() { + log "Cleaning up failure simulation" + + # Restore network connections + if [ -f /tmp/network_failure_state ]; then + while IFS=: read -r cid network; do + docker network connect "$network" "$cid" 2>/dev/null || true + done < /tmp/network_failure_state + rm -f /tmp/network_failure_state + fi + + # Clean up disk fill files + if [ -f /tmp/disk_failure_state ]; then + while IFS=: read -r cid _; do + docker exec "$cid" rm -f /tmp/disk_fill 2>/dev/null || true + done < /tmp/disk_failure_state + rm -f /tmp/disk_failure_state + fi + + # Restart services + if [ -f /tmp/service_failure_state ]; then + while IFS=: read -r cid _; do + docker exec "$cid" systemctl start nginx 2>/dev/null || true + docker exec "$cid" systemctl start postgresql 2>/dev/null || true + docker exec "$cid" systemctl start haproxy 2>/dev/null || true + done < /tmp/service_failure_state + rm -f /tmp/service_failure_state + fi + + # Unpause containers + if [ -f /tmp/node_failure_state ]; then + while IFS=: read -r cid _; do + docker unpause "$cid" 2>/dev/null || true + done < /tmp/node_failure_state + rm -f /tmp/node_failure_state + fi + + # Kill stress processes + if [ -f /tmp/cpu_failure_state ]; then + while IFS=: read -r cid _ pid; do + docker exec "$cid" kill -9 "$pid" 2>/dev/null || true + done < /tmp/cpu_failure_state + rm -f /tmp/cpu_failure_state + fi + + if [ -f /tmp/memory_failure_state ]; then + while IFS=: read -r cid _ pid; do + docker exec "$cid" kill -9 "$pid" 2>/dev/null || true + done < /tmp/memory_failure_state + rm -f /tmp/memory_failure_state + fi +} + +# Monitor failure +monitor_failure() { + local end_time=$(( $(date +%s) + DURATION )) + + log "Monitoring failure for $DURATION seconds" + + while [ $(date +%s) -lt $end_time ]; do + # Check container status + if [ "${SIMULATION_MODE:-false}" = true ]; then + log "SIMULATION_MODE=true: validation simulated" + return + fi + + if ! docker compose ps | grep -q "Up\|Paused"; then + log "WARNING: All containers are down" + fi + + # Log system metrics + log "System status: $(docker stats --no-stream --format 'table {{.Container}}\t{{.CPUPerc}}\t{{.MemPerc}}' | tail -n +2)" + + sleep 10 + done +} + +# Generate failure report +generate_report() { + local report_file="reports/failure_simulation_$(date +%Y%m%d_%H%M%S).txt" + + cat > "$report_file" << EOF +Failure Simulation Report +======================== + +Timestamp: $(date) +Failure Type: $FAILURE_TYPE +Duration: $DURATION seconds +Target Nodes: $TARGET_NODES +Intensity: $INTENSITY + +Pre-failure Status: +$(docker compose ps 2>/dev/null || echo "Docker Compose not running") + +Post-failure Status: +$(docker compose ps 2>/dev/null || echo "Docker Compose not running") + +Log File: $LOG_FILE +EOF + + log "Failure simulation report generated: $report_file" +} + +# Main execution +main() { + log "Starting failure simulation: $FAILURE_TYPE for $DURATION seconds" + + validate_inputs + + # Inject failure + inject_failure + + # Monitor during failure + monitor_failure + + # Cleanup + cleanup_failure + + # Generate report + generate_report + + log "Failure simulation completed successfully" +} + +# Trap for cleanup on script exit +trap cleanup_failure EXIT + +# Initialize logging +mkdir -p logs reports + +# Run main function +main "$@" diff --git a/professional-infra/linux-operations-automation/scripts/simulate_scaling.sh b/professional-infra/linux-operations-automation/scripts/simulate_scaling.sh new file mode 100644 index 0000000..86a2872 --- /dev/null +++ b/professional-infra/linux-operations-automation/scripts/simulate_scaling.sh @@ -0,0 +1,229 @@ +#!/bin/bash + +# Enterprise Infrastructure Scaling Simulation Script +# Simulates scaling operations for infrastructure nodes + +set -euo pipefail + +# Configuration +DOCKER_COMPOSE_FILE="docker-compose.yml" +INVENTORY_FILE="inventory/hosts.ini" +LOG_FILE="logs/scaling_simulation.log" + +# Default values +DIRECTION="${1:-up}" +COUNT="${2:-1}" +NODE_TYPE="${3:-web}" +SIMULATION_MODE="${SIMULATION_MODE:-false}" + +# Logging function +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') - $*" | tee -a "$LOG_FILE" +} + +# Error handling +error_exit() { + log "ERROR: $1" + exit 1 +} + +# Validate inputs +validate_inputs() { + if [[ "$DIRECTION" != "up" && "$DIRECTION" != "down" ]]; then + error_exit "Invalid direction: $DIRECTION. Must be 'up' or 'down'" + fi + + if ! [[ "$COUNT" =~ ^[0-9]+$ ]] || [ "$COUNT" -lt 1 ]; then + error_exit "Invalid count: $COUNT. Must be a positive integer" + fi + + case "$NODE_TYPE" in + web|db|lb|monitor) ;; + *) error_exit "Invalid node type: $NODE_TYPE. Must be web, db, lb, or monitor" ;; + esac +} + +# Get current node count +get_current_count() { + local type="$1" + if [ "$SIMULATION_MODE" = true ]; then + case "$type" in + web) echo 3 ;; + db) echo 2 ;; + lb|monitor) echo 1 ;; + esac + return + fi + + case "$type" in + web) docker compose ps web | grep -c "Up" ;; + db) docker compose ps db | grep -c "Up" ;; + lb) docker compose ps lb | grep -c "Up" ;; + monitor) docker compose ps monitor | grep -c "Up" ;; + esac +} + +# Scale up infrastructure +scale_up() { + local type="$1" + local count="$2" + + log "Scaling up $count $type nodes" + + if [ "$SIMULATION_MODE" = true ]; then + log "SIMULATION_MODE=true: skipping Docker Compose mutation and Ansible provisioning" + update_inventory "$type" "$count" "add" + log "Successfully simulated scale up of $count $type nodes" + return + fi + + docker compose -f "$DOCKER_COMPOSE_FILE" up -d --scale "${type}=${count}" + + # Wait for containers to be ready + log "Waiting for containers to be ready..." + sleep 30 + + # Update inventory + update_inventory "$type" "$count" "add" + + # Run provisioning playbook on new nodes + if [ "$SIMULATION_MODE" = false ]; then + ansible-playbook -i "$INVENTORY_FILE" playbooks/provision.yml --limit "${type}*" + fi + + log "Successfully scaled up $count $type nodes" +} + +# Scale down infrastructure +scale_down() { + local type="$1" + local count="$2" + + local current_count=$(get_current_count "$type") + if [ "$current_count" -lt "$count" ]; then + error_exit "Cannot scale down $count nodes. Only $current_count $type nodes currently running" + fi + + log "Scaling down $count $type nodes" + + # Select nodes to remove (oldest first) + if [ "$SIMULATION_MODE" = true ]; then + log "SIMULATION_MODE=true: skipping Docker Compose mutation and Ansible decommissioning" + update_inventory "$type" "$count" "remove" + log "Successfully simulated scale down of $count $type nodes" + return + fi + + local nodes_to_remove=$(docker compose ps "$type" | grep "Up" | head -n "$count" | awk '{print $1}') + + # Decommission nodes + for node in $nodes_to_remove; do + if [ "$SIMULATION_MODE" = false ]; then + ansible-playbook -i "$INVENTORY_FILE" playbooks/decommission.yml --limit "$node" + fi + docker stop "$node" + docker rm "$node" + done + + # Update inventory + update_inventory "$type" "$count" "remove" + + log "Successfully scaled down $count $type nodes" +} + +# Update Ansible inventory +update_inventory() { + local type="$1" + local count="$2" + local action="$3" + + log "Updating inventory for $action $count $type nodes" + + # This would be more complex in a real implementation + # For simulation, we'll just log the action + case "$action" in + add) + log "Added $count $type nodes to inventory" + ;; + remove) + log "Removed $count $type nodes from inventory" + ;; + esac +} + +# Health check after scaling +health_check() { + log "Running health checks after scaling" + + # Check container status + if [ "$SIMULATION_MODE" = true ]; then + log "SIMULATION_MODE=true: health checks simulated" + return + fi + + if ! docker compose ps | grep -q "Up"; then + error_exit "Some containers failed to start" + fi + + # Ansible ping check + if [ "$SIMULATION_MODE" = false ]; then + if ! ansible -i "$INVENTORY_FILE" all -m ping >/dev/null 2>&1; then + log "WARNING: Some nodes failed Ansible ping check" + fi + fi + + log "Health checks completed" +} + +# Generate scaling report +generate_report() { + local report_file="reports/scaling_report_$(date +%Y%m%d_%H%M%S).txt" + + cat > "$report_file" << EOF +Scaling Simulation Report +======================== + +Timestamp: $(date) +Direction: $DIRECTION +Node Type: $NODE_TYPE +Count: $COUNT +Simulation Mode: $SIMULATION_MODE + +Current Status: +$(docker compose ps 2>/dev/null || echo "Docker Compose not running") + +Inventory Status: +$(ansible -i "$INVENTORY_FILE" --list-hosts all 2>/dev/null || echo "Ansible inventory check failed") + +Log File: $LOG_FILE +EOF + + log "Scaling report generated: $report_file" +} + +# Main execution +main() { + log "Starting scaling simulation: $DIRECTION $COUNT $NODE_TYPE nodes" + + validate_inputs + + case "$DIRECTION" in + up) + scale_up "$NODE_TYPE" "$COUNT" + ;; + down) + scale_down "$NODE_TYPE" "$COUNT" + ;; + esac + + health_check + generate_report + + log "Scaling simulation completed successfully" +} + +# Initialize logging +mkdir -p logs reports + +# Run main function +main "$@" diff --git a/professional-infra/log-observability-elk-grafana/Makefile b/professional-infra/log-observability-elk-grafana/Makefile new file mode 100644 index 0000000..b4a44c6 --- /dev/null +++ b/professional-infra/log-observability-elk-grafana/Makefile @@ -0,0 +1,20 @@ +.PHONY: run test demo down + +run: + docker compose up -d + +test: + docker compose config --quiet + test -f elasticsearch/config/elasticsearch.yml + test -f logstash/config/logstash.yml + test -f logstash/pipeline/logstash.conf + test -f kibana/config/kibana.yml + test -f filebeat/config/filebeat.yml + test -d grafana/provisioning + test -d grafana/dashboards + +demo: + bash ./scenarios/incident_simulation.sh app-errors 3 + +down: + docker compose down diff --git a/professional-infra/log-observability-elk-grafana/README.md b/professional-infra/log-observability-elk-grafana/README.md new file mode 100644 index 0000000..3ef5e88 --- /dev/null +++ b/professional-infra/log-observability-elk-grafana/README.md @@ -0,0 +1,98 @@ +# Log Observability ELK/Grafana + +## Problem + +Operations teams need searchable logs and reviewable incident evidence in addition to simple OS checks. Zabbix is useful for host and service health signals; ELK/Grafana is better suited for log ingestion, error analysis, dashboards, and environment-level observability. + +## CV Relevance + +This project supports the monitoring and troubleshooting part of the CV by showing how incident logs can be collected, parsed, searched, and reviewed. It is separate from the Zabbix project: Zabbix handles simple checks, while this project focuses on logs and observability evidence. + +## What This Project Demonstrates + +- A local Docker Compose scaffold for Elasticsearch, Logstash, Kibana, Grafana, and Filebeat. +- Minimal configs required for the stack to validate independently. +- Sample logs and alert intent that can be reviewed without starting the full stack. +- An incident simulation script for generating operational log evidence. + +This is a local demo stack. The default credentials are for non-production use only. + +## Architecture + +``` +Application/System Logs -> Filebeat -> Logstash -> Elasticsearch -> Kibana + | + v + Grafana + +Incident Scenario -> Sample Logs -> Alert Rules -> Operator Review +``` + +Core components: + +- `docker-compose.yml` defines the observability services. +- `alerting/alert_rules.yml` records alert intent and severity. +- `examples/` contains representative operational logs and alert output. +- `scenarios/incident_simulation.sh` emits incident activity. +- `grafana/`, `kibana/`, `logstash/`, `filebeat/`, and `elasticsearch/` contain minimal local configs. + +## Quickstart + +```bash +cd professional-infra/log-observability-elk-grafana +make test +make demo +``` + +Start the full local stack with Docker: + +```bash +make test +make run +make down +``` + +When running locally: + +- Kibana: `http://localhost:5601` +- Grafana: `http://localhost:3000` +- Elasticsearch: `http://localhost:9200` + +Default demo credentials: + +- Elasticsearch/Kibana: `elastic` / `elastic` +- Grafana: `admin` / `admin` + +## Validation + +```bash +make test +docker compose config --quiet +``` + +`make test` also checks that all bind-mounted config files and directories exist. + +## Example Output + +```text +[2026-04-29 04:18:23] WARN Database connection pool nearing capacity +[2026-04-29 04:18:28] ERROR Database connection pool exhausted +[2026-04-29 04:18:33] ERROR Database query timeout occurred +[2026-04-29 04:18:44] INFO Database connections restored +``` + +Additional examples are available in [examples/alert-output.txt](examples/alert-output.txt) and [examples/sample-log.txt](examples/sample-log.txt). + +## Interview Talking Points + +- When to use Zabbix checks versus ELK log analysis. +- How Filebeat, Logstash, and Elasticsearch fit into a basic log pipeline. +- How incident simulations create evidence for troubleshooting discussions. +- Why local demo credentials and single-node Elasticsearch are not production architecture. + +## Roadmap + +- Add curated Grafana and Kibana dashboards. +- Add Prometheus metrics collection. +- Add distributed tracing with Jaeger or OpenTelemetry. +- Add synthetic monitoring checks. diff --git a/professional-infra/log-observability-elk-grafana/alerting/alert_rules.yml b/professional-infra/log-observability-elk-grafana/alerting/alert_rules.yml new file mode 100644 index 0000000..1e10138 --- /dev/null +++ b/professional-infra/log-observability-elk-grafana/alerting/alert_rules.yml @@ -0,0 +1,326 @@ +# Enterprise Observability Alert Rules +# Alert definitions for automated incident detection and notification + +alert_rules: + # System Resource Alerts + - name: "High CPU Usage" + description: "CPU utilization exceeds threshold" + condition: "cpu_usage_percent > 90" + duration: "5m" + severity: "critical" + tags: + - system + - performance + channels: + - email + - slack + labels: + team: "platform" + component: "system" + + - name: "High Memory Usage" + description: "Memory utilization exceeds threshold" + condition: "memory_usage_percent > 85" + duration: "3m" + severity: "warning" + tags: + - system + - memory + channels: + - email + labels: + team: "platform" + component: "system" + + - name: "Disk Space Critical" + description: "Disk usage exceeds critical threshold" + condition: "disk_usage_percent > 95" + duration: "2m" + severity: "critical" + tags: + - storage + - disk + channels: + - email + - pagerduty + labels: + team: "platform" + component: "storage" + + - name: "Disk Space Warning" + description: "Disk usage exceeds warning threshold" + condition: "disk_usage_percent > 85" + duration: "10m" + severity: "warning" + tags: + - storage + - disk + channels: + - email + labels: + team: "platform" + component: "storage" + + # Service Availability Alerts + - name: "Service Down" + description: "Critical service is not responding" + condition: "service_status == 'down' OR http_status_code >= 500" + duration: "2m" + severity: "critical" + tags: + - service + - availability + channels: + - email + - slack + - pagerduty + labels: + team: "application" + component: "service" + + - name: "Database Connection Failed" + description: "Database connection pool exhausted or unresponsive" + condition: "db_connections_active == 0 OR db_response_time > 5000" + duration: "1m" + severity: "critical" + tags: + - database + - connectivity + channels: + - email + - pagerduty + labels: + team: "database" + component: "postgresql" + + - name: "Cache Unavailable" + description: "Cache service is down or unresponsive" + condition: "cache_hit_ratio < 0.1 OR cache_response_time > 1000" + duration: "3m" + severity: "warning" + tags: + - cache + - performance + channels: + - email + labels: + team: "infrastructure" + component: "redis" + + # Application Performance Alerts + - name: "High Error Rate" + description: "Application error rate exceeds threshold" + condition: "error_rate_percent > 5" + duration: "5m" + severity: "critical" + tags: + - application + - errors + channels: + - email + - slack + labels: + team: "application" + component: "api" + + - name: "Slow Response Time" + description: "API response time exceeds SLA" + condition: "response_time_p95 > 2000" + duration: "5m" + severity: "warning" + tags: + - application + - performance + channels: + - email + labels: + team: "application" + component: "api" + + - name: "High Request Queue" + description: "Request queue depth is too high" + condition: "queue_depth > 100" + duration: "3m" + severity: "warning" + tags: + - application + - queue + channels: + - email + labels: + team: "application" + component: "queue" + + # Infrastructure Alerts + - name: "Network Latency High" + description: "Network round-trip time exceeds threshold" + condition: "network_rtt > 100" + duration: "5m" + severity: "warning" + tags: + - network + - latency + channels: + - email + labels: + team: "network" + component: "infrastructure" + + - name: "Load Balancer Unhealthy" + description: "Load balancer backend servers are unhealthy" + condition: "lb_unhealthy_backends > 0" + duration: "2m" + severity: "critical" + tags: + - loadbalancer + - availability + channels: + - email + - pagerduty + labels: + team: "infrastructure" + component: "loadbalancer" + + # Security Alerts + - name: "Failed Login Attempts" + description: "Multiple failed authentication attempts detected" + condition: "failed_login_attempts > 5" + duration: "5m" + severity: "warning" + tags: + - security + - authentication + channels: + - email + - slack + labels: + team: "security" + component: "authentication" + + - name: "Suspicious Network Traffic" + description: "Unusual network traffic patterns detected" + condition: "network_bytes_unusual > 1000000" + duration: "10m" + severity: "warning" + tags: + - security + - network + channels: + - email + labels: + team: "security" + component: "network" + + # Log-based Alerts + - name: "Application Errors" + description: "High volume of application error logs" + condition: "log_errors_per_minute > 10" + duration: "2m" + severity: "warning" + tags: + - logs + - errors + channels: + - email + labels: + team: "application" + component: "logs" + + - name: "Out of Memory Errors" + description: "Out of memory errors detected in logs" + condition: "log_oom_errors > 0" + duration: "1m" + severity: "critical" + tags: + - memory + - errors + channels: + - email + - pagerduty + labels: + team: "application" + component: "memory" + + # Business Logic Alerts + - name: "Low Business Transactions" + description: "Business transaction volume below expected threshold" + condition: "business_transactions_per_hour < 100" + duration: "15m" + severity: "warning" + tags: + - business + - transactions + channels: + - email + labels: + team: "business" + component: "transactions" + + - name: "Payment Failures" + description: "Payment processing failure rate is high" + condition: "payment_failure_rate > 0.05" + duration: "5m" + severity: "critical" + tags: + - payments + - business + channels: + - email + - pagerduty + labels: + team: "payments" + component: "processing" + +# Alert Channels Configuration +alert_channels: + email: + type: "email" + recipients: + - "platform-team@company.com" + - "oncall@company.com" + subject_template: "[{{severity}}] {{name}} - {{description}}" + + slack: + type: "slack" + webhook_url: "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK" + channel: "#alerts" + username: "Observability Bot" + icon_emoji: ":warning:" + + pagerduty: + type: "pagerduty" + integration_key: "your-pagerduty-integration-key" + severity_mapping: + critical: "critical" + warning: "warning" + info: "info" + +# Alert Silencing Rules +silence_rules: + - name: "Maintenance Window" + condition: "maintenance_window == true" + duration: "4h" + comment: "Silenced during scheduled maintenance" + + - name: "Known Issue" + condition: "known_issue_id == 'TICKET-123'" + duration: "24h" + comment: "Silenced for known issue resolution" + +# Escalation Policies +escalation_policies: + - name: "Default Escalation" + steps: + - delay: "5m" + channels: ["email"] + - delay: "15m" + channels: ["slack"] + - delay: "30m" + channels: ["pagerduty"] + + - name: "Critical Escalation" + steps: + - delay: "0m" + channels: ["email", "slack", "pagerduty"] + - delay: "10m" + channels: ["pagerduty"] # Escalation \ No newline at end of file diff --git a/professional-infra/log-observability-elk-grafana/docker-compose.yml b/professional-infra/log-observability-elk-grafana/docker-compose.yml new file mode 100644 index 0000000..15f5e47 --- /dev/null +++ b/professional-infra/log-observability-elk-grafana/docker-compose.yml @@ -0,0 +1,120 @@ +services: + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0 + container_name: observability-elasticsearch + environment: + - discovery.type=single-node + - xpack.security.enabled=true + - ELASTIC_PASSWORD=elastic + - "ES_JAVA_OPTS=-Xms1g -Xmx1g" + volumes: + - elasticsearch_data:/usr/share/elasticsearch/data + - ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml + ports: + - "9200:9200" + - "9300:9300" + networks: + - observability + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "curl -u elastic:elastic -f http://localhost:9200/_cluster/health || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + + logstash: + image: docker.elastic.co/logstash/logstash:8.11.0 + container_name: observability-logstash + environment: + - "LS_JAVA_OPTS=-Xms512m -Xmx512m" + volumes: + - ./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml + - ./logstash/pipeline:/usr/share/logstash/pipeline + - ./logs:/usr/share/logstash/logs + ports: + - "5044:5044" + - "8080:8080" + networks: + - observability + depends_on: + elasticsearch: + condition: service_healthy + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:9600/_node/pipelines || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + + kibana: + image: docker.elastic.co/kibana/kibana:8.11.0 + container_name: observability-kibana + environment: + - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 + - ELASTICSEARCH_USERNAME=elastic + - ELASTICSEARCH_PASSWORD=elastic + volumes: + - ./kibana/config/kibana.yml:/usr/share/kibana/config/kibana.yml + ports: + - "5601:5601" + networks: + - observability + depends_on: + elasticsearch: + condition: service_healthy + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:5601/api/status || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + + grafana: + image: grafana/grafana:10.2.0 + container_name: observability-grafana + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + - ./grafana/dashboards:/var/lib/grafana/dashboards + ports: + - "3000:3000" + networks: + - observability + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + + filebeat: + image: docker.elastic.co/beats/filebeat:8.11.0 + container_name: observability-filebeat + user: root + volumes: + - ./filebeat/config/filebeat.yml:/usr/share/filebeat/filebeat.yml + - ./logs:/var/log/sample + - /var/lib/docker/containers:/var/lib/docker/containers:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - observability + depends_on: + - logstash + restart: unless-stopped + +volumes: + elasticsearch_data: + driver: local + grafana_data: + driver: local + +networks: + observability: + driver: bridge + ipam: + config: + - subnet: 172.25.0.0/16 diff --git a/professional-infra/log-observability-elk-grafana/docs/architecture.md b/professional-infra/log-observability-elk-grafana/docs/architecture.md new file mode 100644 index 0000000..da43610 --- /dev/null +++ b/professional-infra/log-observability-elk-grafana/docs/architecture.md @@ -0,0 +1,30 @@ +# Log Observability ELK/Grafana Architecture + +## Components + +- Filebeat: tails sample and container logs. +- Logstash: receives and processes log events. +- Elasticsearch: stores searchable observability data. +- Kibana: supports log exploration and dashboards. +- Grafana: provides operational dashboards. +- Alert rules: document symptoms, thresholds, and severity. +- Incident simulation: generates controlled failure signals. + +## Data Flow + +``` +Log source -> Filebeat -> Logstash -> Elasticsearch -> Kibana + | + v + Grafana +``` + +Incident exercises follow this flow: + +``` +Operator -> incident_simulation.sh -> logs/incident_simulation.log -> Filebeat -> Logstash -> alerts/dashboards +``` + +## Notes + +This is a local demonstration stack, not a production Elasticsearch deployment. A production version would add dedicated nodes, TLS, secret management, retention policies, index lifecycle management, and external alert delivery. diff --git a/professional-infra/log-observability-elk-grafana/elasticsearch/config/elasticsearch.yml b/professional-infra/log-observability-elk-grafana/elasticsearch/config/elasticsearch.yml new file mode 100644 index 0000000..b0117da --- /dev/null +++ b/professional-infra/log-observability-elk-grafana/elasticsearch/config/elasticsearch.yml @@ -0,0 +1,4 @@ +cluster.name: portfolio-observability +node.name: elasticsearch-demo +network.host: 0.0.0.0 +xpack.security.enabled: true diff --git a/professional-infra/log-observability-elk-grafana/examples/alert-output.txt b/professional-infra/log-observability-elk-grafana/examples/alert-output.txt new file mode 100644 index 0000000..d6f9bb8 --- /dev/null +++ b/professional-infra/log-observability-elk-grafana/examples/alert-output.txt @@ -0,0 +1,4 @@ +2026-04-29T04:19:00Z alert=database_connection_pool_exhausted severity=critical service=checkout-api host=app-web-02 value=100 threshold=95 status=firing +2026-04-29T04:19:30Z alert=api_error_rate_high severity=warning service=checkout-api host=app-web-02 value=7.8 threshold=5.0 status=firing +2026-04-29T04:22:00Z alert=database_connection_pool_exhausted severity=critical service=checkout-api host=app-web-02 value=71 threshold=95 status=resolved +2026-04-29T04:23:15Z alert=api_error_rate_high severity=warning service=checkout-api host=app-web-02 value=1.2 threshold=5.0 status=resolved diff --git a/professional-infra/log-observability-elk-grafana/examples/sample-log.txt b/professional-infra/log-observability-elk-grafana/examples/sample-log.txt new file mode 100644 index 0000000..b1bbc6c --- /dev/null +++ b/professional-infra/log-observability-elk-grafana/examples/sample-log.txt @@ -0,0 +1,5 @@ +2026-04-29T04:18:21Z INFO service=checkout-api host=app-web-02 request_id=8f4b2 path=/checkout status=200 latency_ms=142 +2026-04-29T04:18:28Z WARN service=checkout-api host=app-web-02 event=db_pool_pressure active=92 max=100 +2026-04-29T04:18:33Z ERROR service=checkout-api host=app-web-02 event=db_timeout query=CreateOrder timeout_ms=5000 customer_tier=enterprise +2026-04-29T04:18:39Z ERROR service=checkout-api host=app-web-02 event=payment_retry_exhausted order_id=ord-104288 provider=stripe +2026-04-29T04:18:44Z INFO service=checkout-api host=app-web-02 event=recovery db_pool_active=48 diff --git a/professional-infra/log-observability-elk-grafana/filebeat/config/filebeat.yml b/professional-infra/log-observability-elk-grafana/filebeat/config/filebeat.yml new file mode 100644 index 0000000..69c40e0 --- /dev/null +++ b/professional-infra/log-observability-elk-grafana/filebeat/config/filebeat.yml @@ -0,0 +1,11 @@ +filebeat.inputs: + - type: filestream + id: portfolio-sample-logs + enabled: true + paths: + - /var/log/sample/*.log + +output.logstash: + hosts: ["logstash:5044"] + +logging.level: info diff --git a/professional-infra/log-observability-elk-grafana/grafana/dashboards/README.md b/professional-infra/log-observability-elk-grafana/grafana/dashboards/README.md new file mode 100644 index 0000000..3dbda14 --- /dev/null +++ b/professional-infra/log-observability-elk-grafana/grafana/dashboards/README.md @@ -0,0 +1,3 @@ +# Dashboards + +This directory is reserved for local demo dashboards. The current portfolio scope validates the observability stack scaffold, sample logs, alert intent, and incident simulation without claiming production-ready dashboards. diff --git a/professional-infra/log-observability-elk-grafana/grafana/provisioning/datasources/datasources.yml b/professional-infra/log-observability-elk-grafana/grafana/provisioning/datasources/datasources.yml new file mode 100644 index 0000000..7a46c5b --- /dev/null +++ b/professional-infra/log-observability-elk-grafana/grafana/provisioning/datasources/datasources.yml @@ -0,0 +1,14 @@ +apiVersion: 1 + +datasources: + - name: Elasticsearch + type: elasticsearch + access: proxy + url: http://elasticsearch:9200 + basicAuth: true + basicAuthUser: elastic + jsonData: + index: portfolio-logs-* + timeField: "@timestamp" + secureJsonData: + basicAuthPassword: elastic diff --git a/professional-infra/log-observability-elk-grafana/kibana/config/kibana.yml b/professional-infra/log-observability-elk-grafana/kibana/config/kibana.yml new file mode 100644 index 0000000..301c3a1 --- /dev/null +++ b/professional-infra/log-observability-elk-grafana/kibana/config/kibana.yml @@ -0,0 +1,4 @@ +server.host: 0.0.0.0 +elasticsearch.hosts: ["http://elasticsearch:9200"] +elasticsearch.username: elastic +elasticsearch.password: elastic diff --git a/professional-infra/log-observability-elk-grafana/logstash/config/logstash.yml b/professional-infra/log-observability-elk-grafana/logstash/config/logstash.yml new file mode 100644 index 0000000..52b1fab --- /dev/null +++ b/professional-infra/log-observability-elk-grafana/logstash/config/logstash.yml @@ -0,0 +1,2 @@ +http.host: 0.0.0.0 +pipeline.ecs_compatibility: disabled diff --git a/professional-infra/log-observability-elk-grafana/logstash/pipeline/logstash.conf b/professional-infra/log-observability-elk-grafana/logstash/pipeline/logstash.conf new file mode 100644 index 0000000..8a11bab --- /dev/null +++ b/professional-infra/log-observability-elk-grafana/logstash/pipeline/logstash.conf @@ -0,0 +1,24 @@ +input { + beats { + port => 5044 + } +} + +filter { + grok { + match => { "message" => "\[%{TIMESTAMP_ISO8601:observed_at}\] %{LOGLEVEL:level} %{GREEDYDATA:event_message}" } + tag_on_failure => ["portfolio_parse_failure"] + } +} + +output { + elasticsearch { + hosts => ["http://elasticsearch:9200"] + user => "elastic" + password => "elastic" + index => "portfolio-logs-%{+YYYY.MM.dd}" + } + stdout { + codec => rubydebug + } +} diff --git a/professional-infra/log-observability-elk-grafana/scenarios/incident_simulation.md b/professional-infra/log-observability-elk-grafana/scenarios/incident_simulation.md new file mode 100644 index 0000000..a91d802 --- /dev/null +++ b/professional-infra/log-observability-elk-grafana/scenarios/incident_simulation.md @@ -0,0 +1,21 @@ +# Scenario: Incident Simulation + +## Description + +Generate a controlled application and infrastructure incident so the logging pipeline, alert rules, and dashboards can be reviewed with realistic event timing. + +## Commands + +```bash +cd professional-infra/log-observability-elk-grafana +docker compose config +./scenarios/incident_simulation.sh comprehensive +tail -n 40 logs/incident_simulation.log +``` + +## Expected Result + +- The compose file validates successfully. +- The simulation writes a sequence of CPU, memory, service, database, and application error events. +- Alert examples indicate firing and resolved states. +- Operators can trace incident progression through logs and dashboard queries. diff --git a/professional-infra/log-observability-elk-grafana/scenarios/incident_simulation.sh b/professional-infra/log-observability-elk-grafana/scenarios/incident_simulation.sh new file mode 100755 index 0000000..d9ad8b1 --- /dev/null +++ b/professional-infra/log-observability-elk-grafana/scenarios/incident_simulation.sh @@ -0,0 +1,318 @@ +#!/bin/bash + +# Enterprise Incident Simulation Script +# Simulates various failure scenarios for testing observability stack + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +LOG_FILE="$PROJECT_ROOT/logs/incident_simulation.log" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Logging function +log() { + local level=$1 + local message=$2 + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + echo "[$timestamp] $level $message" >> "$LOG_FILE" + echo -e "${BLUE}[$timestamp]${NC} $level $message" +} + +# Function to simulate CPU spike +simulate_cpu_spike() { + local duration=${1:-60} + log "INFO" "Starting CPU spike simulation for ${duration} seconds" + + # Launch CPU-intensive processes + for i in {1..4}; do + ( + end_time=$((SECONDS + duration)) + while [ $SECONDS -lt $end_time ]; do + # CPU-intensive calculation + result=0 + for j in {1..100000}; do + result=$((result + j)) + done + done + ) & + PIDS[$i]=$! + done + + # Wait for simulation to complete + for pid in "${PIDS[@]}"; do + wait $pid 2>/dev/null || true + done + + log "INFO" "CPU spike simulation completed" +} + +# Function to simulate memory leak +simulate_memory_leak() { + local duration=${1:-30} + log "INFO" "Starting memory leak simulation for ${duration} seconds" + + # Create a process that gradually consumes memory + ( + data="" + end_time=$((SECONDS + duration)) + while [ $SECONDS -lt $end_time ]; do + # Gradually consume memory + data="${data}X" + sleep 0.1 + done + ) & + MEM_PID=$! + + wait $MEM_PID 2>/dev/null || true + log "INFO" "Memory leak simulation completed" +} + +# Function to simulate disk space exhaustion +simulate_disk_full() { + local target_dir=${1:-"/tmp"} + local duration=${2:-30} + log "INFO" "Starting disk space exhaustion simulation in ${target_dir} for ${duration} seconds" + + # Create large files to fill disk space + ( + end_time=$((SECONDS + duration)) + while [ $SECONDS -lt $end_time ]; do + # Create 100MB file + dd if=/dev/zero of="${target_dir}/incident_test_file_$(date +%s).tmp" bs=1M count=100 2>/dev/null || true + sleep 2 + done + ) & + DISK_PID=$! + + wait $DISK_PID 2>/dev/null || true + + # Cleanup test files + rm -f "${target_dir}"/incident_test_file_*.tmp 2>/dev/null || true + log "INFO" "Disk space exhaustion simulation completed and cleaned up" +} + +# Function to simulate network issues +simulate_network_issues() { + local interface=${1:-"lo"} + local duration=${2:-20} + log "INFO" "Starting network issues simulation on ${interface} for ${duration} seconds" + + # Add network delay and packet loss + sudo tc qdisc add dev $interface root netem delay 100ms 50ms loss 10% 2>/dev/null || true + + sleep $duration + + # Remove network simulation + sudo tc qdisc del dev $interface root 2>/dev/null || true + log "INFO" "Network issues simulation completed" +} + +# Function to simulate service crashes +simulate_service_crash() { + local service_name=${1:-"test-service"} + log "INFO" "Starting service crash simulation for ${service_name}" + + # Simulate service going down + log "ERROR" "Service ${service_name} crashed unexpectedly" + sleep 5 + log "INFO" "Service ${service_name} restarted automatically" + + # Simulate multiple crashes + for i in {1..3}; do + sleep 2 + log "ERROR" "Service ${service_name} crashed again (attempt $i)" + sleep 1 + log "INFO" "Service ${service_name} recovered after crash $i" + done +} + +# Function to simulate database issues +simulate_database_issues() { + local duration=${1:-25} + log "INFO" "Starting database issues simulation for ${duration} seconds" + + # Simulate connection pool exhaustion + log "WARN" "Database connection pool nearing capacity" + sleep 5 + log "ERROR" "Database connection pool exhausted" + sleep 5 + log "ERROR" "Database query timeout occurred" + sleep 5 + log "WARN" "Database connections recovering" + sleep 5 + log "INFO" "Database connections restored" + + log "INFO" "Database issues simulation completed" +} + +# Function to simulate application errors +simulate_application_errors() { + local error_count=${1:-10} + log "INFO" "Starting application error simulation (${error_count} errors)" + + for i in $(seq 1 "$error_count"); do + case $((RANDOM % 4)) in + 0) + log "ERROR" "NullPointerException in UserService.getUser($i)" + ;; + 1) + log "ERROR" "TimeoutException: Database query timed out for user ID: $i" + ;; + 2) + log "ERROR" "ValidationException: Invalid input data for request $i" + ;; + 3) + log "ERROR" "IOException: Failed to write to log file" + ;; + esac + sleep $((RANDOM % 3 + 1)) + done + + log "INFO" "Application error simulation completed" +} + +# Function to run comprehensive incident scenario +run_comprehensive_scenario() { + log "INFO" "Starting comprehensive incident scenario simulation" + + # Phase 1: Initial system stress + log "INFO" "Phase 1: System stress simulation" + simulate_cpu_spike 30 & + CPU_PID=$! + simulate_memory_leak 20 & + MEM_PID=$! + + sleep 10 + + # Phase 2: Service degradation + log "INFO" "Phase 2: Service degradation simulation" + simulate_service_crash "web-service" & + SERVICE_PID=$! + + sleep 5 + + # Phase 3: Database issues + log "INFO" "Phase 3: Database issues simulation" + simulate_database_issues 15 & + DB_PID=$! + + # Phase 4: Application errors + log "INFO" "Phase 4: Application error burst" + simulate_application_errors 15 & + APP_PID=$! + + # Phase 5: Infrastructure issues + log "INFO" "Phase 5: Infrastructure issues simulation" + simulate_disk_full "/tmp" 10 & + DISK_PID=$! + + # Wait for all simulations to complete + wait $CPU_PID 2>/dev/null || true + wait $MEM_PID 2>/dev/null || true + wait $SERVICE_PID 2>/dev/null || true + wait $DB_PID 2>/dev/null || true + wait $APP_PID 2>/dev/null || true + wait $DISK_PID 2>/dev/null || true + + log "INFO" "Comprehensive incident scenario completed" +} + +# Function to show usage +show_usage() { + echo "Enterprise Incident Simulation Script" + echo "Usage: $0 [SCENARIO] [OPTIONS]" + echo "" + echo "SCENARIOS:" + echo " cpu [DURATION] - Simulate CPU spike (default: 60s)" + echo " memory [DURATION] - Simulate memory leak (default: 30s)" + echo " disk [DIR] [DURATION] - Simulate disk space exhaustion (default: /tmp, 30s)" + echo " network [INTERFACE] [DURATION] - Simulate network issues (default: lo, 20s)" + echo " service [NAME] - Simulate service crashes (default: test-service)" + echo " database [DURATION] - Simulate database issues (default: 25s)" + echo " app-errors [COUNT] - Simulate application errors (default: 10)" + echo " comprehensive - Run full incident scenario" + echo " all - Run all individual scenarios sequentially" + echo "" + echo "EXAMPLES:" + echo " $0 cpu 120 - CPU spike for 2 minutes" + echo " $0 disk /var/log 45 - Disk full simulation in /var/log for 45 seconds" + echo " $0 comprehensive - Full incident simulation" + echo "" +} + +# Main execution +main() { + local scenario=${1:-"comprehensive"} + + # Create log directory if it doesn't exist + mkdir -p "$(dirname "$LOG_FILE")" + + log "INFO" "Incident simulation script started" + log "INFO" "Scenario: $scenario" + + case $scenario in + "cpu") + simulate_cpu_spike "${2:-60}" + ;; + "memory") + simulate_memory_leak "${2:-30}" + ;; + "disk") + simulate_disk_full "${2:-/tmp}" "${3:-30}" + ;; + "network") + simulate_network_issues "${2:-lo}" "${3:-20}" + ;; + "service") + simulate_service_crash "${2:-test-service}" + ;; + "database") + simulate_database_issues "${2:-25}" + ;; + "app-errors") + simulate_application_errors "${2:-10}" + ;; + "comprehensive") + run_comprehensive_scenario + ;; + "all") + log "INFO" "Running all scenarios sequentially" + simulate_cpu_spike 30 + sleep 5 + simulate_memory_leak 20 + sleep 5 + simulate_disk_full "/tmp" 15 + sleep 5 + simulate_service_crash "test-service" + sleep 5 + simulate_database_issues 15 + sleep 5 + simulate_application_errors 8 + sleep 5 + simulate_network_issues "lo" 10 + ;; + "help"|"-h"|"--help") + show_usage + exit 0 + ;; + *) + echo -e "${RED}Error: Unknown scenario '$scenario'${NC}" + echo "" + show_usage + exit 1 + ;; + esac + + log "INFO" "Incident simulation script completed successfully" + echo -e "${GREEN}Simulation completed. Check logs at: $LOG_FILE${NC}" +} + +# Run main function with all arguments +main "$@" diff --git a/professional-infra/migration-validation-framework/Makefile b/professional-infra/migration-validation-framework/Makefile new file mode 100644 index 0000000..ee8a2ec --- /dev/null +++ b/professional-infra/migration-validation-framework/Makefile @@ -0,0 +1,11 @@ +.PHONY: run test demo + +run: + python3 cli.py --help + +test: + python3 -m py_compile cli.py collectors/*.py validators/*.py reports/*.py + python3 -m unittest discover -s tests + +demo: + python3 cli.py compare examples/before.json examples/after.json --output /tmp/migration-diff.json diff --git a/professional-infra/migration-validation-framework/README.md b/professional-infra/migration-validation-framework/README.md new file mode 100644 index 0000000..7c37822 --- /dev/null +++ b/professional-infra/migration-validation-framework/README.md @@ -0,0 +1,87 @@ +# Migration Validation Framework + +## Problem + +Infrastructure migrations often fail in small, expensive ways: a mount option changes, a service is disabled, or disk usage moves past an operational threshold. Teams need structured evidence that the migrated host still matches the expected operating profile. + +## CV Relevance + +This project maps to storage/platform migration validation work: collecting pre-migration and post-migration state, comparing results, and producing evidence that can be attached to change or migration tickets. + +## What This Project Demonstrates + +- A Python CLI for collecting and comparing system snapshots. +- Modular collectors for mounts, services, and disk usage. +- Risk assessment and validation checks for before/after drift. +- JSON and HTML evidence suitable for migration review. +- Offline tests and examples that run without remote hosts. + +## Architecture + +``` +Operator -> CLI -> Collectors -> JSON Snapshot -> Comparator -> Diff/Report +``` + +Core components: + +- `cli.py` provides collect, compare, snapshot, list, and report commands. +- `collectors/` gathers mounts, services, and disk usage. +- `validators/compare.py` identifies drift and validation failures. +- `reports/` contains report generation helpers with escaped HTML output. +- `examples/` contains realistic before/after evidence. + +## Quickstart + +```bash +cd professional-infra/migration-validation-framework +make test +make demo +``` + +The demo compares the included example snapshots: + +```bash +python3 cli.py compare examples/before.json examples/after.json --output /tmp/migration-diff.json +``` + +The example intentionally returns `FAIL` because it demonstrates a high-risk migration finding. + +Legacy snapshot IDs are still supported: + +```bash +python3 cli.py snapshot --env prod --label pre --systems web01,db01 +python3 cli.py compare prod-pre-20260429_020000 prod-post-20260429_030000 --output change-0429 +``` + +## Validation + +```bash +make test +``` + +This compiles the Python modules and runs unit tests for example comparison, parser behavior, and HTML escaping. + +## Example Output + +```text +Comparison completed: diff.json (FAIL) +Overall risk: high +Total changes: 4 +Failed checks: critical_services_running +Recommendation: restore sshd before production cutover +``` + +Sample inputs and output are available in [examples/before.json](examples/before.json), [examples/after.json](examples/after.json), and [examples/diff.json](examples/diff.json). + +## Roadmap + +- Add database-specific migration checks. +- Add performance baseline comparisons. +- Add a REST API wrapper for CI/CD integration. +- Add compliance-oriented validation profiles. + +## Interview Talking Points + +- Why pre/post migration evidence reduces risk during storage and platform migrations. +- How to separate collection from comparison so evidence can be replayed. +- How drift detection supports change approval and rollback decisions. diff --git a/professional-infra/migration-validation-framework/cli.py b/professional-infra/migration-validation-framework/cli.py new file mode 100644 index 0000000..29bb57c --- /dev/null +++ b/professional-infra/migration-validation-framework/cli.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python3 +""" +Migration Validation Framework - CLI Interface + +A comprehensive tool for validating system migrations through data collection, +snapshot comparison, and automated reporting. +""" + +import argparse +import json +import logging +import sys +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Optional, Any + +# Import framework modules +from collectors import mounts, services, disk_usage +from validators import compare +from reports import html_report + +# Configuration +SNAPSHOTS_DIR = Path("snapshots") +LOGS_DIR = Path("logs") +REPORTS_DIR = Path("reports") + +class MigrationValidator: + """Main migration validation class.""" + + def __init__(self, verbose: bool = False): + self.verbose = verbose + self.ensure_directories() + self.setup_logging() + + def setup_logging(self): + """Configure logging.""" + log_level = logging.DEBUG if self.verbose else logging.INFO + logging.basicConfig( + level=log_level, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(LOGS_DIR / "validation.log"), + logging.StreamHandler(sys.stdout) + ] + ) + self.logger = logging.getLogger(__name__) + + def ensure_directories(self): + """Ensure required directories exist.""" + for directory in [SNAPSHOTS_DIR, LOGS_DIR, REPORTS_DIR]: + directory.mkdir(exist_ok=True) + + def collect_system_data(self, systems: List[str]) -> Dict[str, Any]: + """Collect data from target systems.""" + self.logger.info(f"Collecting data from systems: {systems}") + + snapshot = { + "metadata": { + "timestamp": datetime.now().isoformat(), + "systems": systems, + "version": "1.0" + }, + "data": {} + } + + collectors = [ + ("mounts", mounts.collect), + ("services", services.collect), + ("disk_usage", disk_usage.collect) + ] + + for system in systems: + self.logger.info(f"Collecting data from {system}") + snapshot["data"][system] = {} + + for collector_name, collector_func in collectors: + try: + self.logger.debug(f"Running {collector_name} collector on {system}") + data = collector_func(system) + snapshot["data"][system][collector_name] = data + except Exception as e: + self.logger.error(f"Failed to collect {collector_name} from {system}: {e}") + snapshot["data"][system][collector_name] = {"error": str(e)} + + return snapshot + + def save_snapshot(self, snapshot: Dict[str, Any], label: str, env: str) -> str: + """Save snapshot to disk.""" + snapshot_id = f"{env}-{label}-{datetime.now().strftime('%Y%m%d_%H%M%S')}" + snapshot_file = SNAPSHOTS_DIR / f"{snapshot_id}.json" + + with open(snapshot_file, 'w') as f: + json.dump(snapshot, f, indent=2) + + self.logger.info(f"Snapshot saved: {snapshot_id}") + return snapshot_id + + def load_snapshot(self, snapshot_id: str) -> Dict[str, Any]: + """Load snapshot from disk.""" + snapshot_path = Path(snapshot_id) + snapshot_file = snapshot_path if snapshot_path.exists() else SNAPSHOTS_DIR / f"{snapshot_id}.json" + if not snapshot_file.exists(): + raise FileNotFoundError(f"Snapshot {snapshot_id} not found") + + with open(snapshot_file, 'r') as f: + return json.load(f) + + def collect_to_file(self, output_file: str, systems: List[str]) -> str: + """Collect a snapshot and write it to an explicit file path.""" + snapshot = self.collect_system_data(systems) + with open(output_file, 'w') as f: + json.dump(snapshot, f, indent=2) + f.write("\n") + self.logger.info(f"Snapshot written: {output_file}") + return output_file + + def create_snapshot(self, env: str, label: str, systems: List[str]) -> str: + """Create and save a system snapshot.""" + self.logger.info(f"Creating snapshot for environment: {env}, label: {label}") + + snapshot = self.collect_system_data(systems) + snapshot_id = self.save_snapshot(snapshot, label, env) + + return snapshot_id + + def compare_snapshots(self, snapshot1_id: str, snapshot2_id: str, output_id: str) -> Dict[str, Any]: + """Compare two snapshots.""" + self.logger.info(f"Comparing snapshots: {snapshot1_id} vs {snapshot2_id}") + + snapshot1 = self.load_snapshot(snapshot1_id) + snapshot2 = self.load_snapshot(snapshot2_id) + + comparison = compare.compare_snapshots(snapshot1, snapshot2) + comparison["metadata"] = { + "snapshot1": snapshot1_id, + "snapshot2": snapshot2_id, + "timestamp": datetime.now().isoformat(), + "comparison_id": output_id + } + + # Save comparison results + comparison_file = REPORTS_DIR / f"comparison_{output_id}.json" + with open(comparison_file, 'w') as f: + json.dump(comparison, f, indent=2) + + self.logger.info(f"Comparison saved: {output_id}") + return comparison + + def compare_files(self, before_file: str, after_file: str, output_file: Optional[str] = None) -> Dict[str, Any]: + """Compare two explicit JSON snapshot files.""" + self.logger.info(f"Comparing files: {before_file} vs {after_file}") + + before = self.load_snapshot(before_file) + after = self.load_snapshot(after_file) + comparison = compare.compare_snapshots(before, after) + comparison["metadata"] = { + "before": before_file, + "after": after_file, + "timestamp": datetime.now().isoformat() + } + + if output_file: + with open(output_file, 'w') as f: + json.dump(comparison, f, indent=2) + f.write("\n") + self.logger.info(f"Comparison written: {output_file}") + + return comparison + + def generate_report(self, comparison_id: str, format_type: str, output_file: Optional[str] = None) -> str: + """Generate a report from comparison results.""" + self.logger.info(f"Generating {format_type} report for comparison: {comparison_id}") + + comparison_file = REPORTS_DIR / f"comparison_{comparison_id}.json" + if not comparison_file.exists(): + raise FileNotFoundError(f"Comparison {comparison_id} not found") + + with open(comparison_file, 'r') as f: + comparison = json.load(f) + + if format_type == "html": + if output_file is None: + output_file = f"migration_report_{comparison_id}.html" + html_report.generate(comparison, output_file) + elif format_type == "json": + if output_file is None: + output_file = f"migration_report_{comparison_id}.json" + with open(output_file, 'w') as f: + json.dump(comparison, f, indent=2) + else: + raise ValueError(f"Unsupported format: {format_type}") + + self.logger.info(f"Report generated: {output_file}") + return output_file + +def main(): + """Main CLI entry point.""" + parser = argparse.ArgumentParser( + description="Migration Validation Framework", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Collect pre-migration snapshot + python3 cli.py collect --output before.json --systems web01,db01 + + # Compare snapshot files + python3 cli.py compare before.json after.json --output diff.json + + # Generate HTML report + python3 cli.py report --comparison comparison_001 --format html + """ + ) + + parser.add_argument('--verbose', '-v', action='store_true', help='Enable verbose logging') + parser.add_argument('--dry-run', action='store_true', help='Preview actions without execution') + + subparsers = parser.add_subparsers(dest='command', help='Available commands') + + # Collect command + collect_parser = subparsers.add_parser('collect', help='Collect a system snapshot to a JSON file') + collect_parser.add_argument('--output', required=True, help='Output JSON file') + collect_parser.add_argument('--systems', default='localhost', help='Comma-separated list of systems') + + # Snapshot command + snapshot_parser = subparsers.add_parser('snapshot', help='Create system snapshot') + snapshot_parser.add_argument('--env', required=True, help='Target environment') + snapshot_parser.add_argument('--label', required=True, help='Snapshot label') + snapshot_parser.add_argument('--systems', required=True, help='Comma-separated list of systems') + + # Compare command + compare_parser = subparsers.add_parser('compare', help='Compare two snapshots') + compare_parser.add_argument('snapshot1', help='First snapshot ID') + compare_parser.add_argument('snapshot2', help='Second snapshot ID') + compare_parser.add_argument('--output', help='Output comparison ID or JSON file') + + # Report command + report_parser = subparsers.add_parser('report', help='Generate report from comparison') + report_parser.add_argument('--comparison', required=True, help='Comparison ID') + report_parser.add_argument('--format', choices=['html', 'json'], default='html', help='Report format') + report_parser.add_argument('--output', help='Output file path') + + # List command + list_parser = subparsers.add_parser('list', help='List snapshots or comparisons') + list_parser.add_argument('type', choices=['snapshots', 'comparisons'], help='Type to list') + + args = parser.parse_args() + + if not args.command: + parser.print_help() + return + + # Initialize validator + validator = MigrationValidator(verbose=args.verbose) + + try: + if args.command == 'collect': + systems = [system.strip() for system in args.systems.split(',') if system.strip()] + if args.dry_run: + print(f"DRY RUN: Would collect {systems} into {args.output}") + return + + output_file = validator.collect_to_file(args.output, systems) + print(f"Snapshot written: {output_file}") + + elif args.command == 'snapshot': + systems = args.systems.split(',') + if args.dry_run: + print(f"DRY RUN: Would create snapshot for systems: {systems}") + return + + snapshot_id = validator.create_snapshot(args.env, args.label, systems) + print(f"Snapshot created: {snapshot_id}") + + elif args.command == 'compare': + if args.dry_run: + print(f"DRY RUN: Would compare {args.snapshot1} vs {args.snapshot2}") + return + + output = args.output + if output and output.endswith('.json'): + comparison = validator.compare_files(args.snapshot1, args.snapshot2, output) + result = "PASS" if comparison.get("validation_results", {}).get("passed") else "FAIL" + print(f"Comparison completed: {output} ({result})") + else: + output_id = output or datetime.now().strftime('%Y%m%d_%H%M%S') + comparison = validator.compare_snapshots(args.snapshot1, args.snapshot2, output_id) + result = "PASS" if comparison.get("validation_results", {}).get("passed") else "FAIL" + print(f"Comparison completed: {output_id} ({result})") + + elif args.command == 'report': + if args.dry_run: + print(f"DRY RUN: Would generate {args.format} report for {args.comparison}") + return + + output_file = validator.generate_report(args.comparison, args.format, args.output) + print(f"Report generated: {output_file}") + + elif args.command == 'list': + if args.type == 'snapshots': + snapshots = list(SNAPSHOTS_DIR.glob("*.json")) + if snapshots: + print("Available snapshots:") + for snapshot in sorted(snapshots): + print(f" {snapshot.stem}") + else: + print("No snapshots found") + elif args.type == 'comparisons': + comparisons = list(REPORTS_DIR.glob("comparison_*.json")) + if comparisons: + print("Available comparisons:") + for comparison in sorted(comparisons): + comp_id = comparison.stem.replace('comparison_', '') + print(f" {comp_id}") + else: + print("No comparisons found") + + except Exception as e: + validator.logger.error(f"Command failed: {e}") + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/professional-infra/migration-validation-framework/collectors/disk_usage.py b/professional-infra/migration-validation-framework/collectors/disk_usage.py new file mode 100644 index 0000000..b76dec2 --- /dev/null +++ b/professional-infra/migration-validation-framework/collectors/disk_usage.py @@ -0,0 +1,206 @@ +""" +Disk Usage Data Collector + +Collects disk usage statistics including directory sizes, +file system usage, and largest files information. +""" + +import logging +import shlex +import subprocess +from typing import Dict, Any, List + +logger = logging.getLogger(__name__) + +class DiskUsageCollector: + """Collector for disk usage statistics.""" + + def __init__(self): + self.max_depth = 3 + self.exclude_paths = [ + "/proc", + "/sys", + "/dev", + "/run", + "/tmp", + "/var/log" + ] + + def collect_disk_usage(self, system: str) -> Dict[str, Any]: + """Collect disk usage information from target system.""" + logger.info(f"Collecting disk usage data from {system}") + + try: + # Collect filesystem usage + filesystem_usage = self.collect_filesystem_usage(system) + + # Collect directory sizes + directory_sizes = self.collect_directory_sizes(system) + + # Collect largest files + largest_files = self.collect_largest_files(system) + + return { + "filesystem_usage": filesystem_usage, + "directory_sizes": directory_sizes, + "largest_files": largest_files, + "timestamp": self.get_timestamp(system) + } + + except Exception as e: + logger.error(f"Failed to collect disk usage from {system}: {e}") + raise + + def collect_filesystem_usage(self, system: str) -> List[Dict[str, Any]]: + """Collect filesystem usage statistics.""" + usage_stats = [] + + try: + # Run df command + result = subprocess.run( + ["ssh", system, "df -h --output=source,fstype,size,used,avail,pcent,target"], + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode != 0: + raise RuntimeError(f"df command failed: {result.stderr}") + + # Parse output + lines = result.stdout.strip().split('\n') + if len(lines) < 2: + return usage_stats + + for line in lines[1:]: # Skip header + parts = line.split() + if len(parts) >= 7: + usage_stat = { + "filesystem": parts[0], + "type": parts[1], + "size": parts[2], + "used": parts[3], + "available": parts[4], + "use_percent": parts[5], + "mountpoint": parts[6] + } + usage_stats.append(usage_stat) + + except subprocess.TimeoutExpired: + logger.error(f"Timeout collecting filesystem usage from {system}") + raise + except Exception as e: + logger.error(f"Failed to collect filesystem usage from {system}: {e}") + raise + + return usage_stats + + def collect_directory_sizes(self, system: str) -> List[Dict[str, Any]]: + """Collect sizes of top-level directories.""" + directory_sizes = [] + + try: + # Get top-level directories + dirs_to_check = ["/", "/home", "/var", "/usr", "/opt", "/etc"] + + for directory in dirs_to_check: + if directory in self.exclude_paths: + continue + + try: + # Run du command for directory size + result = subprocess.run( + ["ssh", system, f"du -sh -- {shlex.quote(directory)} 2>/dev/null"], + capture_output=True, + text=True, + timeout=60 + ) + + if result.returncode == 0: + size, path = result.stdout.strip().split('\t', 1) + directory_sizes.append({ + "path": path, + "size": size + }) + + except subprocess.TimeoutExpired: + logger.warning(f"Timeout getting size for {directory} on {system}") + continue + except Exception as e: + logger.warning(f"Failed to get size for {directory} on {system}: {e}") + continue + + except Exception as e: + logger.error(f"Failed to collect directory sizes from {system}: {e}") + raise + + return directory_sizes + + def collect_largest_files(self, system: str) -> List[Dict[str, Any]]: + """Collect information about largest files in the system.""" + largest_files = [] + + try: + # Find largest files (excluding certain paths) + exclude_expr = " ".join(f"-not -path {shlex.quote(path + '/*')}" for path in self.exclude_paths) + + cmd = f"find / {exclude_expr} -type f -exec ls -lh {{}} \\; 2>/dev/null | sort -k5 -hr | head -20" + + result = subprocess.run( + ["ssh", system, cmd], + capture_output=True, + text=True, + timeout=120 + ) + + if result.returncode == 0: + for line in result.stdout.strip().split('\n'): + if not line.strip(): + continue + + parts = line.split() + if len(parts) >= 9: + file_info = { + "permissions": parts[0], + "links": parts[1], + "owner": parts[2], + "group": parts[3], + "size": parts[4], + "month": parts[5], + "day": parts[6], + "time": parts[7], + "path": " ".join(parts[8:]) + } + largest_files.append(file_info) + + except subprocess.TimeoutExpired: + logger.error(f"Timeout collecting largest files from {system}") + raise + except Exception as e: + logger.error(f"Failed to collect largest files from {system}: {e}") + raise + + return largest_files + + def get_timestamp(self, system: str) -> str: + """Get current timestamp from target system.""" + try: + result = subprocess.run( + ["ssh", system, "date -Iseconds"], + capture_output=True, + text=True, + timeout=10 + ) + + if result.returncode == 0: + return result.stdout.strip() + else: + return "unknown" + + except Exception: + return "unknown" + +def collect(system: str) -> Dict[str, Any]: + """Main collection function for disk usage data.""" + collector = DiskUsageCollector() + return collector.collect_disk_usage(system) diff --git a/professional-infra/migration-validation-framework/collectors/mounts.py b/professional-infra/migration-validation-framework/collectors/mounts.py new file mode 100644 index 0000000..e4dad16 --- /dev/null +++ b/professional-infra/migration-validation-framework/collectors/mounts.py @@ -0,0 +1,172 @@ +""" +Mounts Data Collector + +Collects filesystem mount information including mount points, devices, +filesystem types, and usage statistics. +""" + +import logging +import shlex +import subprocess +from typing import Dict, Any, List + +logger = logging.getLogger(__name__) + +class MountsCollector: + """Collector for filesystem mount information.""" + + def __init__(self): + self.exclude_patterns = [ + "/proc/*", + "/sys/*", + "/dev/*", + "/run/*" + ] + + def collect_mounts(self, system: str) -> Dict[str, Any]: + """Collect mount information from target system.""" + logger.info(f"Collecting mounts data from {system}") + + try: + # Run mount command + result = subprocess.run( + ["ssh", system, "mount"], + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode != 0: + raise RuntimeError(f"Mount command failed: {result.stderr}") + + mounts = self.parse_mount_output(result.stdout) + filtered_mounts = self.filter_mounts(mounts) + + # Get usage statistics + usage_stats = self.collect_usage_stats(system, filtered_mounts) + + return { + "mounts": filtered_mounts, + "usage": usage_stats, + "timestamp": self.get_timestamp(system) + } + + except subprocess.TimeoutExpired: + logger.error(f"Timeout collecting mounts from {system}") + raise + except Exception as e: + logger.error(f"Failed to collect mounts from {system}: {e}") + raise + + def parse_mount_output(self, output: str) -> List[Dict[str, str]]: + """Parse mount command output.""" + mounts = [] + + for line in output.strip().split('\n'): + if not line.strip(): + continue + + # Parse mount output format: device on mountpoint type fstype (options) + parts = line.split() + if len(parts) >= 6 and parts[1] == 'on' and parts[3] == 'type': + mount_info = { + "device": parts[0], + "mountpoint": parts[2], + "fstype": parts[4], + "options": parts[5].strip('()') + } + mounts.append(mount_info) + + return mounts + + def filter_mounts(self, mounts: List[Dict[str, str]]) -> List[Dict[str, str]]: + """Filter out unwanted mount points.""" + filtered = [] + + for mount in mounts: + mountpoint = mount["mountpoint"] + if not any(mountpoint.startswith(pattern.rstrip('*')) for pattern in self.exclude_patterns): + filtered.append(mount) + + return filtered + + def collect_usage_stats(self, system: str, mounts: List[Dict[str, str]]) -> Dict[str, Any]: + """Collect disk usage statistics for mount points.""" + usage_stats = {} + + for mount in mounts: + mountpoint = mount["mountpoint"] + + try: + # Run df command for usage statistics + result = subprocess.run( + ["ssh", system, f"df -BG -- {shlex.quote(mountpoint)}"], + capture_output=True, + text=True, + timeout=15 + ) + + if result.returncode == 0: + usage_stats[mountpoint] = self.parse_df_output(result.stdout) + + except subprocess.TimeoutExpired: + logger.warning(f"Timeout getting usage for {mountpoint} on {system}") + usage_stats[mountpoint] = {"error": "timeout"} + except Exception as e: + logger.warning(f"Failed to get usage for {mountpoint} on {system}: {e}") + usage_stats[mountpoint] = {"error": str(e)} + + return usage_stats + + def parse_df_output(self, output: str) -> Dict[str, Any]: + """Parse df command output.""" + lines = output.strip().split('\n') + if len(lines) < 2: + return {"error": "invalid df output"} + + # Parse header and data + header = lines[0].split() + data = lines[1].split() + + if len(header) != len(data): + return {"error": "header/data mismatch"} + + stats = {} + for i, field in enumerate(header): + if i < len(data): + if field in ['1G-blocks', 'Used', 'Available']: + # Convert to GB + value = data[i] + if value.endswith('G'): + stats[field.lower()] = float(value.rstrip('G')) + else: + stats[field.lower()] = float(value) / (1024**3) # Assume bytes + elif field == 'Use%': + stats['use_percent'] = int(data[i].rstrip('%')) + else: + stats[field.lower()] = data[i] + + return stats + + def get_timestamp(self, system: str) -> str: + """Get current timestamp from target system.""" + try: + result = subprocess.run( + ["ssh", system, "date -Iseconds"], + capture_output=True, + text=True, + timeout=10 + ) + + if result.returncode == 0: + return result.stdout.strip() + else: + return "unknown" + + except Exception: + return "unknown" + +def collect(system: str) -> Dict[str, Any]: + """Main collection function for mounts data.""" + collector = MountsCollector() + return collector.collect_mounts(system) diff --git a/professional-infra/migration-validation-framework/collectors/services.py b/professional-infra/migration-validation-framework/collectors/services.py new file mode 100644 index 0000000..d33ae18 --- /dev/null +++ b/professional-infra/migration-validation-framework/collectors/services.py @@ -0,0 +1,223 @@ +""" +Services Data Collector + +Collects system service information including running services, +their states, startup configuration, and dependencies. +""" + +import logging +import shlex +import subprocess +from typing import Dict, Any, List + +logger = logging.getLogger(__name__) + +class ServicesCollector: + """Collector for system service information.""" + + def __init__(self): + self.service_manager = "systemd" # Default to systemd + self.include_disabled = False + + def collect_services(self, system: str) -> Dict[str, Any]: + """Collect service information from target system.""" + logger.info(f"Collecting services data from {system}") + + try: + # Detect service manager + service_manager = self.detect_service_manager(system) + + if service_manager == "systemd": + services = self.collect_systemd_services(system) + elif service_manager == "sysv": + services = self.collect_sysv_services(system) + else: + raise RuntimeError(f"Unsupported service manager: {service_manager}") + + return { + "service_manager": service_manager, + "services": services, + "timestamp": self.get_timestamp(system) + } + + except Exception as e: + logger.error(f"Failed to collect services from {system}: {e}") + raise + + def detect_service_manager(self, system: str) -> str: + """Detect which service manager is running on the system.""" + try: + # Check for systemd + result = subprocess.run( + ["ssh", system, "ps -p 1 -o comm="], + capture_output=True, + text=True, + timeout=10 + ) + + if result.returncode == 0: + if "systemd" in result.stdout.strip(): + return "systemd" + elif "init" in result.stdout.strip(): + return "sysv" + + # Fallback check + result = subprocess.run( + ["ssh", system, "which systemctl"], + capture_output=True, + text=True, + timeout=10 + ) + + if result.returncode == 0: + return "systemd" + + return "sysv" + + except Exception: + return "unknown" + + def collect_systemd_services(self, system: str) -> List[Dict[str, Any]]: + """Collect systemd service information.""" + services = [] + + try: + # Get all services + result = subprocess.run( + ["ssh", system, "systemctl list-units --type=service --all --no-pager --no-legend"], + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode != 0: + raise RuntimeError(f"systemctl list-units failed: {result.stderr}") + + # Parse service list + for line in result.stdout.strip().split('\n'): + if not line.strip(): + continue + + parts = line.split() + if len(parts) >= 4: + service_name = parts[0] + load_state = parts[1] + active_state = parts[2] + sub_state = parts[3] + + # Skip if disabled and not including disabled + if not self.include_disabled and load_state == "not-found": + continue + + # Get detailed service info + service_info = self.get_systemd_service_details(system, service_name) + + services.append({ + "name": service_name, + "load_state": load_state, + "active_state": active_state, + "sub_state": sub_state, + **service_info + }) + + except subprocess.TimeoutExpired: + logger.error(f"Timeout collecting systemd services from {system}") + raise + except Exception as e: + logger.error(f"Failed to collect systemd services from {system}: {e}") + raise + + return services + + def get_systemd_service_details(self, system: str, service_name: str) -> Dict[str, Any]: + """Get detailed information for a systemd service.""" + details = {} + + try: + # Get service status + result = subprocess.run( + ["ssh", system, f"systemctl show {shlex.quote(service_name)} --no-pager"], + capture_output=True, + text=True, + timeout=15 + ) + + if result.returncode == 0: + for line in result.stdout.strip().split('\n'): + if '=' in line: + key, value = line.split('=', 1) + details[key.lower()] = value + + except Exception as e: + logger.warning(f"Failed to get details for {service_name}: {e}") + + return details + + def collect_sysv_services(self, system: str) -> List[Dict[str, Any]]: + """Collect SysV init service information.""" + services = [] + + try: + # Get service list from /etc/init.d/ + result = subprocess.run( + ["ssh", system, "ls -1 /etc/init.d/"], + capture_output=True, + text=True, + timeout=15 + ) + + if result.returncode != 0: + raise RuntimeError(f"Failed to list init.d services: {result.stderr}") + + for service_name in result.stdout.strip().split('\n'): + if not service_name.strip(): + continue + + # Get service status + status_result = subprocess.run( + ["ssh", system, f"/etc/init.d/{shlex.quote(service_name)} status"], + capture_output=True, + text=True, + timeout=10 + ) + + status = "unknown" + if status_result.returncode == 0: + status = "running" + elif "not running" in status_result.stdout.lower(): + status = "stopped" + + services.append({ + "name": service_name, + "status": status, + "type": "sysv" + }) + + except Exception as e: + logger.error(f"Failed to collect SysV services from {system}: {e}") + raise + + return services + + def get_timestamp(self, system: str) -> str: + """Get current timestamp from target system.""" + try: + result = subprocess.run( + ["ssh", system, "date -Iseconds"], + capture_output=True, + text=True, + timeout=10 + ) + + if result.returncode == 0: + return result.stdout.strip() + else: + return "unknown" + + except Exception: + return "unknown" + +def collect(system: str) -> Dict[str, Any]: + """Main collection function for services data.""" + collector = ServicesCollector() + return collector.collect_services(system) diff --git a/professional-infra/migration-validation-framework/docs/architecture.md b/professional-infra/migration-validation-framework/docs/architecture.md new file mode 100644 index 0000000..425055e --- /dev/null +++ b/professional-infra/migration-validation-framework/docs/architecture.md @@ -0,0 +1,30 @@ +# Migration Validation Framework Architecture + +## Components + +- CLI: parses operator commands and coordinates workflows. +- Collectors: gather mounts, services, and disk usage from target systems. +- Snapshot files: JSON evidence used as immutable migration checkpoints. +- Comparator: evaluates drift between before and after snapshots. +- Reports: stores JSON or HTML output for audit and review. + +## Data Flow + +``` +Operator + -> python3 cli.py collect + -> collectors over SSH + -> before.json / after.json + -> python3 cli.py compare + -> diff.json with PASS/FAIL validation +``` + +## Validation Flow + +``` +before.json -> Comparator -> service checks +after.json -> Comparator -> filesystem checks -> validation result + -> mount checks +``` + +The framework keeps collection and comparison separate so migration evidence can be reviewed, archived, and replayed without recollecting from production systems. diff --git a/professional-infra/migration-validation-framework/examples/after.json b/professional-infra/migration-validation-framework/examples/after.json new file mode 100644 index 0000000..e29e87e --- /dev/null +++ b/professional-infra/migration-validation-framework/examples/after.json @@ -0,0 +1,40 @@ +{ + "metadata": { + "timestamp": "2026-04-29T03:40:00Z", + "systems": ["web01"], + "version": "1.0" + }, + "data": { + "web01": { + "mounts": { + "mounts": [ + {"device": "/dev/sda1", "mountpoint": "/", "fstype": "ext4", "options": "rw,relatime"}, + {"device": "/dev/sdb1", "mountpoint": "/var", "fstype": "xfs", "options": "rw,noatime"} + ], + "usage": { + "/": {"filesystem": "/dev/sda1", "use_percent": "62%"}, + "/var": {"filesystem": "/dev/sdb1", "use_percent": "94%"} + }, + "timestamp": "2026-04-29T03:40:00Z" + }, + "services": { + "service_manager": "systemd", + "services": [ + {"name": "sshd", "active_state": "failed", "sub_state": "failed"}, + {"name": "nginx", "active_state": "active", "sub_state": "running"}, + {"name": "node-exporter", "active_state": "active", "sub_state": "running"} + ], + "timestamp": "2026-04-29T03:40:00Z" + }, + "disk_usage": { + "filesystem_usage": [ + {"filesystem": "/dev/sda1", "type": "ext4", "size": "80G", "used": "50G", "available": "30G", "use_percent": "62%", "mountpoint": "/"}, + {"filesystem": "/dev/sdb1", "type": "xfs", "size": "200G", "used": "188G", "available": "12G", "use_percent": "94%", "mountpoint": "/var"} + ], + "directory_sizes": [{"path": "/var/lib/app", "size": "139G"}], + "largest_files": [{"path": "/var/lib/app/import/archive.tar", "size": "42G"}], + "timestamp": "2026-04-29T03:40:00Z" + } + } + } +} diff --git a/professional-infra/migration-validation-framework/examples/before.json b/professional-infra/migration-validation-framework/examples/before.json new file mode 100644 index 0000000..46dbe8d --- /dev/null +++ b/professional-infra/migration-validation-framework/examples/before.json @@ -0,0 +1,39 @@ +{ + "metadata": { + "timestamp": "2026-04-29T01:15:00Z", + "systems": ["web01"], + "version": "1.0" + }, + "data": { + "web01": { + "mounts": { + "mounts": [ + {"device": "/dev/sda1", "mountpoint": "/", "fstype": "ext4", "options": "rw,relatime"}, + {"device": "/dev/sdb1", "mountpoint": "/var", "fstype": "xfs", "options": "rw,noatime"} + ], + "usage": { + "/": {"filesystem": "/dev/sda1", "use_percent": "61%"}, + "/var": {"filesystem": "/dev/sdb1", "use_percent": "68%"} + }, + "timestamp": "2026-04-29T01:15:00Z" + }, + "services": { + "service_manager": "systemd", + "services": [ + {"name": "sshd", "active_state": "active", "sub_state": "running"}, + {"name": "nginx", "active_state": "active", "sub_state": "running"} + ], + "timestamp": "2026-04-29T01:15:00Z" + }, + "disk_usage": { + "filesystem_usage": [ + {"filesystem": "/dev/sda1", "type": "ext4", "size": "80G", "used": "49G", "available": "31G", "use_percent": "61%", "mountpoint": "/"}, + {"filesystem": "/dev/sdb1", "type": "xfs", "size": "200G", "used": "136G", "available": "64G", "use_percent": "68%", "mountpoint": "/var"} + ], + "directory_sizes": [{"path": "/var/lib/app", "size": "84G"}], + "largest_files": [], + "timestamp": "2026-04-29T01:15:00Z" + } + } + } +} diff --git a/professional-infra/migration-validation-framework/examples/diff.json b/professional-infra/migration-validation-framework/examples/diff.json new file mode 100644 index 0000000..53a4108 --- /dev/null +++ b/professional-infra/migration-validation-framework/examples/diff.json @@ -0,0 +1,211 @@ +{ + "summary": { + "total_systems": 1, + "systems_with_changes": 1, + "total_changes": 7, + "changes_by_type": { + "mounts": 2, + "services": 2, + "disk_usage": 3 + }, + "most_affected_systems": [ + [ + "web01", + 7 + ] + ] + }, + "differences": { + "mounts": { + "web01": { + "added_mounts": [], + "removed_mounts": [], + "changed_mounts": [], + "usage_changes": [ + { + "mountpoint": "/", + "before": { + "filesystem": "/dev/sda1", + "use_percent": "61%" + }, + "after": { + "filesystem": "/dev/sda1", + "use_percent": "62%" + } + }, + { + "mountpoint": "/var", + "before": { + "filesystem": "/dev/sdb1", + "use_percent": "68%" + }, + "after": { + "filesystem": "/dev/sdb1", + "use_percent": "94%" + } + } + ] + } + }, + "services": { + "web01": { + "added_services": [ + { + "name": "node-exporter", + "active_state": "active", + "sub_state": "running" + } + ], + "removed_services": [], + "status_changes": [ + { + "name": "sshd", + "before": { + "active_state": "active", + "sub_state": "running" + }, + "after": { + "active_state": "failed", + "sub_state": "failed" + } + } + ], + "configuration_changes": [] + } + }, + "disk_usage": { + "web01": { + "filesystem_changes": [ + { + "mountpoint": "/", + "before": { + "filesystem": "/dev/sda1", + "type": "ext4", + "size": "80G", + "used": "49G", + "available": "31G", + "use_percent": "61%", + "mountpoint": "/" + }, + "after": { + "filesystem": "/dev/sda1", + "type": "ext4", + "size": "80G", + "used": "50G", + "available": "30G", + "use_percent": "62%", + "mountpoint": "/" + } + }, + { + "mountpoint": "/var", + "before": { + "filesystem": "/dev/sdb1", + "type": "xfs", + "size": "200G", + "used": "136G", + "available": "64G", + "use_percent": "68%", + "mountpoint": "/var" + }, + "after": { + "filesystem": "/dev/sdb1", + "type": "xfs", + "size": "200G", + "used": "188G", + "available": "12G", + "use_percent": "94%", + "mountpoint": "/var" + } + } + ], + "directory_size_changes": [], + "significant_usage_changes": [ + { + "mountpoint": "/var", + "change_percent": 26, + "before": { + "filesystem": "/dev/sdb1", + "type": "xfs", + "size": "200G", + "used": "136G", + "available": "64G", + "use_percent": "68%", + "mountpoint": "/var" + }, + "after": { + "filesystem": "/dev/sdb1", + "type": "xfs", + "size": "200G", + "used": "188G", + "available": "12G", + "use_percent": "94%", + "mountpoint": "/var" + } + } + ] + } + } + }, + "risk_assessment": { + "overall_risk": "high", + "risk_factors": [ + { + "type": "service_failure", + "description": "Service failed: sshd", + "level": 3 + }, + { + "type": "disk_usage_spike", + "description": "Significant disk usage change: /var (26%)", + "level": 2 + } + ], + "critical_changes": [], + "recommendations": [ + "Immediate review required - critical changes detected", + "Consider rolling back migration if critical services are affected" + ] + }, + "validation_results": { + "passed": false, + "checks": [ + { + "name": "critical_services_running", + "description": "Verify critical services remain operational", + "passed": false, + "details": [ + "Critical service sshd failed on web01" + ] + }, + { + "name": "filesystem_integrity", + "description": "Verify filesystem integrity maintained", + "passed": true, + "details": [] + }, + { + "name": "no_critical_mounts_removed", + "description": "Verify critical mount points remain", + "passed": true, + "details": [] + } + ], + "failed_checks": [ + { + "name": "critical_services_running", + "description": "Verify critical services remain operational", + "passed": false, + "details": [ + "Critical service sshd failed on web01" + ] + } + ], + "result": "FAIL" + }, + "metadata": { + "before": "migration-validation-framework/examples/before.json", + "after": "migration-validation-framework/examples/after.json", + "timestamp": "2026-04-29T23:29:07.510774" + } +} diff --git a/professional-infra/migration-validation-framework/scenarios/before_after_comparison.md b/professional-infra/migration-validation-framework/scenarios/before_after_comparison.md new file mode 100644 index 0000000..93b9606 --- /dev/null +++ b/professional-infra/migration-validation-framework/scenarios/before_after_comparison.md @@ -0,0 +1,19 @@ +# Scenario: Before/After Migration Comparison + +## Description + +Compare a pre-cutover host snapshot against a post-cutover snapshot and determine whether the migrated system is ready for production traffic. + +## Commands + +```bash +cd professional-infra/migration-validation-framework +python3 cli.py compare examples/before.json examples/after.json --output /tmp/migration-diff.json +``` + +## Expected Result + +- The command writes a JSON diff. +- The result is `FAIL` because `sshd` is failed after migration. +- The risk assessment highlights the `/var` disk usage increase. +- The remediation path is to restore SSH and reduce or expand `/var` before approving cutover. diff --git a/professional-infra/migration-validation-framework/tests/test_framework.py b/professional-infra/migration-validation-framework/tests/test_framework.py new file mode 100644 index 0000000..e9e24e1 --- /dev/null +++ b/professional-infra/migration-validation-framework/tests/test_framework.py @@ -0,0 +1,67 @@ +import json +import unittest +from pathlib import Path + +from collectors.mounts import MountsCollector +from reports.html_report import HTMLReportGenerator +from validators.compare import compare_snapshots + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +class ComparatorExampleTests(unittest.TestCase): + def test_example_comparison_detects_expected_failure(self): + before = json.loads((PROJECT_ROOT / "examples" / "before.json").read_text()) + after = json.loads((PROJECT_ROOT / "examples" / "after.json").read_text()) + + comparison = compare_snapshots(before, after) + + self.assertFalse(comparison["validation_results"]["passed"]) + self.assertEqual(comparison["validation_results"]["result"], "FAIL") + self.assertGreater(comparison["summary"]["total_changes"], 0) + + +class HtmlReportTests(unittest.TestCase): + def test_report_escapes_untrusted_snapshot_content(self): + report = HTMLReportGenerator().build_html_content({ + "metadata": {"comparison_id": ""}, + "summary": { + "total_systems": 1, + "systems_with_changes": 1, + "total_changes": 1, + "changes_by_type": {"services": 1}, + "most_affected_systems": [("", 1)], + }, + "differences": {}, + "risk_assessment": {"overall_risk": "low", "risk_factors": [], "critical_changes": [], "recommendations": ["Review change"]}, + "validation_results": {"passed": True, "checks": []}, + }) + + self.assertNotIn("", report) + self.assertNotIn("", report) + self.assertIn("<script>alert(1)</script>", report) + self.assertIn("<b>change</b>", report) + + +class CollectorParserTests(unittest.TestCase): + def test_mount_parser_handles_standard_mount_output(self): + output = "/dev/sda1 on / type ext4 (rw,relatime)\nproc on /proc type proc (rw,nosuid,nodev,noexec,relatime)\n" + mounts = MountsCollector().parse_mount_output(output) + + self.assertEqual(mounts[0]["device"], "/dev/sda1") + self.assertEqual(mounts[0]["mountpoint"], "/") + self.assertEqual(mounts[0]["fstype"], "ext4") + + def test_df_parser_handles_gigabyte_output(self): + output = "Filesystem 1G-blocks Used Available Use% Mounted\n/dev/sda1 100G 45G 55G 45% /\n" + stats = MountsCollector().parse_df_output(output) + + self.assertEqual(stats["1g-blocks"], 100.0) + self.assertEqual(stats["used"], 45.0) + self.assertEqual(stats["available"], 55.0) + self.assertEqual(stats["use_percent"], 45) + + +if __name__ == "__main__": + unittest.main() diff --git a/professional-infra/migration-validation-framework/validators/compare.py b/professional-infra/migration-validation-framework/validators/compare.py new file mode 100644 index 0000000..50ab0d0 --- /dev/null +++ b/professional-infra/migration-validation-framework/validators/compare.py @@ -0,0 +1,501 @@ +""" +Snapshot Comparison Engine + +Compares two system snapshots and identifies differences, +risk levels, and validation results. +""" + +import json +import logging +from typing import Dict, Any, List, Tuple +from datetime import datetime + +logger = logging.getLogger(__name__) + +class SnapshotComparator: + """Engine for comparing system snapshots.""" + + def __init__(self): + self.risk_levels = { + "low": 1, + "medium": 2, + "high": 3, + "critical": 4 + } + + def compare_snapshots(self, snapshot1: Dict[str, Any], snapshot2: Dict[str, Any]) -> Dict[str, Any]: + """Compare two snapshots and return detailed comparison results.""" + logger.info("Starting snapshot comparison") + + comparison = { + "summary": {}, + "differences": {}, + "risk_assessment": {}, + "validation_results": {} + } + + # Compare each data type + data_types = ["mounts", "services", "disk_usage"] + + data1 = snapshot1.get("data", {}) + data2 = snapshot2.get("data", {}) + + for data_type in data_types: + if self.data_type_exists(data1, data_type) or self.data_type_exists(data2, data_type): + differences = self.compare_data_type(data1, data2, data_type) + comparison["differences"][data_type] = differences + + # Generate summary + comparison["summary"] = self.generate_summary(comparison["differences"]) + + # Risk assessment + comparison["risk_assessment"] = self.assess_risks(comparison["differences"]) + + # Validation results + comparison["validation_results"] = self.validate_changes(comparison["differences"]) + comparison["validation_results"]["result"] = "PASS" if comparison["validation_results"]["passed"] else "FAIL" + + logger.info("Snapshot comparison completed") + return comparison + + def data_type_exists(self, systems: Dict[str, Any], data_type: str) -> bool: + """Return true when at least one system has the requested collector data.""" + return any(data_type in system_data for system_data in systems.values()) + + def compare_data_type(self, data1: Dict[str, Any], data2: Dict[str, Any], data_type: str) -> Dict[str, Any]: + """Compare a specific data type between two snapshots.""" + differences = {} + + # Get all systems from both snapshots + systems1 = set(data1.keys()) + systems2 = set(data2.keys()) + all_systems = systems1.union(systems2) + + for system in all_systems: + system_diffs = {} + + if system not in data1: + system_diffs["status"] = "added" + system_diffs["details"] = {"new_system": True} + elif system not in data2: + system_diffs["status"] = "removed" + system_diffs["details"] = {"removed_system": True} + else: + # Compare data for this system and data type + if data_type in data1[system] and data_type in data2[system]: + system_diffs = self.compare_system_data( + data1[system][data_type], + data2[system][data_type], + data_type + ) + else: + system_diffs["status"] = "data_missing" + system_diffs["details"] = {"missing_data_type": data_type} + + if system_diffs: + differences[system] = system_diffs + + return differences + + def compare_system_data(self, data1: Dict[str, Any], data2: Dict[str, Any], data_type: str) -> Dict[str, Any]: + """Compare data for a specific system and data type.""" + differences = {} + + if data_type == "mounts": + differences = self.compare_mounts(data1, data2) + elif data_type == "services": + differences = self.compare_services(data1, data2) + elif data_type == "disk_usage": + differences = self.compare_disk_usage(data1, data2) + else: + differences["status"] = "unknown_data_type" + + return differences + + def compare_mounts(self, mounts1: Dict[str, Any], mounts2: Dict[str, Any]) -> Dict[str, Any]: + """Compare mounts data between snapshots.""" + differences = { + "added_mounts": [], + "removed_mounts": [], + "changed_mounts": [], + "usage_changes": [] + } + + # Compare mount lists + mounts_list1 = mounts1.get("mounts", []) + mounts_list2 = mounts2.get("mounts", []) + + # Create mountpoint maps + mounts_map1 = {m["mountpoint"]: m for m in mounts_list1} + mounts_map2 = {m["mountpoint"]: m for m in mounts_list2} + + # Find added and removed mounts + added = set(mounts_map2.keys()) - set(mounts_map1.keys()) + removed = set(mounts_map1.keys()) - set(mounts_map2.keys()) + + differences["added_mounts"] = [{"mountpoint": mp, **mounts_map2[mp]} for mp in added] + differences["removed_mounts"] = [{"mountpoint": mp, **mounts_map1[mp]} for mp in removed] + + # Find changed mounts + common = set(mounts_map1.keys()) & set(mounts_map2.keys()) + for mp in common: + m1, m2 = mounts_map1[mp], mounts_map2[mp] + if m1 != m2: + differences["changed_mounts"].append({ + "mountpoint": mp, + "before": m1, + "after": m2 + }) + + # Compare usage statistics + usage1 = mounts1.get("usage", {}) + usage2 = mounts2.get("usage", {}) + + for mp in set(usage1.keys()) | set(usage2.keys()): + if mp in usage1 and mp in usage2: + u1, u2 = usage1[mp], usage2[mp] + if u1 != u2: + differences["usage_changes"].append({ + "mountpoint": mp, + "before": u1, + "after": u2 + }) + + return differences + + def compare_services(self, services1: Dict[str, Any], services2: Dict[str, Any]) -> Dict[str, Any]: + """Compare services data between snapshots.""" + differences = { + "added_services": [], + "removed_services": [], + "status_changes": [], + "configuration_changes": [] + } + + # Compare service lists + services_list1 = services1.get("services", []) + services_list2 = services2.get("services", []) + + # Create service maps + services_map1 = {s["name"]: s for s in services_list1} + services_map2 = {s["name"]: s for s in services_list2} + + # Find added and removed services + added = set(services_map2.keys()) - set(services_map1.keys()) + removed = set(services_map1.keys()) - set(services_map2.keys()) + + differences["added_services"] = [{"name": name, **services_map2[name]} for name in added] + differences["removed_services"] = [{"name": name, **services_map1[name]} for name in removed] + + # Find status changes + common = set(services_map1.keys()) & set(services_map2.keys()) + for name in common: + s1, s2 = services_map1[name], services_map2[name] + if s1.get("active_state") != s2.get("active_state") or s1.get("sub_state") != s2.get("sub_state"): + differences["status_changes"].append({ + "name": name, + "before": {"active_state": s1.get("active_state"), "sub_state": s1.get("sub_state")}, + "after": {"active_state": s2.get("active_state"), "sub_state": s2.get("sub_state")} + }) + + return differences + + def compare_disk_usage(self, usage1: Dict[str, Any], usage2: Dict[str, Any]) -> Dict[str, Any]: + """Compare disk usage data between snapshots.""" + differences = { + "filesystem_changes": [], + "directory_size_changes": [], + "significant_usage_changes": [] + } + + # Compare filesystem usage + fs1 = usage1.get("filesystem_usage", []) + fs2 = usage2.get("filesystem_usage", []) + + # Create filesystem maps by mountpoint + fs_map1 = {fs["mountpoint"]: fs for fs in fs1} + fs_map2 = {fs["mountpoint"]: fs for fs in fs2} + + common_fs = set(fs_map1.keys()) & set(fs_map2.keys()) + for mp in common_fs: + f1, f2 = fs_map1[mp], fs_map2[mp] + if f1 != f2: + differences["filesystem_changes"].append({ + "mountpoint": mp, + "before": f1, + "after": f2 + }) + + # Check for significant usage changes + try: + use1 = int(f1.get("use_percent", "0").rstrip("%")) + use2 = int(f2.get("use_percent", "0").rstrip("%")) + if abs(use2 - use1) > 10: # 10% change threshold + differences["significant_usage_changes"].append({ + "mountpoint": mp, + "change_percent": use2 - use1, + "before": f1, + "after": f2 + }) + except (ValueError, KeyError): + pass + + return differences + + def generate_summary(self, differences: Dict[str, Any]) -> Dict[str, Any]: + """Generate a summary of all differences.""" + summary = { + "total_systems": 0, + "systems_with_changes": 0, + "total_changes": 0, + "changes_by_type": {}, + "most_affected_systems": [] + } + + system_change_counts = {} + + for data_type, systems in differences.items(): + summary["changes_by_type"][data_type] = 0 + + for system, system_diffs in systems.items(): + if system not in system_change_counts: + system_change_counts[system] = 0 + + # Count changes for this system and data type + change_count = self.count_changes(system_diffs) + system_change_counts[system] += change_count + summary["changes_by_type"][data_type] += change_count + summary["total_changes"] += change_count + + summary["total_systems"] = len(system_change_counts) + + # Count systems with changes + summary["systems_with_changes"] = len([s for s in system_change_counts.values() if s > 0]) + + # Find most affected systems + sorted_systems = sorted(system_change_counts.items(), key=lambda x: x[1], reverse=True) + summary["most_affected_systems"] = sorted_systems[:5] + + return summary + + def count_changes(self, system_diffs: Dict[str, Any]) -> int: + """Count the number of changes in system differences.""" + count = 0 + + for key, value in system_diffs.items(): + if isinstance(value, list): + count += len(value) + elif isinstance(value, dict) and key not in ["status"]: + # Count nested changes + count += sum(1 for v in value.values() if isinstance(v, list) and v) + + return count + + def assess_risks(self, differences: Dict[str, Any]) -> Dict[str, Any]: + """Assess risk levels for the changes.""" + risk_assessment = { + "overall_risk": "low", + "risk_factors": [], + "critical_changes": [], + "recommendations": [] + } + + max_risk_level = 1 + + # Analyze each type of change + for data_type, systems in differences.items(): + for system, system_diffs in systems.items(): + risk_factors = self.analyze_system_risks(system_diffs, data_type) + risk_assessment["risk_factors"].extend(risk_factors) + + for factor in risk_factors: + if factor["level"] > max_risk_level: + max_risk_level = factor["level"] + + if factor["level"] >= 4: # Critical + risk_assessment["critical_changes"].append({ + "system": system, + "data_type": data_type, + "factor": factor + }) + + # Set overall risk + risk_levels = {1: "low", 2: "medium", 3: "high", 4: "critical"} + risk_assessment["overall_risk"] = risk_levels.get(max_risk_level, "unknown") + + # Generate recommendations + risk_assessment["recommendations"] = self.generate_recommendations(risk_assessment) + + return risk_assessment + + def analyze_system_risks(self, system_diffs: Dict[str, Any], data_type: str) -> List[Dict[str, Any]]: + """Analyze risks for a specific system's changes.""" + risk_factors = [] + + if data_type == "mounts": + # Check for removed critical mounts + for mount in system_diffs.get("removed_mounts", []): + if mount["mountpoint"] in ["/", "/boot", "/usr", "/var"]: + risk_factors.append({ + "type": "critical_mount_removed", + "description": f"Critical mount point removed: {mount['mountpoint']}", + "level": 4 + }) + + # Check for significant usage changes + for change in system_diffs.get("usage_changes", []): + try: + before_pct = int(change["before"].get("use_percent", "0").rstrip("%")) + after_pct = int(change["after"].get("use_percent", "0").rstrip("%")) + if after_pct > 95: + risk_factors.append({ + "type": "filesystem_full", + "description": f"Filesystem usage critical: {change['mountpoint']} at {after_pct}%", + "level": 3 + }) + except (ValueError, KeyError): + pass + + elif data_type == "services": + # Check for critical service changes + critical_services = ["sshd", "systemd", "networking", "dbus"] + for service in system_diffs.get("removed_services", []): + if service["name"] in critical_services: + risk_factors.append({ + "type": "critical_service_removed", + "description": f"Critical service removed: {service['name']}", + "level": 4 + }) + + for change in system_diffs.get("status_changes", []): + if change["after"]["active_state"] == "failed": + risk_factors.append({ + "type": "service_failure", + "description": f"Service failed: {change['name']}", + "level": 3 + }) + + elif data_type == "disk_usage": + for change in system_diffs.get("significant_usage_changes", []): + if change["change_percent"] > 20: + risk_factors.append({ + "type": "disk_usage_spike", + "description": f"Significant disk usage change: {change['mountpoint']} ({change['change_percent']}%)", + "level": 2 + }) + + return risk_factors + + def generate_recommendations(self, risk_assessment: Dict[str, Any]) -> List[str]: + """Generate recommendations based on risk assessment.""" + recommendations = [] + + if risk_assessment["overall_risk"] in ["high", "critical"]: + recommendations.append("Immediate review required - critical changes detected") + recommendations.append("Consider rolling back migration if critical services are affected") + + if any(f["type"] == "critical_mount_removed" for f in risk_assessment["risk_factors"]): + recommendations.append("Verify system boot capability after mount changes") + + if any(f["type"] == "critical_service_removed" for f in risk_assessment["risk_factors"]): + recommendations.append("Ensure critical services are restored before production cutover") + + if any(f["type"] == "filesystem_full" for f in risk_assessment["risk_factors"]): + recommendations.append("Monitor disk space closely - cleanup may be required") + + if not recommendations: + recommendations.append("Changes appear safe - proceed with standard validation procedures") + + return recommendations + + def validate_changes(self, differences: Dict[str, Any]) -> Dict[str, Any]: + """Validate that changes meet requirements.""" + validation_results = { + "passed": True, + "checks": [], + "failed_checks": [] + } + + # Define validation checks + checks = [ + self.check_critical_services_running, + self.check_filesystem_integrity, + self.check_no_critical_mounts_removed + ] + + for check_func in checks: + check_result = check_func(differences) + validation_results["checks"].append(check_result) + + if not check_result["passed"]: + validation_results["passed"] = False + validation_results["failed_checks"].append(check_result) + + return validation_results + + def check_critical_services_running(self, differences: Dict[str, Any]) -> Dict[str, Any]: + """Check that critical services are still running.""" + check = { + "name": "critical_services_running", + "description": "Verify critical services remain operational", + "passed": True, + "details": [] + } + + critical_services = ["sshd", "systemd"] + + for data_type, systems in differences.items(): + if data_type == "services": + for system, system_diffs in systems.items(): + for change in system_diffs.get("status_changes", []): + if change["name"] in critical_services: + if change["after"]["active_state"] == "failed": + check["passed"] = False + check["details"].append(f"Critical service {change['name']} failed on {system}") + + return check + + def check_filesystem_integrity(self, differences: Dict[str, Any]) -> Dict[str, Any]: + """Check filesystem integrity after changes.""" + check = { + "name": "filesystem_integrity", + "description": "Verify filesystem integrity maintained", + "passed": True, + "details": [] + } + + for data_type, systems in differences.items(): + if data_type == "disk_usage": + for system, system_diffs in systems.items(): + for change in system_diffs.get("significant_usage_changes", []): + if change["change_percent"] > 50: # Arbitrary threshold + check["passed"] = False + check["details"].append(f"Extreme usage change on {system}:{change['mountpoint']}") + + return check + + def check_no_critical_mounts_removed(self, differences: Dict[str, Any]) -> Dict[str, Any]: + """Check that no critical mount points were removed.""" + check = { + "name": "no_critical_mounts_removed", + "description": "Verify critical mount points remain", + "passed": True, + "details": [] + } + + critical_mounts = ["/", "/boot", "/usr", "/var"] + + for data_type, systems in differences.items(): + if data_type == "mounts": + for system, system_diffs in systems.items(): + for mount in system_diffs.get("removed_mounts", []): + if mount["mountpoint"] in critical_mounts: + check["passed"] = False + check["details"].append(f"Critical mount {mount['mountpoint']} removed from {system}") + + return check + +def compare_snapshots(snapshot1: Dict[str, Any], snapshot2: Dict[str, Any]) -> Dict[str, Any]: + """Main comparison function.""" + comparator = SnapshotComparator() + return comparator.compare_snapshots(snapshot1, snapshot2) diff --git a/professional-infra/zabbix-monitoring-incident-response/.ansible-lint b/professional-infra/zabbix-monitoring-incident-response/.ansible-lint new file mode 100644 index 0000000..3e5738f --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/.ansible-lint @@ -0,0 +1,8 @@ +--- +skip_list: + - role-name + - name[casing] + - line-too-long + +exclude_paths: + - .git diff --git a/professional-infra/zabbix-monitoring-incident-response/Makefile b/professional-infra/zabbix-monitoring-incident-response/Makefile new file mode 100644 index 0000000..962acc8 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/Makefile @@ -0,0 +1,19 @@ +.PHONY: help test lint syntax validate-assets + +help: + @echo "Zabbix Monitoring + Incident Response" + @echo " make test Run syntax, lint, and asset validation" + @echo " make syntax Run Ansible syntax checks" + @echo " make lint Run ansible-lint" + @echo " make validate-assets Validate template and sample JSON assets" + +test: syntax lint validate-assets + +syntax: + ansible-playbook --syntax-check playbooks/*.yml + +lint: + ansible-lint + +validate-assets: + python3 scripts/validate_assets.py diff --git a/professional-infra/zabbix-monitoring-incident-response/README.md b/professional-infra/zabbix-monitoring-incident-response/README.md new file mode 100644 index 0000000..1007c55 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/README.md @@ -0,0 +1,63 @@ +# Zabbix Monitoring + Incident Response + +## Problem + +Large Linux/Unix environments need simple, reliable OS checks before more advanced observability becomes useful. Filesystems, CPU, memory, network, process status, proxy backlog, and agent availability must be monitored consistently across Linux and AIX estates. + +## CV Relevance + +This project maps to Zabbix monitoring platform work, proxy maintenance, custom checks, alert noise reduction, and incident response in enterprise environments. It shows operational design and automation without pretending to run AIX locally. + +## What This Project Demonstrates + +- Ansible-first Zabbix server, proxy, and agent/agent2 configuration structure. +- Proxy topology for active and passive checks. +- Linux and AIX OS monitoring templates as reviewable JSON assets. +- Sample Linux/AIX check data for filesystem, CPU, memory, network, and process monitoring. +- Runbooks for Zabbix maintenance and incident response. + +## Architecture + +```text +Linux/AIX hosts -> Zabbix agent/agent2 -> Zabbix proxy -> Zabbix server/web + | | + v v + OS simple checks proxy queue/cache + +Incident -> Alert -> Operator triage -> Maintenance or remediation evidence +``` + +## Quickstart + +```bash +cd professional-infra/zabbix-monitoring-incident-response +make test +``` + +`make test` performs Ansible syntax/lint checks and validates the Zabbix template/sample JSON assets. + +## Validation + +```bash +ansible-playbook --syntax-check playbooks/*.yml +ansible-lint +python3 scripts/validate_assets.py +``` + +## Example Output + +Sample check payloads are available in `samples/linux-os-checks.json` and `samples/aix-os-checks.json`. These show what a reviewable `zabbix_sender` or API-driven evidence artifact could look like for Linux and AIX hosts. + +## Interview Talking Points + +- Why Zabbix is suitable for simple OS checks while ELK/Grafana is better for log analysis. +- How proxies reduce WAN dependency and support branch/client environments. +- Difference between active and passive checks. +- How to troubleshoot unsupported items, missing data, proxy backlog, and agent reachability. +- How Linux and AIX monitoring differ without inventing local AIX runtime. + +## Roadmap + +- Add API import helpers for templates. +- Add a Docker-based Zabbix server/proxy demo scaffold. +- Add Wazuh or security monitoring integration as a separate side lab. diff --git a/professional-infra/zabbix-monitoring-incident-response/ansible.cfg b/professional-infra/zabbix-monitoring-incident-response/ansible.cfg new file mode 100644 index 0000000..f295265 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/ansible.cfg @@ -0,0 +1,5 @@ +[defaults] +roles_path = ./roles +inventory = ./inventory/hosts.ini +host_key_checking = False +retry_files_enabled = False diff --git a/professional-infra/zabbix-monitoring-incident-response/docs/incident-response-runbook.md b/professional-infra/zabbix-monitoring-incident-response/docs/incident-response-runbook.md new file mode 100644 index 0000000..76d1cf3 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/docs/incident-response-runbook.md @@ -0,0 +1,30 @@ +# Incident Response Runbook + +## Filesystem Alert + +1. Confirm current usage and growth trend. +2. Check whether the host is Linux or AIX and use the correct runbook. +3. Validate application ownership of the filesystem. +4. Clean known temporary paths or request LVM expansion when approved. +5. Attach before/after evidence to the incident ticket. + +## Agent Unreachable + +1. Confirm whether data loss affects one host, one proxy, or one network segment. +2. Check proxy queue and last seen timestamp. +3. Validate agent service state and firewall path. +4. For active checks, confirm `ServerActive` and hostname match. + +## Proxy Backlog + +1. Check server reachability from proxy. +2. Check proxy DB filesystem usage. +3. Confirm whether config sync recently changed. +4. Reduce noise by temporarily disabling non-critical discovery rules if required. + +## Unsupported Items + +1. Identify affected template and item key. +2. Check whether item is Linux-specific or AIX-specific. +3. Validate agent version and custom user parameters. +4. Roll back template change if canary host group is affected. diff --git a/professional-infra/zabbix-monitoring-incident-response/docs/maintenance-runbook.md b/professional-infra/zabbix-monitoring-incident-response/docs/maintenance-runbook.md new file mode 100644 index 0000000..08ba0f2 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/docs/maintenance-runbook.md @@ -0,0 +1,29 @@ +# Zabbix Maintenance Runbook + +## Server Checks + +- Confirm Zabbix server process and web frontend availability. +- Check database health, free space, and slow queries. +- Review cache usage, poller utilization, and housekeeper activity. +- Confirm recent values are arriving for representative Linux and AIX hosts. + +## Proxy Checks + +- Confirm proxy last seen timestamp. +- Check proxy queue and delayed values. +- Validate proxy database size and filesystem usage. +- Confirm active/passive connectivity based on proxy mode. + +## Template Maintenance + +- Import templates in a controlled window. +- Watch unsupported items after import. +- Validate a small canary host group before wider rollout. +- Document changed triggers and thresholds. + +## Common Failure Modes + +- Agent unreachable: check DNS, firewall, agent service, proxy route. +- Unsupported item: check key spelling, OS capability, agent version, user parameter. +- Proxy backlog: check WAN, DB size, proxy process, server availability. +- Alert noise: review trigger thresholds and dependency design. diff --git a/professional-infra/zabbix-monitoring-incident-response/docs/proxy-design.md b/professional-infra/zabbix-monitoring-incident-response/docs/proxy-design.md new file mode 100644 index 0000000..7555655 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/docs/proxy-design.md @@ -0,0 +1,27 @@ +# Zabbix Proxy Design + +## Purpose + +Zabbix proxies reduce dependency on direct connectivity between the central server and monitored hosts. They are useful for client networks, segmented environments, remote sites, and maintenance windows. + +## Active Proxy + +- Proxy connects to the Zabbix server. +- Good for restricted networks where inbound access to the proxy is not allowed. +- Hosts can use active agent checks against the proxy. +- Main operational checks: proxy last seen, delayed values, local DB size, config sync. + +## Passive Proxy + +- Zabbix server connects to the proxy. +- Useful when central server can reach the proxy network. +- Requires firewall rules from server to proxy. +- Main operational checks: proxy listener, network latency, poller load. + +## Operational Signals + +- Proxy queue growth. +- Unsupported items after template changes. +- Agent unreachable or active checks delayed. +- Proxy DB growth during WAN outage. +- Config sync failures after maintenance. diff --git a/professional-infra/zabbix-monitoring-incident-response/examples/alert-evidence.txt b/professional-infra/zabbix-monitoring-incident-response/examples/alert-evidence.txt new file mode 100644 index 0000000..fca08e2 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/examples/alert-evidence.txt @@ -0,0 +1,4 @@ +2026-05-04 10:21:14 WARN zbx-proxy-bank01 proxy queue above threshold: 420 delayed values +2026-05-04 10:22:01 HIGH linux-app01 Root filesystem above 85 percent +2026-05-04 10:25:33 INFO linux-app01 filesystem cleanup completed, usage back to 74 percent +2026-05-04 10:30:12 WARN aix-core01 active check delayed, proxy connectivity validated diff --git a/professional-infra/zabbix-monitoring-incident-response/inventory/hosts.ini b/professional-infra/zabbix-monitoring-incident-response/inventory/hosts.ini new file mode 100644 index 0000000..f276a31 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/inventory/hosts.ini @@ -0,0 +1,12 @@ +[zabbix_server] +zbx-server01 ansible_connection=local + +[zabbix_proxy] +zbx-proxy-bank01 ansible_connection=local zabbix_proxy_mode=active +zbx-proxy-bank02 ansible_connection=local zabbix_proxy_mode=passive + +[zabbix_agents_linux] +linux-app01 ansible_connection=local zabbix_agent_mode=active + +[zabbix_agents_aix] +aix-core01 ansible_connection=local zabbix_agent_mode=active diff --git a/professional-infra/zabbix-monitoring-incident-response/playbooks/zabbix_agent.yml b/professional-infra/zabbix-monitoring-incident-response/playbooks/zabbix_agent.yml new file mode 100644 index 0000000..1d2743b --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/playbooks/zabbix_agent.yml @@ -0,0 +1,8 @@ +--- +- name: Configure Zabbix agents + hosts: zabbix_agents_linux:zabbix_agents_aix + become: true + gather_facts: false + + roles: + - role: zabbix_agent diff --git a/professional-infra/zabbix-monitoring-incident-response/playbooks/zabbix_proxy.yml b/professional-infra/zabbix-monitoring-incident-response/playbooks/zabbix_proxy.yml new file mode 100644 index 0000000..7b6f964 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/playbooks/zabbix_proxy.yml @@ -0,0 +1,8 @@ +--- +- name: Configure Zabbix proxy nodes + hosts: zabbix_proxy + become: true + gather_facts: false + + roles: + - role: zabbix_proxy diff --git a/professional-infra/zabbix-monitoring-incident-response/playbooks/zabbix_server.yml b/professional-infra/zabbix-monitoring-incident-response/playbooks/zabbix_server.yml new file mode 100644 index 0000000..0702996 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/playbooks/zabbix_server.yml @@ -0,0 +1,8 @@ +--- +- name: Configure Zabbix server control plane + hosts: zabbix_server + become: true + gather_facts: false + + roles: + - role: zabbix_server diff --git a/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_agent/defaults/main.yml b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_agent/defaults/main.yml new file mode 100644 index 0000000..17e7a06 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_agent/defaults/main.yml @@ -0,0 +1,7 @@ +--- +zabbix_agent_server: zbx-proxy-bank01 +zabbix_agent_server_active: zbx-proxy-bank01 +zabbix_agent_hostname: "{{ inventory_hostname }}" +zabbix_agent_mode: active +zabbix_agent_listen_port: 10050 +zabbix_agent_include_dir: /etc/zabbix/zabbix_agentd.d diff --git a/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_agent/tasks/main.yml b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_agent/tasks/main.yml new file mode 100644 index 0000000..6397e53 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_agent/tasks/main.yml @@ -0,0 +1,38 @@ +--- +- name: Validate agent mode + ansible.builtin.assert: + that: + - zabbix_agent_mode in ["active", "passive"] + fail_msg: "zabbix_agent_mode must be active or passive" + +- name: Create Zabbix agent include directory + ansible.builtin.file: + path: "{{ zabbix_agent_include_dir }}" + state: directory + owner: root + group: root + mode: "0755" + +- name: Render Zabbix agent configuration example + ansible.builtin.template: + src: zabbix_agentd.conf.j2 + dest: /etc/zabbix/zabbix_agentd.conf + owner: root + group: root + mode: "0644" + +- name: Render custom OS check keys + ansible.builtin.template: + src: os_checks.conf.j2 + dest: "{{ zabbix_agent_include_dir }}/os_checks.conf" + owner: root + group: root + mode: "0644" + +- name: Report agent check model + ansible.builtin.debug: + msg: + host: "{{ zabbix_agent_hostname }}" + mode: "{{ zabbix_agent_mode }}" + server: "{{ zabbix_agent_server }}" + server_active: "{{ zabbix_agent_server_active }}" diff --git a/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_agent/templates/os_checks.conf.j2 b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_agent/templates/os_checks.conf.j2 new file mode 100644 index 0000000..59a73f3 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_agent/templates/os_checks.conf.j2 @@ -0,0 +1,4 @@ +UserParameter=os.fs.discovery,echo '{"data":[]}' +UserParameter=os.cpu.runqueue,uptime +UserParameter=os.net.tcp_established,ss -tan state established | wc -l +UserParameter=os.process.count[*],pgrep -fc "$1" diff --git a/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_agent/templates/zabbix_agentd.conf.j2 b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_agent/templates/zabbix_agentd.conf.j2 new file mode 100644 index 0000000..7391833 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_agent/templates/zabbix_agentd.conf.j2 @@ -0,0 +1,5 @@ +Server={{ zabbix_agent_server }} +ServerActive={{ zabbix_agent_server_active }} +Hostname={{ zabbix_agent_hostname }} +ListenPort={{ zabbix_agent_listen_port }} +Include={{ zabbix_agent_include_dir }}/*.conf diff --git a/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_proxy/defaults/main.yml b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_proxy/defaults/main.yml new file mode 100644 index 0000000..7a8095a --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_proxy/defaults/main.yml @@ -0,0 +1,7 @@ +--- +zabbix_proxy_server: zbx-server01 +zabbix_proxy_hostname: "{{ inventory_hostname }}" +zabbix_proxy_mode: active +zabbix_proxy_database: zabbix_proxy +zabbix_proxy_config_frequency: 60 +zabbix_proxy_offline_buffer_hours: 24 diff --git a/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_proxy/tasks/main.yml b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_proxy/tasks/main.yml new file mode 100644 index 0000000..b8c2d07 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_proxy/tasks/main.yml @@ -0,0 +1,31 @@ +--- +- name: Validate proxy mode + ansible.builtin.assert: + that: + - zabbix_proxy_mode in ["active", "passive"] + fail_msg: "zabbix_proxy_mode must be active or passive" + +- name: Create Zabbix proxy config directory + ansible.builtin.file: + path: /etc/zabbix + state: directory + owner: root + group: root + mode: "0755" + +- name: Render Zabbix proxy configuration example + ansible.builtin.template: + src: zabbix_proxy.conf.j2 + dest: /etc/zabbix/zabbix_proxy.conf + owner: root + group: root + mode: "0644" + +- name: Report proxy operating model + ansible.builtin.debug: + msg: + proxy: "{{ zabbix_proxy_hostname }}" + server: "{{ zabbix_proxy_server }}" + mode: "{{ zabbix_proxy_mode }}" + active_checks: "{{ zabbix_proxy_mode == 'active' }}" + offline_buffer_hours: "{{ zabbix_proxy_offline_buffer_hours }}" diff --git a/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_proxy/templates/zabbix_proxy.conf.j2 b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_proxy/templates/zabbix_proxy.conf.j2 new file mode 100644 index 0000000..fd19c76 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_proxy/templates/zabbix_proxy.conf.j2 @@ -0,0 +1,6 @@ +Server={{ zabbix_proxy_server }} +Hostname={{ zabbix_proxy_hostname }} +ProxyMode={{ 0 if zabbix_proxy_mode == 'active' else 1 }} +DBName={{ zabbix_proxy_database }} +ConfigFrequency={{ zabbix_proxy_config_frequency }} +ProxyOfflineBuffer={{ zabbix_proxy_offline_buffer_hours }} diff --git a/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_server/defaults/main.yml b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_server/defaults/main.yml new file mode 100644 index 0000000..e4a6828 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_server/defaults/main.yml @@ -0,0 +1,7 @@ +--- +zabbix_server_listen_port: 10051 +zabbix_server_database: zabbix +zabbix_server_housekeeping_frequency: 1 +zabbix_server_cache_size: 256M +zabbix_server_trend_retention_days: 365 +zabbix_server_history_retention_days: 90 diff --git a/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_server/tasks/main.yml b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_server/tasks/main.yml new file mode 100644 index 0000000..4ffd874 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_server/tasks/main.yml @@ -0,0 +1,25 @@ +--- +- name: Create Zabbix server config directory + ansible.builtin.file: + path: /etc/zabbix + state: directory + owner: root + group: root + mode: "0755" + +- name: Render Zabbix server configuration example + ansible.builtin.template: + src: zabbix_server.conf.j2 + dest: /etc/zabbix/zabbix_server.conf + owner: root + group: root + mode: "0644" + +- name: Report Zabbix server maintenance settings + ansible.builtin.debug: + msg: + listen_port: "{{ zabbix_server_listen_port }}" + cache_size: "{{ zabbix_server_cache_size }}" + housekeeping_frequency: "{{ zabbix_server_housekeeping_frequency }}" + history_retention_days: "{{ zabbix_server_history_retention_days }}" + trend_retention_days: "{{ zabbix_server_trend_retention_days }}" diff --git a/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_server/templates/zabbix_server.conf.j2 b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_server/templates/zabbix_server.conf.j2 new file mode 100644 index 0000000..f8d019a --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/roles/zabbix_server/templates/zabbix_server.conf.j2 @@ -0,0 +1,5 @@ +ListenPort={{ zabbix_server_listen_port }} +DBName={{ zabbix_server_database }} +CacheSize={{ zabbix_server_cache_size }} +HousekeepingFrequency={{ zabbix_server_housekeeping_frequency }} +HistoryStorageDateIndex=1 diff --git a/professional-infra/zabbix-monitoring-incident-response/samples/aix-os-checks.json b/professional-infra/zabbix-monitoring-incident-response/samples/aix-os-checks.json new file mode 100644 index 0000000..445cb92 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/samples/aix-os-checks.json @@ -0,0 +1,13 @@ +{ + "host": "aix-core01", + "proxy": "zbx-proxy-bank01", + "mode": "active", + "checks": { + "aix.fs.root.pused": 68.2, + "aix.cpu.user": 17.5, + "aix.memory.free_mb": 8192, + "aix.net.errin": 0, + "aix.process.count[cron]": 1 + }, + "note": "Sample payload for review; AIX runtime is not emulated locally." +} diff --git a/professional-infra/zabbix-monitoring-incident-response/samples/linux-os-checks.json b/professional-infra/zabbix-monitoring-incident-response/samples/linux-os-checks.json new file mode 100644 index 0000000..7fa114a --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/samples/linux-os-checks.json @@ -0,0 +1,12 @@ +{ + "host": "linux-app01", + "proxy": "zbx-proxy-bank01", + "mode": "active", + "checks": { + "vfs.fs.size[/,pused]": 72.4, + "system.cpu.util[,idle]": 83.1, + "vm.memory.size[pavailable]": 41.7, + "net.if.in[eth0]": 184320, + "proc.num[sshd]": 2 + } +} diff --git a/professional-infra/zabbix-monitoring-incident-response/scripts/validate_assets.py b/professional-infra/zabbix-monitoring-incident-response/scripts/validate_assets.py new file mode 100644 index 0000000..cd28325 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/scripts/validate_assets.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Validate Zabbix portfolio template and sample assets.""" + +import json +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def load_json(path: Path) -> dict: + with path.open(encoding="utf-8") as handle: + return json.load(handle) + + +def validate_template(path: Path) -> None: + data = load_json(path) + for field in ["template", "items", "triggers"]: + if field not in data: + raise ValueError(f"{path}: missing {field}") + if not data["items"]: + raise ValueError(f"{path}: template must define at least one item") + for item in data["items"]: + for field in ["key", "name", "type", "value_type"]: + if field not in item: + raise ValueError(f"{path}: item missing {field}") + + +def validate_sample(path: Path) -> None: + data = load_json(path) + for field in ["host", "proxy", "mode", "checks"]: + if field not in data: + raise ValueError(f"{path}: missing {field}") + if data["mode"] not in ["active", "passive"]: + raise ValueError(f"{path}: mode must be active or passive") + if not data["checks"]: + raise ValueError(f"{path}: checks cannot be empty") + + +def main() -> None: + for path in sorted((ROOT / "templates").glob("*.json")): + validate_template(path) + for path in sorted((ROOT / "samples").glob("*.json")): + validate_sample(path) + print("Zabbix template and sample assets are valid") + + +if __name__ == "__main__": + main() diff --git a/professional-infra/zabbix-monitoring-incident-response/templates/aix-os-template.json b/professional-infra/zabbix-monitoring-incident-response/templates/aix-os-template.json new file mode 100644 index 0000000..b9dc8fa --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/templates/aix-os-template.json @@ -0,0 +1,16 @@ +{ + "template": "Template OS AIX - Portfolio Simple Checks", + "groups": ["Templates/Operating systems"], + "items": [ + {"key": "aix.fs.root.pused", "name": "AIX root filesystem usage percent", "type": "ZABBIX_AGENT_ACTIVE", "value_type": "FLOAT", "units": "%"}, + {"key": "aix.cpu.user", "name": "AIX CPU user percent", "type": "ZABBIX_AGENT_ACTIVE", "value_type": "FLOAT", "units": "%"}, + {"key": "aix.memory.free_mb", "name": "AIX free memory MB", "type": "ZABBIX_AGENT_ACTIVE", "value_type": "UNSIGNED", "units": "MB"}, + {"key": "aix.net.errin", "name": "AIX network input errors", "type": "ZABBIX_AGENT_ACTIVE", "value_type": "UNSIGNED"}, + {"key": "aix.process.count[cron]", "name": "AIX cron process count", "type": "ZABBIX_AGENT_ACTIVE", "value_type": "UNSIGNED"} + ], + "triggers": [ + {"name": "AIX root filesystem above 85 percent", "expression": "last(/Template OS AIX - Portfolio Simple Checks/aix.fs.root.pused)>85"}, + {"name": "AIX cron is not running", "expression": "last(/Template OS AIX - Portfolio Simple Checks/aix.process.count[cron])=0"} + ], + "notes": "AIX checks are represented as template keys and sample data. They are not executed locally in this repository." +} diff --git a/professional-infra/zabbix-monitoring-incident-response/templates/linux-os-template.json b/professional-infra/zabbix-monitoring-incident-response/templates/linux-os-template.json new file mode 100644 index 0000000..5eb2215 --- /dev/null +++ b/professional-infra/zabbix-monitoring-incident-response/templates/linux-os-template.json @@ -0,0 +1,16 @@ +{ + "template": "Template OS Linux - Portfolio Simple Checks", + "groups": ["Templates/Operating systems"], + "items": [ + {"key": "vfs.fs.size[/,pused]", "name": "Root filesystem usage percent", "type": "ZABBIX_AGENT", "value_type": "FLOAT", "units": "%"}, + {"key": "system.cpu.util[,idle]", "name": "CPU idle percent", "type": "ZABBIX_AGENT", "value_type": "FLOAT", "units": "%"}, + {"key": "vm.memory.size[pavailable]", "name": "Available memory percent", "type": "ZABBIX_AGENT", "value_type": "FLOAT", "units": "%"}, + {"key": "net.if.in[eth0]", "name": "Network inbound on eth0", "type": "ZABBIX_AGENT", "value_type": "UNSIGNED", "units": "bps"}, + {"key": "proc.num[sshd]", "name": "sshd process count", "type": "ZABBIX_AGENT", "value_type": "UNSIGNED"} + ], + "triggers": [ + {"name": "Root filesystem above 85 percent", "expression": "last(/Template OS Linux - Portfolio Simple Checks/vfs.fs.size[/,pused])>85"}, + {"name": "Low available memory", "expression": "last(/Template OS Linux - Portfolio Simple Checks/vm.memory.size[pavailable])<10"}, + {"name": "sshd is not running", "expression": "last(/Template OS Linux - Portfolio Simple Checks/proc.num[sshd])=0"} + ] +}