171 lines
6.0 KiB
Python
171 lines
6.0 KiB
Python
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
|