feat: Add comprehensive enterprise Linux infrastructure portfolio with Ansible, Python, and ELK stack
CI Pipeline / lint-ansible (push) Waiting to run
CI Pipeline / test-python (push) Waiting to run
CI Pipeline / validate-docker (push) Waiting to run
CI Pipeline / security-scan (push) Waiting to run
CI Pipeline / documentation (push) Waiting to run
CI Pipeline / integration-test (push) Blocked by required conditions
CI Pipeline / lint-ansible (push) Waiting to run
CI Pipeline / test-python (push) Waiting to run
CI Pipeline / validate-docker (push) Waiting to run
CI Pipeline / security-scan (push) Waiting to run
CI Pipeline / documentation (push) Waiting to run
CI Pipeline / integration-test (push) Blocked by required conditions
This commit is contained in:
@@ -0,0 +1,608 @@
|
||||
"""
|
||||
HTML Report Generator
|
||||
|
||||
Generates comprehensive HTML reports from migration validation comparison results.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class HTMLReportGenerator:
|
||||
"""Generator for HTML migration validation reports."""
|
||||
|
||||
def __init__(self):
|
||||
self.css_styles = self.get_css_styles()
|
||||
self.js_scripts = self.get_js_scripts()
|
||||
|
||||
def generate_report(self, comparison: Dict[str, Any], output_file: str) -> str:
|
||||
"""Generate HTML report from comparison data."""
|
||||
logger.info(f"Generating HTML report: {output_file}")
|
||||
|
||||
html_content = self.build_html_content(comparison)
|
||||
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(html_content)
|
||||
|
||||
logger.info(f"HTML report generated: {output_file}")
|
||||
return output_file
|
||||
|
||||
def build_html_content(self, comparison: Dict[str, Any]) -> str:
|
||||
"""Build complete HTML content."""
|
||||
metadata = comparison.get("metadata", {})
|
||||
summary = comparison.get("summary", {})
|
||||
differences = comparison.get("differences", {})
|
||||
risk_assessment = comparison.get("risk_assessment", {})
|
||||
validation_results = comparison.get("validation_results", {})
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Migration Validation Report</title>
|
||||
<style>{self.css_styles}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Migration Validation Report</h1>
|
||||
<div class="report-meta">
|
||||
<p><strong>Report Generated:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||
<p><strong>Comparison ID:</strong> {metadata.get('comparison_id', 'N/A')}</p>
|
||||
<p><strong>Snapshot 1:</strong> {metadata.get('snapshot1', 'N/A')}</p>
|
||||
<p><strong>Snapshot 2:</strong> {metadata.get('snapshot2', 'N/A')}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="toc">
|
||||
<h2>Table of Contents</h2>
|
||||
<ul>
|
||||
<li><a href="#executive-summary">Executive Summary</a></li>
|
||||
<li><a href="#risk-assessment">Risk Assessment</a></li>
|
||||
<li><a href="#validation-results">Validation Results</a></li>
|
||||
<li><a href="#detailed-changes">Detailed Changes</a></li>
|
||||
<li><a href="#recommendations">Recommendations</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<section id="executive-summary">
|
||||
<h2>Executive Summary</h2>
|
||||
{self.generate_summary_section(summary)}
|
||||
</section>
|
||||
|
||||
<section id="risk-assessment">
|
||||
<h2>Risk Assessment</h2>
|
||||
{self.generate_risk_section(risk_assessment)}
|
||||
</section>
|
||||
|
||||
<section id="validation-results">
|
||||
<h2>Validation Results</h2>
|
||||
{self.generate_validation_section(validation_results)}
|
||||
</section>
|
||||
|
||||
<section id="detailed-changes">
|
||||
<h2>Detailed Changes</h2>
|
||||
{self.generate_changes_section(differences)}
|
||||
</section>
|
||||
|
||||
<section id="recommendations">
|
||||
<h2>Recommendations</h2>
|
||||
{self.generate_recommendations_section(risk_assessment)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>{self.js_scripts}</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
return html
|
||||
|
||||
def generate_summary_section(self, summary: Dict[str, Any]) -> str:
|
||||
"""Generate executive summary HTML."""
|
||||
total_systems = summary.get('total_systems', 0)
|
||||
systems_with_changes = summary.get('systems_with_changes', 0)
|
||||
total_changes = summary.get('total_changes', 0)
|
||||
|
||||
html = f"""
|
||||
<div class="summary-grid">
|
||||
<div class="summary-card">
|
||||
<h3>Systems Analyzed</h3>
|
||||
<div class="metric">{total_systems}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<h3>Systems with Changes</h3>
|
||||
<div class="metric">{systems_with_changes}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<h3>Total Changes</h3>
|
||||
<div class="metric">{total_changes}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Changes by Type</h3>
|
||||
<table class="changes-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Data Type</th>
|
||||
<th>Changes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>"""
|
||||
|
||||
for data_type, count in summary.get('changes_by_type', {}).items():
|
||||
html += f"""
|
||||
<tr>
|
||||
<td>{data_type.replace('_', ' ').title()}</td>
|
||||
<td>{count}</td>
|
||||
</tr>"""
|
||||
|
||||
html += """
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Most Affected Systems</h3>
|
||||
<table class="systems-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>System</th>
|
||||
<th>Changes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>"""
|
||||
|
||||
for system, count in summary.get('most_affected_systems', []):
|
||||
html += f"""
|
||||
<tr>
|
||||
<td>{system}</td>
|
||||
<td>{count}</td>
|
||||
</tr>"""
|
||||
|
||||
html += """
|
||||
</tbody>
|
||||
</table>"""
|
||||
|
||||
return html
|
||||
|
||||
def generate_risk_section(self, risk_assessment: Dict[str, Any]) -> str:
|
||||
"""Generate risk assessment HTML."""
|
||||
overall_risk = risk_assessment.get('overall_risk', 'unknown')
|
||||
risk_color = self.get_risk_color(overall_risk)
|
||||
|
||||
html = f"""
|
||||
<div class="risk-overview">
|
||||
<h3>Overall Risk Level</h3>
|
||||
<div class="risk-badge risk-{overall_risk}" style="background-color: {risk_color}">
|
||||
{overall_risk.upper()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Risk Factors</h3>
|
||||
<div class="risk-factors">"""
|
||||
|
||||
for factor in risk_assessment.get('risk_factors', []):
|
||||
factor_color = self.get_risk_color(self.get_risk_level_name(factor.get('level', 1)))
|
||||
html += f"""
|
||||
<div class="risk-factor">
|
||||
<div class="risk-badge risk-{self.get_risk_level_name(factor.get('level', 1))}" style="background-color: {factor_color}">
|
||||
{self.get_risk_level_name(factor.get('level', 1)).upper()}
|
||||
</div>
|
||||
<div class="risk-details">
|
||||
<strong>{factor.get('type', 'Unknown').replace('_', ' ').title()}</strong>
|
||||
<p>{factor.get('description', 'No description')}</p>
|
||||
</div>
|
||||
</div>"""
|
||||
|
||||
html += """
|
||||
</div>
|
||||
|
||||
<h3>Critical Changes</h3>
|
||||
<div class="critical-changes">"""
|
||||
|
||||
for change in risk_assessment.get('critical_changes', []):
|
||||
html += f"""
|
||||
<div class="critical-change">
|
||||
<h4>{change.get('system', 'Unknown System')}</h4>
|
||||
<p><strong>Type:</strong> {change.get('data_type', 'Unknown')}</p>
|
||||
<p>{change.get('factor', {}).get('description', 'No details')}</p>
|
||||
</div>"""
|
||||
|
||||
html += "</div>"
|
||||
|
||||
return html
|
||||
|
||||
def generate_validation_section(self, validation_results: Dict[str, Any]) -> str:
|
||||
"""Generate validation results HTML."""
|
||||
passed = validation_results.get('passed', False)
|
||||
status_color = "#28a745" if passed else "#dc3545"
|
||||
status_text = "PASSED" if passed else "FAILED"
|
||||
|
||||
html = f"""
|
||||
<div class="validation-status">
|
||||
<div class="status-indicator" style="background-color: {status_color}">
|
||||
{status_text}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Validation Checks</h3>
|
||||
<div class="validation-checks">"""
|
||||
|
||||
for check in validation_results.get('checks', []):
|
||||
check_status = "✓" if check.get('passed', False) else "✗"
|
||||
check_color = "#28a745" if check.get('passed', False) else "#dc3545"
|
||||
|
||||
html += f"""
|
||||
<div class="validation-check">
|
||||
<div class="check-status" style="color: {check_color}">{check_status}</div>
|
||||
<div class="check-details">
|
||||
<h4>{check.get('name', 'Unknown').replace('_', ' ').title()}</h4>
|
||||
<p>{check.get('description', 'No description')}</p>
|
||||
{"<ul>" + "".join(f"<li>{detail}</li>" for detail in check.get('details', [])) + "</ul>" if check.get('details') else ""}
|
||||
</div>
|
||||
</div>"""
|
||||
|
||||
html += "</div>"
|
||||
|
||||
return html
|
||||
|
||||
def generate_changes_section(self, differences: Dict[str, Any]) -> str:
|
||||
"""Generate detailed changes HTML."""
|
||||
html = ""
|
||||
|
||||
for data_type, systems in differences.items():
|
||||
html += f"<h3>{data_type.replace('_', ' ').title()} Changes</h3>"
|
||||
|
||||
for system, system_diffs in systems.items():
|
||||
html += f"<h4>System: {system}</h4>"
|
||||
|
||||
# Generate tables for different change types
|
||||
html += self.generate_change_tables(system_diffs)
|
||||
|
||||
return html
|
||||
|
||||
def generate_change_tables(self, system_diffs: Dict[str, Any]) -> str:
|
||||
"""Generate HTML tables for different types of changes."""
|
||||
html = ""
|
||||
|
||||
change_types = {
|
||||
'added_mounts': ('Added Mounts', ['mountpoint', 'device', 'fstype']),
|
||||
'removed_mounts': ('Removed Mounts', ['mountpoint', 'device', 'fstype']),
|
||||
'changed_mounts': ('Changed Mounts', ['mountpoint', 'before', 'after']),
|
||||
'usage_changes': ('Usage Changes', ['mountpoint', 'before', 'after']),
|
||||
'added_services': ('Added Services', ['name', 'active_state', 'sub_state']),
|
||||
'removed_services': ('Removed Services', ['name', 'active_state', 'sub_state']),
|
||||
'status_changes': ('Service Status Changes', ['name', 'before', 'after']),
|
||||
'filesystem_changes': ('Filesystem Changes', ['mountpoint', 'before', 'after']),
|
||||
'directory_size_changes': ('Directory Size Changes', ['path', 'before', 'after']),
|
||||
'significant_usage_changes': ('Significant Usage Changes', ['mountpoint', 'change_percent', 'before', 'after'])
|
||||
}
|
||||
|
||||
for change_key, (title, columns) in change_types.items():
|
||||
if change_key in system_diffs and system_diffs[change_key]:
|
||||
html += f"<h5>{title}</h5>"
|
||||
html += '<table class="changes-table">'
|
||||
html += '<thead><tr>'
|
||||
|
||||
for col in columns:
|
||||
html += f'<th>{col.replace("_", " ").title()}</th>'
|
||||
|
||||
html += '</tr></thead><tbody>'
|
||||
|
||||
for item in system_diffs[change_key]:
|
||||
html += '<tr>'
|
||||
|
||||
for col in columns:
|
||||
value = item.get(col, '')
|
||||
if isinstance(value, dict):
|
||||
value = json.dumps(value, indent=2)
|
||||
html += f'<td><pre>{value}</pre></td>'
|
||||
|
||||
html += '</tr>'
|
||||
|
||||
html += '</tbody></table>'
|
||||
|
||||
return html
|
||||
|
||||
def generate_recommendations_section(self, risk_assessment: Dict[str, Any]) -> str:
|
||||
"""Generate recommendations HTML."""
|
||||
recommendations = risk_assessment.get('recommendations', [])
|
||||
|
||||
html = '<div class="recommendations">'
|
||||
|
||||
if not recommendations:
|
||||
html += '<p>No specific recommendations at this time.</p>'
|
||||
else:
|
||||
html += '<ul>'
|
||||
for rec in recommendations:
|
||||
html += f'<li>{rec}</li>'
|
||||
html += '</ul>'
|
||||
|
||||
html += '</div>'
|
||||
|
||||
return html
|
||||
|
||||
def get_risk_color(self, risk_level: str) -> str:
|
||||
"""Get color for risk level."""
|
||||
colors = {
|
||||
'low': '#28a745',
|
||||
'medium': '#ffc107',
|
||||
'high': '#fd7e14',
|
||||
'critical': '#dc3545'
|
||||
}
|
||||
return colors.get(risk_level.lower(), '#6c757d')
|
||||
|
||||
def get_risk_level_name(self, level: int) -> str:
|
||||
"""Get risk level name from numeric level."""
|
||||
levels = {1: 'low', 2: 'medium', 3: 'high', 4: 'critical'}
|
||||
return levels.get(level, 'unknown')
|
||||
|
||||
def get_css_styles(self) -> str:
|
||||
"""Get CSS styles for the report."""
|
||||
return """
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.report-meta {
|
||||
color: #6c757d;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.toc {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.toc ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.toc li {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.toc a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.toc a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #495057;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.risk-overview {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.risk-badge {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.risk-factors, .critical-changes {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.risk-factor, .critical-change {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin: 10px 0;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.risk-factor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.risk-details {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.validation-status {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
padding: 15px 30px;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.validation-checks {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.validation-check {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.check-status {
|
||||
font-size: 1.5em;
|
||||
margin-right: 15px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.check-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.recommendations ul {
|
||||
background: #e7f3ff;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.recommendations li {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.risk-factor {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.validation-check {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def get_js_scripts(self) -> str:
|
||||
"""Get JavaScript for interactive features."""
|
||||
return """
|
||||
// Add smooth scrolling to TOC links
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const links = document.querySelectorAll('.toc a');
|
||||
links.forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add collapsible sections for large tables
|
||||
const tables = document.querySelectorAll('table');
|
||||
tables.forEach(table => {
|
||||
if (table.rows.length > 10) {
|
||||
const toggle = document.createElement('button');
|
||||
toggle.textContent = 'Toggle Details';
|
||||
toggle.style.marginBottom = '10px';
|
||||
toggle.addEventListener('click', function() {
|
||||
const tbody = table.querySelector('tbody');
|
||||
tbody.style.display = tbody.style.display === 'none' ? '' : 'none';
|
||||
});
|
||||
table.parentNode.insertBefore(toggle, table);
|
||||
}
|
||||
});
|
||||
});
|
||||
"""
|
||||
|
||||
def generate(comparison: Dict[str, Any], output_file: str) -> str:
|
||||
"""Main function to generate HTML report."""
|
||||
generator = HTMLReportGenerator()
|
||||
return generator.generate_report(comparison, output_file)
|
||||
Reference in New Issue
Block a user