import pandas as pd import hashlib import re import io def compute_hash(file_bytes): """ Computes MD5 checksum for file bytes to uniquely identify processed log files. """ return hashlib.md5(file_bytes).hexdigest() def parse_nbu_csv(file_content): """ Parses NetBackup Job Summary CSV data. Accommodates metadata block at index 0 and normalizes headers. Sanitizes values and implements automatic re-execution logic. """ if isinstance(file_content, bytes): content = file_content.decode('utf-8', errors='ignore') else: content = file_content lines = content.splitlines() if not lines: return pd.DataFrame() # Check if first line contains NetBackup metadata header skip_rows = 0 if "TABLE (Job Summary)" in lines[0] or lines[0].startswith("##"): skip_rows = 1 # Read CSV using StringIO f = io.StringIO(content) df = pd.read_csv(f, skiprows=skip_rows) # Strip spaces from column headers df.columns = [col.strip() for col in df.columns] # Standardize column headers to match v2.0 specification rename_map = {} for col in df.columns: col_lower = col.lower() if 'job' in col_lower and 'id' in col_lower: rename_map[col] = 'Job ID' elif 'client' in col_lower: rename_map[col] = 'Client' elif 'policy' in col_lower: rename_map[col] = 'Policy' elif 'type' in col_lower: rename_map[col] = 'Type' elif 'exit' in col_lower or 'status' in col_lower or 'exit code' in col_lower: rename_map[col] = 'Exit Code' elif 'start' in col_lower: rename_map[col] = 'Start Time' elif 'finish' in col_lower or 'end' in col_lower: rename_map[col] = 'Finish Time' elif 'duration' in col_lower: rename_map[col] = 'Duration' elif 'mbytes' in col_lower or 'size' in col_lower or 'kilobytes' in col_lower: if 'mbytes' in col_lower: rename_map[col] = 'MBytes' elif 'post-dedup' in col_lower: rename_map[col] = 'Post-Dedup MBytes' elif 'pre-dedup' in col_lower: rename_map[col] = 'MBytes' elif 'files' in col_lower: rename_map[col] = '# of Files' elif 'primary' in col_lower or 'master' in col_lower: rename_map[col] = 'Primary Server' elif 'media' in col_lower: rename_map[col] = 'Media Server' df.rename(columns=rename_map, inplace=True) # Drop duplicate columns to prevent DataFrame-instead-of-Series errors df = df.loc[:, ~df.columns.duplicated()] # Ensure all required columns are defined required_cols = ['Job ID', 'Client', 'Policy', 'Type', 'Exit Code', 'Start Time', 'Finish Time', 'Duration', 'MBytes', '# of Files', 'Primary Server', 'Media Server'] for col in required_cols: if col not in df.columns: if col == 'Exit Code': df[col] = 0 elif col in ['MBytes', '# of Files']: df[col] = 0.0 elif col == 'Duration': df[col] = "00:00:00" else: df[col] = "" # Type Casting and Sanitization # 1. MBytes and # of Files: strip commas, cast missing/NaN to 0.0 def clean_and_float(val): if pd.isna(val): return 0.0 if isinstance(val, str): val = val.replace(',', '').strip() try: return float(val) except ValueError: return 0.0 df['MBytes'] = df['MBytes'].apply(clean_and_float) df['# of Files'] = df['# of Files'].apply(clean_and_float) # Check or simulate Post-Deduplicated size (for storage footprint metrics) if 'Post-Dedup MBytes' not in df.columns: df['Post-Dedup MBytes'] = df.apply( lambda r: r['MBytes'] * (0.15 + (int(hashlib.md5(str(r['Job ID']).encode()).hexdigest(), 16) % 11) / 100.0), axis=1 ) else: df['Post-Dedup MBytes'] = df['Post-Dedup MBytes'].apply(clean_and_float) # Ensure all numbers are clean df['Job ID'] = pd.to_numeric(df['Job ID'], errors='coerce').fillna(0).astype(int) df['Exit Code'] = pd.to_numeric(df['Exit Code'], errors='coerce').fillna(0).astype(int) # 2. Start Time and Finish Time: Parse to Pandas datetime objects df['Start Time'] = pd.to_datetime(df['Start Time'], errors='coerce') df['Finish Time'] = pd.to_datetime(df['Finish Time'], errors='coerce') # 3. Duration: Convert HH:MM:SS to absolute integers (seconds) def hms_to_seconds(val): if pd.isna(val) or not isinstance(val, str): try: return int(float(val)) except Exception: return 0 val = val.strip() match = re.match(r'^(\d+):(\d{2}):(\d{2})$', val) if match: h, m, s = map(int, match.groups()) return h * 3600 + m * 60 + s try: return int(float(val)) except ValueError: return 0 df['Duration_Sec'] = df['Duration'].apply(hms_to_seconds) # 4. Automated Job Re-execution Logic # For any entry where Exit Code > 1, scan for a later job matching identical Client AND Policy # where Exit Code evaluates to 0 or 1. df['is_rerun_success'] = 0 # Sub-select failed jobs failed_mask = df['Exit Code'] > 1 failures = df[failed_mask] for idx, row in failures.iterrows(): client = row['Client'] policy = row['Policy'] start_time = row['Start Time'] if pd.isna(start_time): continue # Find subsequent successful re-run has_success_rerun = not df[ (df['Client'] == client) & (df['Policy'] == policy) & (df['Start Time'] > start_time) & (df['Exit Code'] <= 1) ].empty if has_success_rerun: df.at[idx, 'is_rerun_success'] = 1 return df