Untitled

 avatar
unknown
plain_text
9 months ago
24 kB
11
Indexable
import argparse
import subprocess
import json
import datetime
import os
import glob
import csv
from concurrent.futures import ThreadPoolExecutor, as_completed


def run_command(command):
    """Run a shell command and return the output."""
    try:
        output = subprocess.check_output(command, shell=True)
        return output.decode('utf-8').strip()
    except subprocess.CalledProcessError as e:
        print(f"Failed to run command: {command}")
        print(f"Error: {e}")
        return None


def run_command_list(args):
    """Run a command with a list of arguments to avoid shell parsing issues."""
    try:
        output = subprocess.check_output(args)
        return output.decode('utf-8').strip()
    except subprocess.CalledProcessError as e:
        print(f"Failed to run command: {' '.join(args)}")
        print(f"Error: {e}")
        return None


def check_state_history_exists(rack_serial, device_serial):
    """Check if state_history.json exists for a specific rack_serial/device_serial combination."""
    bucket_name = "burninator_orchestrator"
    namespace = "axi697ybxm4m"
    object_name = f"{rack_serial}/{device_serial}/state_history.json"
    cmd = [
        "oci", "os", "object", "head",
        "--bucket-name", bucket_name,
        "--namespace", namespace,
        "--name", object_name,
        "--profile", "BOAT",
        "--auth", "security_token"
    ]
    try:
        subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        return True
    except subprocess.CalledProcessError:
        return False


def download_state_history(rack_serial, device_serial, save_dir):
    """Download state_history.json file for a specific rack_serial/device_serial combination."""
    bucket_name = "burninator_orchestrator"
    namespace = "axi697ybxm4m"
    object_name = f"{rack_serial}/{device_serial}/state_history.json"
    local_path = os.path.join(save_dir, "state_history.json")
    os.makedirs(save_dir, exist_ok=True)
    cmd = [
        "oci", "os", "object", "get",
        "--bucket-name", bucket_name,
        "--namespace", namespace,
        "--name", object_name,
        "--file", local_path,
        "--profile", "BOAT",
        "--auth", "security_token"
    ]
    try:
        subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        print(f"Downloaded state_history.json for {rack_serial}/{device_serial}")
    except subprocess.CalledProcessError as e:
        print(f"Error downloading {object_name}: {e.stderr}")


def threaded_download_state_histories(download_tasks, max_workers=8):
    """Run downloads in parallel using threads."""
    print(f"Starting threaded download of {len(download_tasks)} state history files...")
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = [
            executor.submit(download_state_history, rack_serial, device_serial, save_dir)
            for (rack_serial, device_serial, save_dir) in download_tasks
        ]
        for future in as_completed(futures):
            try:
                future.result()
            except Exception as e:
                print(f"Exception during threaded download: {e}")
    print("Download completed!")


# Test case categorization
ILOM_BOOTSTRAP_TESTS = [
    'test_clear_faults',
    'test_ilom_firmware_upgrade',
    'test_pldm_firmware_update',
    'test_validate_server_assets',
    'test_ilom_health_command',
    'test_bootstrap_ilom_arts'
]

BOOTSTRAP_TESTS = [
    'test_setup_sshfs_bootstrap_configuration',
    'test_reboot_command',
    'test_validate_server',
    'test_cx7_cx8_and_ssd_firmware_upgrade',
    'test_max_clock_speed_setting',
    'test_osfp_internal_probes'
]


def load_state_history_json(file_path):
    """Load and parse state history JSON file."""
    try:
        with open(file_path, 'r') as f:
            data = json.load(f)
        return data
    except (json.JSONDecodeError, FileNotFoundError) as e:
        print(f"Error loading {file_path}: {e}")
        return []


def get_workflow_for_test(test_name):
    """Determine which workflow a test belongs to."""
    if test_name in ILOM_BOOTSTRAP_TESTS:
        return 'ILOM_BOOTSTRAP'
    elif test_name in BOOTSTRAP_TESTS:
        return 'BOOTSTRAP'
    else:
        return 'OTHER'


def calculate_test_runtime(events):
    """Calculate runtime for a specific test across all job runs."""
    test_events = [e for e in events if e.get('test_name')]

    if not test_events:
        return None, None, None

    # Group by test_name and job_id
    test_jobs = {}
    for event in test_events:
        test_name = event['test_name']
        job_id = event['job_id']
        key = f"{test_name}_{job_id}"

        if key not in test_jobs:
            test_jobs[key] = []
        test_jobs[key].append(event)

    runtimes = []
    failure_count = 0
    retry_count = 0

    for key, job_events in test_jobs.items():
        # Sort by timestamp
        job_events.sort(key=lambda x: x['timestamp'])

        # Find start and end times
        start_time = None
        end_time = None
        final_status = None

        for event in job_events:
            if event['status'] in ['IN_PROGRESS', 'PASSED', 'FAILED']:
                if start_time is None:
                    start_time = event['timestamp']
                end_time = event['timestamp']
                final_status = event['status']

        if start_time and end_time:
            start_dt = datetime.datetime.fromisoformat(start_time.replace('Z', '+00:00'))
            end_dt = datetime.datetime.fromisoformat(end_time.replace('Z', '+00:00'))
            runtime_minutes = (end_dt - start_dt).total_seconds() / 60
            runtimes.append(runtime_minutes)

            if final_status == 'FAILED':
                failure_count += 1
            elif final_status == 'SKIPPED':
                retry_count += 1

    if not runtimes:
        return None, 0, 0

    avg_runtime = sum(runtimes) / len(runtimes)
    total_runs = len(runtimes)

    return avg_runtime, failure_count, total_runs


def calculate_workflow_runtime(events, workflow_name):
    """Calculate total runtime for a workflow."""
    workflow_tests = ILOM_BOOTSTRAP_TESTS if workflow_name == 'ILOM_BOOTSTRAP' else BOOTSTRAP_TESTS
    workflow_events = [e for e in events if e.get('test_name') in workflow_tests]

    if not workflow_events:
        return None

    # Group by job_id to get complete workflow runs
    job_events = {}
    for event in workflow_events:
        job_id = event['job_id']
        if job_id not in job_events:
            job_events[job_id] = []
        job_events[job_id].append(event)

    workflow_runtimes = []

    for job_id, job_events_list in job_events.items():
        # Sort by timestamp
        job_events_list.sort(key=lambda x: x['timestamp'])

        # Find the earliest start and latest end for this job
        start_time = None
        end_time = None

        for event in job_events_list:
            if event['status'] in ['IN_PROGRESS', 'PASSED', 'FAILED']:
                if start_time is None:
                    start_time = event['timestamp']
                end_time = event['timestamp']

        if start_time and end_time:
            start_dt = datetime.datetime.fromisoformat(start_time.replace('Z', '+00:00'))
            end_dt = datetime.datetime.fromisoformat(end_time.replace('Z', '+00:00'))
            runtime_minutes = (end_dt - start_dt).total_seconds() / 60
            workflow_runtimes.append(runtime_minutes)

    if not workflow_runtimes:
        return None

    return sum(workflow_runtimes) / len(workflow_runtimes)


def create_combined_csv(csv_file_path, workflow_data, testcase_data, posthandover_ticket_count):
    """Create a single comprehensive CSV file with all data."""
    # Calculate summary data
    summary_data = []
    for test_name in set(tc['Test_Name'] for tc in testcase_data):
        test_records = [tc for tc in testcase_data if tc['Test_Name'] == test_name]
        avg_runtime = sum(tc['Avg_Runtime_Minutes'] for tc in test_records) / len(test_records)
        avg_failure_rate = sum(tc['Failure_Rate_Percent'] for tc in test_records) / len(test_records)
        total_runs = sum(tc['Total_Runs'] for tc in test_records)

        summary_data.append({
            'Test_Name': test_name,
            'Workflow': get_workflow_for_test(test_name),
            'Avg_Runtime_Minutes': avg_runtime,
            'Total_Runs_Across_All_Devices': total_runs,
            'Avg_Failure_Rate_Percent': avg_failure_rate,
            'Device_Count': len(test_records)
        })

    # Calculate slowest test case per workflow per rack
    slowest_tests_data = []
    rack_workflow_tests = {}

    # Group test cases by rack and workflow
    for data in testcase_data:
        rack_serial = data['Rack_Serial']
        workflow = data['Workflow']
        key = f"{rack_serial}_{workflow}"

        if key not in rack_workflow_tests:
            rack_workflow_tests[key] = []
        rack_workflow_tests[key].append(data)

    # Find slowest test case for each rack-workflow combination
    for key, tests in rack_workflow_tests.items():
        if tests:
            # Find the test with maximum runtime
            slowest_test = max(tests, key=lambda x: x['Avg_Runtime_Minutes'])
            slowest_tests_data.append({
                'Rack_Serial': slowest_test['Rack_Serial'],
                'Workflow': slowest_test['Workflow'],
                'Slowest_Test_Name': slowest_test['Test_Name'],
                'Max_Runtime_Minutes': slowest_test['Avg_Runtime_Minutes'],
                'Failure_Rate_Percent': slowest_test['Failure_Rate_Percent']
            })

    with open(csv_file_path, 'w', newline='', encoding='utf-8') as csvfile:
        writer = csv.writer(csvfile)

        # Write section headers and data
        # 0. PostHandover Tickets Count Section
        writer.writerow(['=== POSTHANDOVER TICKETS COUNT ==='])
        writer.writerow(['Number_of_PostHandover_Tickets'])
        writer.writerow([posthandover_ticket_count])
        
        writer.writerow([])  # Empty row separator

        # 1. Workflow Runtimes Section
        writer.writerow(['=== WORKFLOW RUNTIMES ==='])
        writer.writerow(['Rack_Serial', 'Device_Serial', 'ILOM_BOOTSTRAP_Runtime_Minutes', 'BOOTSTRAP_Runtime_Minutes'])
        for data in workflow_data:
            writer.writerow([
                data['Rack_Serial'],
                data['Device_Serial'],
                data['ILOM_BOOTSTRAP_Runtime_Minutes'],
                data['BOOTSTRAP_Runtime_Minutes']
            ])

        writer.writerow([])  # Empty row separator

        # 2. Test Case Analysis Section
        writer.writerow(['=== TEST CASE ANALYSIS ==='])
        writer.writerow(['Rack_Serial', 'Device_Serial', 'Test_Name', 'Workflow', 'Avg_Runtime_Minutes',
                         'Total_Runs', 'Failure_Count', 'Failure_Rate_Percent'])
        for data in testcase_data:
            writer.writerow([
                data['Rack_Serial'],
                data['Device_Serial'],
                data['Test_Name'],
                data['Workflow'],
                data['Avg_Runtime_Minutes'],
                data['Total_Runs'],
                data['Failure_Count'],
                data['Failure_Rate_Percent']
            ])

        writer.writerow([])  # Empty row separator

        # 3. Test Case Summary Section
        writer.writerow(['=== TEST CASE SUMMARY ==='])
        writer.writerow(['Test_Name', 'Workflow', 'Avg_Runtime_Minutes', 'Total_Runs_Across_All_Devices',
                         'Avg_Failure_Rate_Percent', 'Device_Count'])
        for data in summary_data:
            writer.writerow([
                data['Test_Name'],
                data['Workflow'],
                data['Avg_Runtime_Minutes'],
                data['Total_Runs_Across_All_Devices'],
                data['Avg_Failure_Rate_Percent'],
                data['Device_Count']
            ])

        writer.writerow([])  # Empty row separator

        # 4. Slowest Test Cases per Workflow per Rack Section
        writer.writerow(['=== SLOWEST TEST CASES PER WORKFLOW PER RACK ==='])
        writer.writerow(['Rack_Serial', 'Workflow', 'Slowest_Test_Name', 'Max_Runtime_Minutes', 'Failure_Rate_Percent'])
        for data in slowest_tests_data:
            writer.writerow([
                data['Rack_Serial'],
                data['Workflow'],
                data['Slowest_Test_Name'],
                data['Max_Runtime_Minutes'],
                data['Failure_Rate_Percent']
            ])


def analyze_state_history_files(csv_file_path, posthandover_ticket_count):
    """Analyze all downloaded state history files and create Excel report."""
    print("Analyzing state history files...")

    # Find all state history files
    state_files = glob.glob("state_history_downloads/**/state_history.json", recursive=True)

    if not state_files:
        print("No state history files found in state_history_downloads/")
        # Still create CSV with ticket count
        csv_basename = os.path.basename(csv_file_path)
        if csv_basename.endswith('.csv') or csv_basename.endswith('.xlsx'):
            csv_basename = os.path.splitext(csv_basename)[0]
        output_file = f"{csv_basename}_state_history_analysis.csv"
        create_combined_csv(output_file, [], [], posthandover_ticket_count)
        print(f"Created CSV with ticket count: {output_file}")
        return

    print(f"Found {len(state_files)} state history files")

    # Extract base name from csv_file_path for sheet naming
    csv_basename = os.path.basename(csv_file_path)
    if csv_basename.endswith('.csv') or csv_basename.endswith('.xlsx'):
        csv_basename = os.path.splitext(csv_basename)[0]

    # Data collection
    workflow_data = []
    testcase_data = []

    for file_path in state_files:
        # Extract rack_serial and device_serial from path
        path_parts = file_path.split('/')
        if len(path_parts) >= 3:
            rack_serial = path_parts[1]
            device_serial = path_parts[2]
        else:
            continue

        print(f"Processing: {rack_serial}/{device_serial}")

        # Load state history data
        events = load_state_history_json(file_path)
        if not events:
            continue

        # Calculate workflow runtimes
        ilom_bootstrap_runtime = calculate_workflow_runtime(events, 'ILOM_BOOTSTRAP')
        bootstrap_runtime = calculate_workflow_runtime(events, 'BOOTSTRAP')

        workflow_data.append({
            'Rack_Serial': rack_serial,
            'Device_Serial': device_serial,
            'ILOM_BOOTSTRAP_Runtime_Minutes': ilom_bootstrap_runtime,
            'BOOTSTRAP_Runtime_Minutes': bootstrap_runtime
        })

        # Calculate test case runtimes and failure rates
        all_tests = set(e.get('test_name') for e in events if e.get('test_name'))

        for test_name in all_tests:
            if test_name in ILOM_BOOTSTRAP_TESTS + BOOTSTRAP_TESTS:
                avg_runtime, failure_count, total_runs = calculate_test_runtime(
                    [e for e in events if e.get('test_name') == test_name]
                )

                if avg_runtime is not None:
                    failure_rate = (failure_count / total_runs * 100) if total_runs > 0 else 0

                    testcase_data.append({
                        'Rack_Serial': rack_serial,
                        'Device_Serial': device_serial,
                        'Test_Name': test_name,
                        'Workflow': get_workflow_for_test(test_name),
                        'Avg_Runtime_Minutes': avg_runtime,
                        'Total_Runs': total_runs,
                        'Failure_Count': failure_count,
                        'Failure_Rate_Percent': failure_rate
                    })

    # Create single comprehensive CSV file
    output_file = f"{csv_basename}_state_history_analysis.csv"

    # Create combined CSV file (always create, even if no data, to include ticket count)
    create_combined_csv(output_file, workflow_data, testcase_data, posthandover_ticket_count)
    print(f"Created comprehensive CSV: {output_file}")

    print(f"Analysis complete! Results saved to {output_file}")
    print(f"Processed {len(workflow_data)} device combinations")
    print(f"Analyzed {len(testcase_data)} test case records")


def get_repair_order_details(repair_order_id):
    """Get repair order details using burninatorOps.py command."""
    repair_json_command = f"python3 ../../burninator_ops/burninatorOps.py repair get -id '{repair_order_id}'"
    repair_json_output = run_command(repair_json_command)

    if repair_json_output is None:
        return None

    try:
        repair_json = json.loads(repair_json_output)
    except json.JSONDecodeError:
        print(f"Failed to parse repair_json output: {repair_json_output}")
        return None

    return repair_json


def get_device_details(device_serial):
    """Get device details using burninatorOps.py command."""
    device_command = f"python3 ../../burninator_ops/burninatorOps.py device get -s '{device_serial}'"
    device_output = run_command(device_command)

    if device_output is None:
        return None

    try:
        device_json = json.loads(device_output)
    except json.JSONDecodeError:
        print(f"Failed to parse device_json output: {device_output}")
        return None

    return device_json


def process_query_results(results):
    """Process query results to extract device serials and rack serials."""
    rack_serial_device_serials = {}
    tickets_processed = 0
    download_tasks = []
    scheduled = set()
    
    # Count total number of postHandover tickets
    total_tickets = 0
    for result in results.split('\n'):
        if result.startswith('ROMAOPS'):
            total_tickets += 1

    for result in results.split('\n'):
        try:
            if result.startswith('ROMAOPS'):
                fields = result.split(',')
                if len(fields) >= 4:
                    key = fields[0]
                    title = fields[1]
                    rack_position = fields[2]
                    repair_order_id = fields[3].strip()

                    print(f"Processing result: {result}")
                    print(
                        f"Key: {key}, Title: {title}, Rack Position: {rack_position}, Repair Order ID: {repair_order_id}")

                    if repair_order_id:
                        repair_details = get_repair_order_details(repair_order_id)
                        if repair_details:
                            device_serial = repair_details.get('deviceSerial')

                            if device_serial:
                                print(f"Device Serial: {device_serial}")

                                # Get device details to get rack serial
                                device_details = get_device_details(device_serial)
                                if device_details:
                                    rack_serial = device_details.get('rackSerial')
                                    print(f"Rack Serial: {rack_serial}")

                                    # Store the mapping
                                    if rack_serial in rack_serial_device_serials:
                                        rack_serial_device_serials[rack_serial].append(device_serial)
                                    else:
                                        rack_serial_device_serials[rack_serial] = [device_serial]

                                    # Check if state history file exists and create download task
                                    if (rack_serial, device_serial) not in scheduled:
                                        if check_state_history_exists(rack_serial, device_serial):
                                            save_dir = os.path.join("state_history_downloads", rack_serial,
                                                                    device_serial)
                                            download_tasks.append((rack_serial, device_serial, save_dir))
                                            scheduled.add((rack_serial, device_serial))
                                            print(f"Added download task for {rack_serial}/{device_serial}")
                                        else:
                                            print(f"State history file not found for {rack_serial}/{device_serial}")

                                    tickets_processed += 1
                                    print(f"Successfully processed ticket {key}")
                                else:
                                    print(f"Failed to get device details for {device_serial}")
                            else:
                                print("No device serial found in repair order")
                        else:
                            print("Failed to get repair order details")
                    else:
                        print("No repair order ID found in ticket")

                    print("-" * 80)
                else:
                    print(f"Skipping malformed result: {result}")
        except Exception as e:
            print(f"Skipping ticket entry: {result}")
            print(f"Failure cause: {e}")

    print(f"Total tickets processed: {tickets_processed}")
    print(f"Total postHandover tickets: {total_tickets}")
    print(f"Total download tasks collected: {len(download_tasks)}")
    return rack_serial_device_serials, download_tasks, total_tickets


def query_tickets():
    """Query tickets using burninatorOps.py directly."""
    query = 'Project = ROMAOPS and ("Searchable Data" ~ "HandedOver" or Title ~ "ROMA_CUSTOMER") and status in ("Resolved", "Closed","Done","Pending Customer") and (Description ~ "gpu_count_mismatch") AND "component" = "ROMA-Ingestion"'
    args = [
        'python3',
        '../../burninator_ops/burninatorOps.py',
        'ots',
        'query',
        '-q', query,
        '-f', 'key,title,rack_position,repair_order_id,component,status,searchable_data,labels'
    ]

    command_str = ' '.join(args)
    print(f"Running query with command: \n    {command_str}")
    results = run_command_list(args)

    if results is None:
        print("Failed to get query results")
        return

    print("Query results:")
    print(results)
    print("=" * 80)

    # Process the results to get device serials and rack serials
    rack_serial_device_serials, download_tasks, total_tickets = process_query_results(results)

    # Print summary
    print(f"\nSummary:")
    print(f"Found {len(rack_serial_device_serials)} unique rack serials")
    for rack_serial, device_serials in rack_serial_device_serials.items():
        print(f"Rack Serial: {rack_serial} -> Device Serials: {device_serials}")

    # Download state history files for all tickets
    if download_tasks:
        print(f"\nDownloading state history files for {len(download_tasks)} folders...")
        threaded_download_state_histories(download_tasks, max_workers=8)
    else:
        print("\nNo state history files to download.")

    return rack_serial_device_serials, download_tasks, total_tickets


def main(csv_file_path):
    print(f"The Script output path: {csv_file_path}")
    # Query tickets and get deviceSerial and rackSerial
    result = query_tickets()
    if result:
        rack_serial_device_serials, download_tasks, total_tickets = result
        print(f"\nFinal Summary:")
        print(f"Processed {len(rack_serial_device_serials)} rack serials")
        print(f"Total postHandover tickets: {total_tickets}")
        print(f"Downloaded state history files for {len(download_tasks)} folders")
        print(f"Files saved in: state_history_downloads/")

        # Analyze the downloaded state history files
        print(f"\nStarting state history analysis...")
        analyze_state_history_files(csv_file_path, total_tickets)
    else:
        print("No results to process.")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='KPI Analysis Script')
    parser.add_argument('--csv_file_path', type=str, help='Path to CSV files', required=True)
    args = parser.parse_args()
    main(args.csv_file_path)
Editor is loading...
Leave a Comment