Files
portfolio/migration-validation-framework/reports/html_report.py
T

608 lines
19 KiB
Python
Raw Normal View History

"""
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)