poc1028c

Young-Kyoo Kim·2025년 10월 27일
#!/usr/bin/env python3
"""
MinIO Maintenance Group Planner

This script analyzes MinIO erasure set distribution and generates optimal
node groupings for safe maintenance operations. It determines which nodes
can be taken down simultaneously without risking data availability.
"""

import argparse
import json
import re
import subprocess
import sys
from collections import defaultdict, Counter
from typing import Dict, List, Set, Tuple
import itertools


def natural_sort_key(text):
    """Generate a key for natural sorting that handles numbers correctly."""
    def atoi(text):
        return int(text) if text.isdigit() else text
    return [atoi(c) for c in re.split(r'(\d+)', text)]


def run_mc_command(alias: str, insecure: bool = False) -> Dict:
    """Run mc admin info command and return JSON output"""
    cmd = f"mc admin info {alias} --json"
    if insecure:
        cmd += " --insecure"
    
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"Error running mc command: {result.stderr}", file=sys.stderr)
        sys.exit(1)
    
    try:
        return json.loads(result.stdout)
    except json.JSONDecodeError as e:
        print(f"Error parsing mc output as JSON: {e}", file=sys.stderr)
        sys.exit(1)


def get_pod_node_mapping(namespace: str, objectstore_name: str = None) -> Dict[str, str]:
    """Get mapping of pod names to node names using kubectl"""
    pod_to_node = {}
    
    label_selectors = [
        f'v1.min.io/tenant={objectstore_name}' if objectstore_name else None,
        'app=minio',
        'app.kubernetes.io/name=minio',
    ]
    
    for selector in label_selectors:
        if not selector:
            continue
        try:
            result = subprocess.run(
                ['kubectl', '-n', namespace, 'get', 'pods', '-l', selector, '-o', 'json'],
                capture_output=True,
                text=True,
                check=True
            )
            
            pods_info = json.loads(result.stdout)
            items = pods_info.get('items', [])
            
            if items:
                for pod in items:
                    pod_name = pod['metadata']['name']
                    node_name = pod['spec'].get('nodeName', 'unknown')
                    if objectstore_name:
                        if objectstore_name in pod_name or 'minio' in pod_name.lower():
                            pod_to_node[pod_name] = node_name
                    else:
                        if 'minio' in pod_name.lower():
                            pod_to_node[pod_name] = node_name
                
                if pod_to_node:
                    break
                    
        except (subprocess.CalledProcessError, json.JSONDecodeError):
            continue
    
    if not pod_to_node:
        try:
            result = subprocess.run(
                ['kubectl', '-n', namespace, 'get', 'pods', '-o', 'json'],
                capture_output=True,
                text=True,
                check=True
            )
            
            pods_info = json.loads(result.stdout)
            for pod in pods_info.get('items', []):
                pod_name = pod['metadata']['name']
                if 'minio' in pod_name.lower() or (objectstore_name and objectstore_name in pod_name):
                    node_name = pod['spec'].get('nodeName', 'unknown')
                    pod_to_node[pod_name] = node_name
                    
        except (subprocess.CalledProcessError, json.JSONDecodeError):
            pass
    
    return pod_to_node


def analyze_erasure_sets(mc_info: Dict, pod_to_node: Dict[str, str] = None) -> Dict:
    """Analyze erasure set distribution from mc admin info output"""
    
    servers = mc_info.get('info', {}).get('servers', [])
    
    if not servers:
        print("Error: No servers found in mc admin info output", file=sys.stderr)
        return {}
    
    erasure_sets = defaultdict(list)
    
    for server in servers:
        endpoint = server.get('endpoint', '')
        
        # Extract pod/node name
        pod_or_node_name = endpoint.split(':')[0] if ':' in endpoint else endpoint
        pod_or_node_name = pod_or_node_name.split('.')[0] if '.' in pod_or_node_name else pod_or_node_name
        
        # Determine actual node
        actual_node = 'unknown'
        
        if pod_to_node and pod_or_node_name in pod_to_node:
            actual_node = pod_to_node[pod_or_node_name]
        else:
            if pod_to_node:
                for pod_name, node_name in pod_to_node.items():
                    if (pod_name == pod_or_node_name or 
                        pod_or_node_name in pod_name or
                        pod_name in pod_or_node_name):
                        actual_node = node_name
                        break
        
        if actual_node == 'unknown':
            actual_node = pod_or_node_name
        
        # Process drives
        drives = server.get('drives', [])
        for drive in drives:
            erasure_set_id = drive.get('set_index')
            if erasure_set_id is None:
                erasure_set_id = drive.get('set')
            
            if erasure_set_id is not None:
                erasure_sets[erasure_set_id].append({
                    'node': actual_node,
                    'drive': drive.get('path', ''),
                    'state': drive.get('state', 'unknown'),
                })
    
    return erasure_sets


def calculate_erasure_set_tolerance(total_drives: int) -> int:
    """Calculate how many drives can be lost in an erasure set"""
    if total_drives >= 16:
        parity = total_drives // 2
    elif total_drives >= 8:
        parity = total_drives // 2
    else:
        parity = total_drives // 2
    
    return parity


def check_group_safety(node_group: Set[str], erasure_sets: Dict, 
                       max_risk_level: str = 'degraded') -> Tuple[bool, Dict]:
    """Check if taking down a group of nodes is safe"""
    
    impact = {}
    is_safe = True
    
    for set_id, drives in erasure_sets.items():
        total_drives = len(drives)
        affected_drives = sum(1 for d in drives if d['node'] in node_group)
        parity = calculate_erasure_set_tolerance(total_drives)
        data_drives = total_drives - parity
        healthy_drives = total_drives - affected_drives
        
        # Determine status
        if affected_drives == 0:
            status = 'healthy'
        elif affected_drives <= parity:
            status = 'degraded'
        elif healthy_drives >= data_drives:
            status = 'critical'
        else:
            status = 'failed'
        
        impact[set_id] = {
            'status': status,
            'affected': affected_drives,
            'total': total_drives,
            'parity': parity
        }
        
        # Check against max risk level
        risk_levels = {'healthy': 0, 'degraded': 1, 'critical': 2, 'failed': 3}
        if risk_levels[status] > risk_levels[max_risk_level]:
            is_safe = False
    
    return is_safe, impact


def find_conflicting_nodes(node: str, erasure_sets: Dict, max_tolerance: int) -> Set[str]:
    """Find nodes that conflict with the given node (share too many erasure sets)"""
    
    # Find which erasure sets this node participates in
    node_sets = set()
    for set_id, drives in erasure_sets.items():
        if any(d['node'] == node for d in drives):
            node_sets.add(set_id)
    
    # Find nodes that share erasure sets and would exceed tolerance
    conflicting_nodes = set()
    
    for other_node in get_all_nodes(erasure_sets):
        if other_node == node:
            continue
        
        # Count shared erasure sets
        shared_sets = []
        for set_id in node_sets:
            drives = erasure_sets[set_id]
            if any(d['node'] == other_node for d in drives):
                shared_sets.append(set_id)
        
        # Check if taking both nodes down would be unsafe
        if shared_sets:
            test_group = {node, other_node}
            is_safe, _ = check_group_safety(test_group, erasure_sets, 'degraded')
            if not is_safe:
                conflicting_nodes.add(other_node)
    
    return conflicting_nodes


def get_all_nodes(erasure_sets: Dict) -> List[str]:
    """Get all unique nodes from erasure sets"""
    nodes = set()
    for drives in erasure_sets.values():
        for drive in drives:
            nodes.add(drive['node'])
    return sorted(nodes, key=natural_sort_key)


def greedy_group_assignment(all_nodes: List[str], erasure_sets: Dict, 
                           max_risk_level: str = 'degraded') -> List[Set[str]]:
    """Greedy algorithm to assign nodes to groups"""
    
    groups = []
    remaining_nodes = set(all_nodes)
    
    while remaining_nodes:
        # Start a new group with the first remaining node
        current_group = set()
        nodes_to_try = sorted(remaining_nodes, key=natural_sort_key)
        
        for node in nodes_to_try:
            # Try adding this node to the current group
            test_group = current_group | {node}
            is_safe, _ = check_group_safety(test_group, erasure_sets, max_risk_level)
            
            if is_safe:
                current_group.add(node)
        
        # Add the group and remove nodes from remaining
        if current_group:
            groups.append(current_group)
            remaining_nodes -= current_group
        else:
            # If we can't add any node safely, something is wrong
            # Add single node and continue
            node = nodes_to_try[0]
            groups.append({node})
            remaining_nodes.remove(node)
    
    return groups


def optimize_groups(groups: List[Set[str]], erasure_sets: Dict, 
                   max_risk_level: str = 'degraded') -> List[Set[str]]:
    """Try to merge groups to minimize the total number of groups"""
    
    improved = True
    while improved:
        improved = False
        
        # Try to merge each pair of groups
        for i in range(len(groups)):
            for j in range(i + 1, len(groups)):
                merged = groups[i] | groups[j]
                is_safe, _ = check_group_safety(merged, erasure_sets, max_risk_level)
                
                if is_safe:
                    # Merge groups
                    new_groups = [groups[k] for k in range(len(groups)) if k != i and k != j]
                    new_groups.append(merged)
                    groups = new_groups
                    improved = True
                    break
            
            if improved:
                break
    
    return groups


def generate_html_report(groups: List[Set[str]], erasure_sets: Dict, 
                        all_nodes: List[str], max_risk_level: str,
                        objectstore_name: str = "MinIO") -> str:
    """Generate HTML report for maintenance groups"""
    
    # Calculate impact for each group
    group_impacts = []
    for i, group in enumerate(groups):
        is_safe, impact = check_group_safety(group, erasure_sets, max_risk_level)
        group_impacts.append({
            'group_id': i,
            'nodes': sorted(group, key=natural_sort_key),
            'is_safe': is_safe,
            'impact': impact
        })
    
    html = f"""<!DOCTYPE html>
<html>
<head>
    <title>MinIO Maintenance Plan - {objectstore_name}</title>
    <meta charset="UTF-8">
    <style>
        * {{
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }}
        
        body {{
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }}
        
        .container {{
            max-width: 1600px;
            margin: 0 auto;
            background: white;
            border-radius: 20px;
            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
            overflow: hidden;
        }}
        
        .header {{
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 40px;
            text-align: center;
        }}
        
        .header h1 {{
            font-size: 2.5em;
            margin-bottom: 10px;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
        }}
        
        .header .subtitle {{
            font-size: 1.2em;
            opacity: 0.9;
        }}
        
        .summary {{
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
            gap: 20px;
            padding: 30px;
            background: #f8f9fa;
        }}
        
        .summary-card {{
            background: white;
            padding: 25px;
            border-radius: 15px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
            transition: transform 0.3s ease;
        }}
        
        .summary-card:hover {{
            transform: translateY(-5px);
        }}
        
        .summary-card .label {{
            font-size: 0.9em;
            color: #666;
            text-transform: uppercase;
            letter-spacing: 1px;
            margin-bottom: 10px;
        }}
        
        .summary-card .value {{
            font-size: 2.5em;
            font-weight: bold;
            color: #667eea;
        }}
        
        .info-box {{
            background: #e7f3ff;
            border-left: 4px solid #2196f3;
            padding: 20px;
            margin: 20px 30px;
            border-radius: 10px;
        }}
        
        .info-box h3 {{
            color: #1976d2;
            margin-bottom: 10px;
        }}
        
        .maintenance-groups {{
            padding: 30px;
        }}
        
        .maintenance-groups h2 {{
            font-size: 2em;
            margin-bottom: 25px;
            color: #333;
            border-bottom: 3px solid #667eea;
            padding-bottom: 10px;
        }}
        
        .timeline {{
            position: relative;
            padding: 20px 0;
        }}
        
        .timeline::before {{
            content: '';
            position: absolute;
            left: 50%;
            top: 0;
            bottom: 0;
            width: 4px;
            background: #e0e0e0;
            transform: translateX(-50%);
        }}
        
        .group-card {{
            position: relative;
            margin: 40px 0;
            display: grid;
            grid-template-columns: 1fr 80px 1fr;
            gap: 20px;
            align-items: center;
        }}
        
        .group-card:nth-child(even) .group-content {{
            grid-column: 3;
        }}
        
        .group-card:nth-child(even) .timeline-marker {{
            grid-column: 2;
        }}
        
        .group-card:nth-child(even) .group-info {{
            grid-column: 1;
        }}
        
        .group-card:nth-child(odd) .group-content {{
            grid-column: 1;
        }}
        
        .group-card:nth-child(odd) .timeline-marker {{
            grid-column: 2;
        }}
        
        .group-card:nth-child(odd) .group-info {{
            grid-column: 3;
        }}
        
        .timeline-marker {{
            width: 80px;
            height: 80px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 1.5em;
            font-weight: bold;
            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
            z-index: 1;
            position: relative;
        }}
        
        .group-content {{
            background: white;
            padding: 25px;
            border-radius: 15px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.1);
            border: 2px solid #e0e0e0;
            transition: all 0.3s ease;
        }}
        
        .group-content:hover {{
            transform: translateY(-5px);
            box-shadow: 0 8px 20px rgba(0,0,0,0.15);
            border-color: #667eea;
        }}
        
        .group-header {{
            margin-bottom: 20px;
        }}
        
        .group-title {{
            font-size: 1.5em;
            font-weight: bold;
            color: #333;
            margin-bottom: 10px;
        }}
        
        .group-subtitle {{
            color: #666;
            font-size: 0.95em;
        }}
        
        .nodes-grid {{
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
            gap: 10px;
            margin-top: 15px;
        }}
        
        .node-badge {{
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 10px 15px;
            border-radius: 8px;
            text-align: center;
            font-weight: 500;
            transition: transform 0.2s ease;
        }}
        
        .node-badge:hover {{
            transform: scale(1.05);
        }}
        
        .group-info {{
            background: #f8f9fa;
            padding: 20px;
            border-radius: 15px;
            border: 2px solid #e0e0e0;
        }}
        
        .info-item {{
            margin-bottom: 15px;
        }}
        
        .info-label {{
            font-size: 0.85em;
            color: #666;
            text-transform: uppercase;
            letter-spacing: 1px;
            margin-bottom: 5px;
        }}
        
        .info-value {{
            font-size: 1.2em;
            font-weight: bold;
            color: #333;
        }}
        
        .status-indicator {{
            display: inline-block;
            padding: 5px 12px;
            border-radius: 20px;
            font-size: 0.85em;
            font-weight: 600;
            margin-top: 5px;
        }}
        
        .status-safe {{
            background: #d4edda;
            color: #155724;
        }}
        
        .status-warning {{
            background: #fff3cd;
            color: #856404;
        }}
        
        .erasure-summary {{
            margin-top: 20px;
            padding-top: 20px;
            border-top: 1px solid #e0e0e0;
        }}
        
        .erasure-stats {{
            display: flex;
            gap: 15px;
            flex-wrap: wrap;
        }}
        
        .erasure-stat {{
            background: white;
            padding: 8px 15px;
            border-radius: 8px;
            border: 2px solid #e0e0e0;
            font-size: 0.9em;
        }}
        
        .erasure-stat.healthy {{
            border-color: #28a745;
            background: #d4edda;
        }}
        
        .erasure-stat.degraded {{
            border-color: #ffc107;
            background: #fff3cd;
        }}
        
        .instructions {{
            background: #fff3cd;
            border-left: 4px solid #ffc107;
            padding: 20px;
            margin: 30px;
            border-radius: 10px;
        }}
        
        .instructions h3 {{
            color: #856404;
            margin-bottom: 15px;
        }}
        
        .instructions ol {{
            margin-left: 20px;
        }}
        
        .instructions li {{
            margin: 10px 0;
            color: #856404;
        }}
        
        @media (max-width: 1200px) {{
            .timeline::before {{
                display: none;
            }}
            
            .group-card {{
                grid-template-columns: 1fr !important;
            }}
            
            .group-card > * {{
                grid-column: 1 !important;
            }}
            
            .timeline-marker {{
                margin: 0 auto;
            }}
        }}
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>🔧 Maintenance Group Plan</h1>
            <div class="subtitle">{objectstore_name} - Safe Node Grouping Strategy</div>
        </div>
        
        <div class="summary">
            <div class="summary-card">
                <div class="label">Total Nodes</div>
                <div class="value">{len(all_nodes)}</div>
            </div>
            <div class="summary-card">
                <div class="label">Maintenance Groups</div>
                <div class="value">{len(groups)}</div>
            </div>
            <div class="summary-card">
                <div class="label">Erasure Sets</div>
                <div class="value">{len(erasure_sets)}</div>
            </div>
            <div class="summary-card">
                <div class="label">Max Risk Level</div>
                <div class="value" style="font-size: 1.5em; text-transform: uppercase;">{max_risk_level}</div>
            </div>
        </div>
        
        <div class="info-box">
            <h3>📋 Maintenance Strategy</h3>
            <p>The cluster has been divided into <strong>{len(groups)} groups</strong> that can be safely taken offline for maintenance sequentially. 
            Each group is guaranteed not to exceed the <strong>{max_risk_level.upper()}</strong> risk level when taken offline simultaneously.</p>
        </div>
        
        <div class="maintenance-groups">
            <h2>Maintenance Groups Timeline</h2>
            <div class="timeline">
"""
    
    for group_info in group_impacts:
        group_id = group_info['group_id']
        nodes = group_info['nodes']
        impact = group_info['impact']
        is_safe = group_info['is_safe']
        
        # Count impact statuses
        status_counts = Counter(info['status'] for info in impact.values())
        
        html += f"""
                <div class="group-card">
                    <div class="timeline-marker">{group_id + 1}</div>
                    <div class="group-content">
                        <div class="group-header">
                            <div class="group-title">Group {group_id + 1}</div>
                            <div class="group-subtitle">{len(nodes)} nodes can be maintained together</div>
                        </div>
                        <div class="nodes-grid">
"""
        
        for node in nodes:
            html += f'                            <div class="node-badge">{node}</div>\n'
        
        html += """
                        </div>
                        <div class="erasure-summary">
                            <div class="erasure-stats">
"""
        
        if status_counts.get('healthy', 0) > 0:
            html += f'                                <div class="erasure-stat healthy">✅ {status_counts["healthy"]} Healthy</div>\n'
        if status_counts.get('degraded', 0) > 0:
            html += f'                                <div class="erasure-stat degraded">⚠️ {status_counts["degraded"]} Degraded</div>\n'
        
        html += """
                            </div>
                        </div>
                    </div>
                    <div class="group-info">
                        <div class="info-item">
                            <div class="info-label">Nodes in Group</div>
                            <div class="info-value">{}</div>
                        </div>
                        <div class="info-item">
                            <div class="info-label">Safety Status</div>
                            <div class="status-indicator status-safe">✅ SAFE</div>
                        </div>
                        <div class="info-item">
                            <div class="info-label">Affected Sets</div>
                            <div class="info-value">{}</div>
                        </div>
                    </div>
                </div>
""".format(len(nodes), sum(1 for info in impact.values() if info['status'] != 'healthy'))
    
    html += """
            </div>
        </div>
        
        <div class="instructions">
            <h3>📖 Maintenance Execution Plan</h3>
            <ol>
                <li><strong>Verify cluster health</strong> before starting maintenance</li>
                <li><strong>Take down Group 1 nodes</strong> for maintenance</li>
                <li><strong>Perform maintenance operations</strong> (updates, hardware replacement, etc.)</li>
                <li><strong>Bring Group 1 nodes back online</strong> and verify health</li>
                <li><strong>Wait for data rebalancing</strong> if necessary</li>
                <li><strong>Repeat for subsequent groups</strong> (Group 2, 3, ...)</li>
                <li><strong>Final verification</strong> after all groups are complete</li>
            </ol>
            <p style="margin-top: 15px;"><strong>⚠️ Important:</strong> Always complete maintenance for one group and verify cluster health before proceeding to the next group.</p>
        </div>
    </div>
</body>
</html>
"""
    
    return html


def generate_text_report(groups: List[Set[str]], erasure_sets: Dict, 
                        max_risk_level: str) -> str:
    """Generate text-based maintenance plan report"""
    
    report = []
    report.append("=" * 80)
    report.append("MinIO Maintenance Group Plan")
    report.append("=" * 80)
    report.append("")
    
    report.append(f"📊 SUMMARY:")
    report.append(f"  Total Nodes: {sum(len(g) for g in groups)}")
    report.append(f"  Maintenance Groups: {len(groups)}")
    report.append(f"  Erasure Sets: {len(erasure_sets)}")
    report.append(f"  Max Risk Level: {max_risk_level.upper()}")
    report.append("")
    
    report.append("🔧 MAINTENANCE GROUPS:")
    report.append("-" * 80)
    
    for i, group in enumerate(groups, 1):
        report.append(f"\n📦 Group {i} ({len(group)} nodes):")
        
        # List nodes
        for node in sorted(group, key=natural_sort_key):
            report.append(f"  • {node}")
        
        # Check impact
        is_safe, impact = check_group_safety(group, erasure_sets, max_risk_level)
        
        # Count statuses
        status_counts = Counter(info['status'] for info in impact.values())
        
        report.append(f"\n  Impact Analysis:")
        report.append(f"    Safety: {'✅ SAFE' if is_safe else '❌ UNSAFE'}")
        report.append(f"    Healthy Sets: {status_counts.get('healthy', 0)}")
        report.append(f"    Degraded Sets: {status_counts.get('degraded', 0)}")
        report.append(f"    Critical Sets: {status_counts.get('critical', 0)}")
        report.append(f"    Failed Sets: {status_counts.get('failed', 0)}")
    
    report.append("")
    report.append("=" * 80)
    report.append("📖 EXECUTION PLAN:")
    report.append("-" * 80)
    
    for i in range(len(groups)):
        report.append(f"\nStep {i + 1}: Maintain Group {i + 1}")
        report.append(f"  1. Verify cluster health")
        report.append(f"  2. Take down Group {i + 1} nodes")
        report.append(f"  3. Perform maintenance")
        report.append(f"  4. Bring nodes back online")
        report.append(f"  5. Verify cluster health")
        if i < len(groups) - 1:
            report.append(f"  6. Proceed to Group {i + 2}")
    
    report.append("")
    report.append("=" * 80)
    report.append("⚠️  IMPORTANT NOTES:")
    report.append("  • Always complete one group before starting the next")
    report.append("  • Verify cluster health between groups")
    report.append("  • Monitor erasure set status during maintenance")
    report.append("  • Have a rollback plan ready")
    report.append("=" * 80)
    
    return "\n".join(report)


def main():
    parser = argparse.ArgumentParser(
        description='Generate optimal maintenance groups for MinIO cluster',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Generate maintenance groups allowing degraded state
  %(prog)s --mc-alias myminio --max-risk degraded
  
  # Generate conservative groups (healthy only)
  %(prog)s --mc-alias myminio --max-risk healthy
  
  # With HTML output
  %(prog)s --mc-alias myminio --max-risk degraded --html-output plan.html
  
  # With namespace for better mapping
  %(prog)s --mc-alias myminio --namespace minio-tenant --name minio \\
           --max-risk degraded --html-output plan.html --json-output plan.json
        """
    )
    
    parser.add_argument('--mc-alias', required=True,
                       help='MinIO mc alias for admin info command')
    parser.add_argument('--max-risk', choices=['healthy', 'degraded', 'critical'],
                       default='degraded',
                       help='Maximum acceptable risk level when nodes are down (default: degraded)')
    parser.add_argument('--namespace', '-n',
                       help='Kubernetes namespace (for better pod-to-node mapping)')
    parser.add_argument('--name',
                       help='ObjectStore name (for better pod-to-node mapping)')
    parser.add_argument('--html-output',
                       help='Generate HTML maintenance plan to specified file')
    parser.add_argument('--json-output',
                       help='Save maintenance plan as JSON')
    parser.add_argument('--insecure', action='store_true',
                       help='Use --insecure flag with mc command')
    parser.add_argument('--algorithm', choices=['greedy', 'optimal'],
                       default='greedy',
                       help='Grouping algorithm: greedy (fast) or optimal (slower, better) (default: greedy)')
    
    args = parser.parse_args()
    
    # Get MinIO server info
    print("🔍 Fetching MinIO server information...")
    mc_info = run_mc_command(args.mc_alias, args.insecure)
    
    # Get pod-to-node mapping if namespace provided
    pod_to_node = None
    if args.namespace:
        print(f"📍 Fetching pod-to-node mapping from namespace '{args.namespace}'...")
        pod_to_node = get_pod_node_mapping(args.namespace, args.name)
        if pod_to_node:
            print(f"   Found {len(pod_to_node)} pods")
    
    # Analyze erasure sets
    print("📊 Analyzing erasure set configuration...")
    erasure_sets = analyze_erasure_sets(mc_info, pod_to_node)
    
    if not erasure_sets:
        print("Error: No erasure sets found", file=sys.stderr)
        sys.exit(1)
    
    print(f"   Found {len(erasure_sets)} erasure sets")
    
    # Get all nodes
    all_nodes = get_all_nodes(erasure_sets)
    print(f"   Total nodes in cluster: {len(all_nodes)}")
    
    # Analyze erasure set configuration
    print(f"\n📋 Erasure Set Configuration:")
    drive_counts = [len(drives) for drives in erasure_sets.values()]
    if drive_counts:
        min_drives = min(drive_counts)
        max_drives = max(drive_counts)
        avg_drives = sum(drive_counts) / len(drive_counts)
        print(f"   Drives per set: min={min_drives}, max={max_drives}, avg={avg_drives:.1f}")
        
        # Show typical tolerance
        typical_tolerance = calculate_erasure_set_tolerance(int(avg_drives))
        print(f"   Typical fault tolerance: {typical_tolerance} drives per set")
    
    # Generate maintenance groups
    print(f"\n🔧 Generating maintenance groups (max risk: {args.max_risk.upper()})...")
    
    groups = greedy_group_assignment(all_nodes, erasure_sets, args.max_risk)
    
    if args.algorithm == 'optimal':
        print("   Optimizing group assignments...")
        groups = optimize_groups(groups, erasure_sets, args.max_risk)
    
    print(f"   Generated {len(groups)} maintenance groups")
    
    # Verify all groups are safe
    all_safe = True
    for i, group in enumerate(groups):
        is_safe, _ = check_group_safety(group, erasure_sets, args.max_risk)
        if not is_safe:
            print(f"   ⚠️  Warning: Group {i+1} exceeds max risk level!")
            all_safe = False
    
    if all_safe:
        print("   ✅ All groups verified safe")
    
    # Generate text report
    text_report = generate_text_report(groups, erasure_sets, args.max_risk)
    print("\n" + text_report)
    
    # Generate HTML report if requested
    if args.html_output:
        print(f"\n📄 Generating HTML maintenance plan...")
        objectstore_name = args.name or "MinIO Cluster"
        html_report = generate_html_report(groups, erasure_sets, all_nodes, 
                                          args.max_risk, objectstore_name)
        
        with open(args.html_output, 'w', encoding='utf-8') as f:
            f.write(html_report)
        
        print(f"   ✅ HTML plan saved to: {args.html_output}")
    
    # Save JSON if requested
    if args.json_output:
        print(f"\n💾 Saving maintenance plan as JSON...")
        
        groups_data = []
        for i, group in enumerate(groups):
            is_safe, impact = check_group_safety(group, erasure_sets, args.max_risk)
            status_counts = Counter(info['status'] for info in impact.values())
            
            groups_data.append({
                'group_id': i + 1,
                'nodes': sorted(group, key=natural_sort_key),
                'node_count': len(group),
                'is_safe': is_safe,
                'impact_summary': {
                    'healthy_sets': status_counts.get('healthy', 0),
                    'degraded_sets': status_counts.get('degraded', 0),
                    'critical_sets': status_counts.get('critical', 0),
                    'failed_sets': status_counts.get('failed', 0),
                }
            })
        
        json_data = {
            'max_risk_level': args.max_risk,
            'total_nodes': len(all_nodes),
            'total_groups': len(groups),
            'total_erasure_sets': len(erasure_sets),
            'groups': groups_data,
            'execution_order': [i + 1 for i in range(len(groups))]
        }
        
        with open(args.json_output, 'w') as f:
            json.dump(json_data, f, indent=2)
        
        print(f"   ✅ JSON data saved to: {args.json_output}")
    
    # Print quick summary
    print(f"\n✨ SUMMARY:")
    print(f"   • Divide cluster into {len(groups)} groups")
    print(f"   • Maintain groups sequentially: Group 1 → Group 2 → ... → Group {len(groups)}")
    print(f"   • Each group can be safely taken offline simultaneously")
    print(f"   • Maximum risk level per group: {args.max_risk.upper()}")
    
    # Show group sizes
    group_sizes = [len(g) for g in groups]
    print(f"\n📦 Group Sizes:")
    for i, size in enumerate(group_sizes, 1):
        nodes_in_group = sorted(groups[i-1], key=natural_sort_key)
        print(f"   Group {i}: {size} nodes ({', '.join(nodes_in_group[:3])}{'...' if size > 3 else ''})")


if __name__ == "__main__":
    main()

0개의 댓글