From 6558507bbc68762109a53872bfd837fef1b2bbc0 Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Fri, 21 Nov 2025 14:51:58 -0800 Subject: [PATCH 01/19] NGPS mods --- triage_package/triage_tool.py | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/triage_package/triage_tool.py b/triage_package/triage_tool.py index 34b9acc..0a0b65d 100755 --- a/triage_package/triage_tool.py +++ b/triage_package/triage_tool.py @@ -64,6 +64,7 @@ def load_config(self, config): else: self.email_alerts = False self.r_path = self.config["Report"]["report_path"] + self.log_dir = self.config["Logs"]["logs_dir"] self.reports_path = f"{self.r_path}/{self.current_utc_date}" if not os.path.exists(f"{self.reports_path}"): os.mkdir(f"{self.reports_path}") @@ -187,37 +188,25 @@ def _worker(): def gather_logs(self): ''' Gathers Logs from all paths to dump into tar comp. ''' #Iterate through log paths in config file - for log in self.config["Logs"]: - log_path = self.config["Logs"][log] - - #Check that a log path exist - if os.path.exists(log_path): + for subdir, dirs, files in os.walk(self.log_dir): + for file in files: + log_file = os.path.join(subdir, file) #Copy over to logs folder try: - shutil.copy2(log_path,f"{self.reports_path}") + shutil.copy2(log_file,f"{self.reports_path}") except FileNotFoundError: print(f"Error: The file at {log_path} was not found.") except PermissionError: print(f"Error: Permission denied to access the file at {log_path}.") except Exception as e: # pylint: disable = W0718 print(f"An unexpected error occurred: {e}") - else: - print(f"Log path {log_path} does not exist.") - def comb_logs(self): ''' Comb logs for errors and warnings ''' - #Iterate through log paths in config file - for log in self.config["Logs"]: - path = self.config["Logs"][log] - #Edit this to format to LOG structure - log_path = f"{path}/{self.utc_date}/" - #Put log_path into report file - with open(self.report_name, 'a', encoding='utf-8') as file: - file.write(f"\n\n====={log_path}=====\n") - if os.path.exists(log_path): - print(f"Gathering log from: {log_path}") + for subdir, dirs, files in os.walk(self.log_dir): + for file in files: + log_file = os.path.join(subdir, file) try: with open(log_path, 'r', encoding='utf-8') as log_file: #Search for occurances of warnings and errors sequentially @@ -241,8 +230,6 @@ def comb_logs(self): print(f"Error: Permission denied to access the file at {log_path}.") except Exception as e: # pylint: disable = W0718 print(f"An unexpected error occurred: {e}") - else: - print(f"Log path {log_path} does not exist.") def compress_report(self): '''Compresses report file into a tar.gz format to be emailed''' From 51c770d721bcd308476da2230672a5dc2a8fb0f4 Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Fri, 21 Nov 2025 15:00:48 -0800 Subject: [PATCH 02/19] spelling error --- triage_package/triage.ini | 5 +++-- triage_package/triage_tool.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/triage_package/triage.ini b/triage_package/triage.ini index 3e143f9..938a50d 100644 --- a/triage_package/triage.ini +++ b/triage_package/triage.ini @@ -9,6 +9,7 @@ os_version = [Logs] #Example: system = /var/log/syslog #system = /home/path/to/logging/blue_cal_gammavac.log +logs_dir = /home/elijahab/Downloads [Report] #Enable or disable email alerts @@ -16,7 +17,7 @@ email_alerts=True #***Does Nothing with this info right now*** #Email address to send alerts to instrument_master_email="" #***Does Nothing with this info right now*** #Where to store generated reports (full path) -report_path= #/home/user/dir/reports +report_path= reports [Machine] #Threshold for alerting on high CPU usage (in percentage) @@ -28,4 +29,4 @@ memory_threshold=90 #***Does Nothing with this info right now*** #IP address or host of session host = #host.provider.com host.iden.edu password = #Password1234 -vnc_sessions = #1,2,3,4,5,12 +vnc_sessions = 1,2,3,4,5,12 diff --git a/triage_package/triage_tool.py b/triage_package/triage_tool.py index 0a0b65d..c3b4eb8 100755 --- a/triage_package/triage_tool.py +++ b/triage_package/triage_tool.py @@ -206,7 +206,7 @@ def comb_logs(self): #Iterate through log paths in config file for subdir, dirs, files in os.walk(self.log_dir): for file in files: - log_file = os.path.join(subdir, file) + log_path = os.path.join(subdir, file) try: with open(log_path, 'r', encoding='utf-8') as log_file: #Search for occurances of warnings and errors sequentially From 82486190738c95bbab204d969b3518fdbd3faaf0 Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Fri, 21 Nov 2025 15:01:13 -0800 Subject: [PATCH 03/19] spelling error --- triage_package/triage.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/triage_package/triage.ini b/triage_package/triage.ini index 938a50d..b6e2f5f 100644 --- a/triage_package/triage.ini +++ b/triage_package/triage.ini @@ -9,7 +9,7 @@ os_version = [Logs] #Example: system = /var/log/syslog #system = /home/path/to/logging/blue_cal_gammavac.log -logs_dir = /home/elijahab/Downloads +logs_dir = [Report] #Enable or disable email alerts @@ -17,7 +17,7 @@ email_alerts=True #***Does Nothing with this info right now*** #Email address to send alerts to instrument_master_email="" #***Does Nothing with this info right now*** #Where to store generated reports (full path) -report_path= reports +report_path= [Machine] #Threshold for alerting on high CPU usage (in percentage) From 7d8ffb8d22f719c66dfad53309d32e229a5fbf20 Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Fri, 21 Nov 2025 15:02:05 -0800 Subject: [PATCH 04/19] spelling error --- triage_package/triage.ini | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/triage_package/triage.ini b/triage_package/triage.ini index b6e2f5f..3e143f9 100644 --- a/triage_package/triage.ini +++ b/triage_package/triage.ini @@ -9,7 +9,6 @@ os_version = [Logs] #Example: system = /var/log/syslog #system = /home/path/to/logging/blue_cal_gammavac.log -logs_dir = [Report] #Enable or disable email alerts @@ -17,7 +16,7 @@ email_alerts=True #***Does Nothing with this info right now*** #Email address to send alerts to instrument_master_email="" #***Does Nothing with this info right now*** #Where to store generated reports (full path) -report_path= +report_path= #/home/user/dir/reports [Machine] #Threshold for alerting on high CPU usage (in percentage) @@ -29,4 +28,4 @@ memory_threshold=90 #***Does Nothing with this info right now*** #IP address or host of session host = #host.provider.com host.iden.edu password = #Password1234 -vnc_sessions = 1,2,3,4,5,12 +vnc_sessions = #1,2,3,4,5,12 From f313493e960ce6b736f96e0dc70da2c81cd7a7b0 Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Fri, 21 Nov 2025 15:27:36 -0800 Subject: [PATCH 05/19] Adding details to .txt file --- triage_package/triage_tool.py | 59 ++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/triage_package/triage_tool.py b/triage_package/triage_tool.py index c3b4eb8..10f173c 100755 --- a/triage_package/triage_tool.py +++ b/triage_package/triage_tool.py @@ -188,48 +188,51 @@ def _worker(): def gather_logs(self): ''' Gathers Logs from all paths to dump into tar comp. ''' #Iterate through log paths in config file - for subdir, dirs, files in os.walk(self.log_dir): + for subdir, dirs, files in os.walk(self.log_dir): # pylint: disable = W0612 for file in files: log_file = os.path.join(subdir, file) #Copy over to logs folder try: shutil.copy2(log_file,f"{self.reports_path}") except FileNotFoundError: - print(f"Error: The file at {log_path} was not found.") + print(f"Error: The file at {log_file} was not found.") except PermissionError: - print(f"Error: Permission denied to access the file at {log_path}.") + print(f"Error: Permission denied to access the file at {log_file}.") except Exception as e: # pylint: disable = W0718 print(f"An unexpected error occurred: {e}") def comb_logs(self): ''' Comb logs for errors and warnings ''' #Iterate through log paths in config file - for subdir, dirs, files in os.walk(self.log_dir): + for subdir, dirs, files in os.walk(self.log_dir): # pylint: disable = W0612 for file in files: - log_path = os.path.join(subdir, file) - try: - with open(log_path, 'r', encoding='utf-8') as log_file: - #Search for occurances of warnings and errors sequentially - full_log = log_file.read() - - #Use Regex - matches = (re.findall(self.regex_pattern, full_log, - re.IGNORECASE | re.MULTILINE)) - timeframe_matches = [ - match for match in matches - if (m := re.match(self.time_pattern, match)) - and datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S") >= self.cutoff - ] - - with open(self.report_name, 'a', encoding='utf-8') as report_file: - for match in timeframe_matches: - report_file.write(f"{match}\n") - except FileNotFoundError: - print(f"Error: The file at {log_path} was not found.") - except PermissionError: - print(f"Error: Permission denied to access the file at {log_path}.") - except Exception as e: # pylint: disable = W0718 - print(f"An unexpected error occurred: {e}") + if file.lower().endswith(".log"): + log_path = os.path.join(subdir, file) + with open(self.report_name, 'a', encoding='utf-8') as file: + file.write(f"\n\n====={log_path}=====\n") + try: + with open(log_path, 'r', encoding='utf-8') as log_file: + #Search for occurances of warnings and errors sequentially + full_log = log_file.read() + + #Use Regex + matches = (re.findall(self.regex_pattern, full_log, + re.IGNORECASE | re.MULTILINE)) + timeframe_matches = [ + match for match in matches + if (m := re.match(self.time_pattern, match)) + and datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S") >= self.cutoff + ] + + with open(self.report_name, 'a', encoding='utf-8') as report_file: + for match in timeframe_matches: + report_file.write(f"{match}\n") + except FileNotFoundError: + print(f"Error: The file at {log_path} was not found.") + except PermissionError: + print(f"Error: Permission denied to access the file at {log_path}.") + except Exception as e: # pylint: disable = W0718 + print(f"An unexpected error occurred: {e}") def compress_report(self): '''Compresses report file into a tar.gz format to be emailed''' From 010bad97261af447912224342b85ccde37cfabf4 Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Mon, 24 Nov 2025 11:52:18 -0800 Subject: [PATCH 06/19] Mods for NGPS implementation --- gecko | 1 + triage_package/triage_tool.py | 40 ++++++++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/gecko b/gecko index aea07fc..d44408c 100755 --- a/gecko +++ b/gecko @@ -41,6 +41,7 @@ def run_triage(): gecko.gather_logs() gecko.comb_logs() gecko.take_screenshots() + gecko.grab_science_image() gecko.compress_report() #gecko.send_report() diff --git a/triage_package/triage_tool.py b/triage_package/triage_tool.py index 10f173c..4cd00d4 100755 --- a/triage_package/triage_tool.py +++ b/triage_package/triage_tool.py @@ -43,7 +43,8 @@ def __init__(self, config: str, message:str = ""): self.utc_time = str(self.utc_date.time()) self.cutoff = datetime.now().replace(tzinfo=None) - timedelta(hours=24) self.message = message - self.time_pattern = r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})" + self.time_pattern = r"^(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?)" + # Load config file self.load_config(self.config_file) @@ -65,6 +66,7 @@ def load_config(self, config): self.email_alerts = False self.r_path = self.config["Report"]["report_path"] self.log_dir = self.config["Logs"]["logs_dir"] + self.science_dir = self.config["Logs"]["science_dir"] self.reports_path = f"{self.r_path}/{self.current_utc_date}" if not os.path.exists(f"{self.reports_path}"): os.mkdir(f"{self.reports_path}") @@ -210,6 +212,7 @@ def comb_logs(self): log_path = os.path.join(subdir, file) with open(self.report_name, 'a', encoding='utf-8') as file: file.write(f"\n\n====={log_path}=====\n") + print(f"Gathering log from: {log_path}") try: with open(log_path, 'r', encoding='utf-8') as log_file: #Search for occurances of warnings and errors sequentially @@ -221,8 +224,8 @@ def comb_logs(self): timeframe_matches = [ match for match in matches if (m := re.match(self.time_pattern, match)) - and datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S") >= self.cutoff - ] + and datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S") >= + self.cutoff] with open(self.report_name, 'a', encoding='utf-8') as report_file: for match in timeframe_matches: @@ -240,6 +243,37 @@ def compress_report(self): with tarfile.open(f"{self.reports_path}/gecko_{self.utc_date}.tar.gz", "w:gz") as tar: tar.add(f"{self.reports_path}", arcname=os.path.basename(f"{self.reports_path}")) + def grab_science_image(self): + '''Grabs most recent science image(s) to include in triage''' + #Edit this section to fit instrument data schema + image_dirs = [ + f"{self.science_dir}/acam", + f"{self.science_dir}/slicecam", + self.science_dir, + ] + #Iterate through dirs and grab most recent modified file, + # which should be the most recent image taken + for i_dir in image_dirs: + if not os.path.exists(i_dir): + with open(self.report_name, 'a', encoding='utf-8') as file: + file.write(f"\nScience Directory does not Exist: {dir}\n") + else: + files = [ + os.path.join(i_dir, f) + for f in os.listdir(i_dir) + if os.path.isfile(os.path.join(i_dir), f) + ] + + if not files: + raise FileNotFoundError("No files found in source directory.") + + # Pick newest by modification time + newest_file = max(files, key=os.path.getmtime) + + # Copy to destination + shutil.copy(newest_file, self.reports_path) + + def send_report(self): ''' Send the generated report to specified recipients ''' #send message using smtplib From ae4a3ffea9eb79825ff5086dbcb4242818625f2b Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Mon, 24 Nov 2025 12:16:40 -0800 Subject: [PATCH 07/19] Time Pattern changes --- triage_package/triage_tool.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/triage_package/triage_tool.py b/triage_package/triage_tool.py index 4cd00d4..822d263 100755 --- a/triage_package/triage_tool.py +++ b/triage_package/triage_tool.py @@ -221,11 +221,15 @@ def comb_logs(self): #Use Regex matches = (re.findall(self.regex_pattern, full_log, re.IGNORECASE | re.MULTILINE)) + #timeframe_matches = [ + # match for match in matches + # if (m := re.match(self.time_pattern, match)) + # and datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S") >= + # self.cutoff] timeframe_matches = [ match for match in matches if (m := re.match(self.time_pattern, match)) - and datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S") >= - self.cutoff] + and datetime.fromisoformat(m.group(1)) >= self.cutoff] with open(self.report_name, 'a', encoding='utf-8') as report_file: for match in timeframe_matches: From 96315d487a85e6d76b5da69fbe14b83a0af53c5d Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Mon, 24 Nov 2025 12:20:53 -0800 Subject: [PATCH 08/19] Time Pattern changes --- triage_package/triage_tool.py | 39 +++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/triage_package/triage_tool.py b/triage_package/triage_tool.py index 822d263..c4cff23 100755 --- a/triage_package/triage_tool.py +++ b/triage_package/triage_tool.py @@ -258,25 +258,28 @@ def grab_science_image(self): #Iterate through dirs and grab most recent modified file, # which should be the most recent image taken for i_dir in image_dirs: - if not os.path.exists(i_dir): + try: + if not os.path.exists(i_dir): + with open(self.report_name, 'a', encoding='utf-8') as file: + file.write(f"\nScience Directory does not Exist: {dir}\n") + else: + files = [ + os.path.join(i_dir, f) + for f in os.listdir(i_dir) + if os.path.isfile(os.path.join(i_dir), f) + ] + + if not files: + raise FileNotFoundError("No files found in source directory.") + + # Pick newest by modification time + newest_file = max(files, key=os.path.getmtime) + + # Copy to destination + shutil.copy(newest_file, self.reports_path) + except FileNotFoundError as e: with open(self.report_name, 'a', encoding='utf-8') as file: - file.write(f"\nScience Directory does not Exist: {dir}\n") - else: - files = [ - os.path.join(i_dir, f) - for f in os.listdir(i_dir) - if os.path.isfile(os.path.join(i_dir), f) - ] - - if not files: - raise FileNotFoundError("No files found in source directory.") - - # Pick newest by modification time - newest_file = max(files, key=os.path.getmtime) - - # Copy to destination - shutil.copy(newest_file, self.reports_path) - + file.write(f"\nSource Directory: {dir}\n--{e}\n") def send_report(self): ''' Send the generated report to specified recipients ''' From 73bb42641e2f464174b0c51b065d2547fcbfbe12 Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Mon, 24 Nov 2025 12:23:15 -0800 Subject: [PATCH 09/19] Time Pattern changes --- triage_package/triage_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/triage_package/triage_tool.py b/triage_package/triage_tool.py index c4cff23..3c87034 100755 --- a/triage_package/triage_tool.py +++ b/triage_package/triage_tool.py @@ -266,7 +266,7 @@ def grab_science_image(self): files = [ os.path.join(i_dir, f) for f in os.listdir(i_dir) - if os.path.isfile(os.path.join(i_dir), f) + if os.path.isfile(os.path.join(i_dir, f)) ] if not files: From bf1e1233e61caf35ae3c52448dc274791bbabb95 Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Mon, 24 Nov 2025 12:26:14 -0800 Subject: [PATCH 10/19] Time Pattern changes --- triage_package/triage_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/triage_package/triage_tool.py b/triage_package/triage_tool.py index 3c87034..f04b1b5 100755 --- a/triage_package/triage_tool.py +++ b/triage_package/triage_tool.py @@ -279,7 +279,7 @@ def grab_science_image(self): shutil.copy(newest_file, self.reports_path) except FileNotFoundError as e: with open(self.report_name, 'a', encoding='utf-8') as file: - file.write(f"\nSource Directory: {dir}\n--{e}\n") + file.write(f"\nSource Directory: {i_dir}\n--{e}\n") def send_report(self): ''' Send the generated report to specified recipients ''' From 1996c55b3a5b35ff7c7ab85681bc577cf320192a Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Mon, 24 Nov 2025 12:39:23 -0800 Subject: [PATCH 11/19] Pylint errors --- triage_package/triage_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/triage_package/triage_tool.py b/triage_package/triage_tool.py index f04b1b5..80c38f4 100755 --- a/triage_package/triage_tool.py +++ b/triage_package/triage_tool.py @@ -279,7 +279,7 @@ def grab_science_image(self): shutil.copy(newest_file, self.reports_path) except FileNotFoundError as e: with open(self.report_name, 'a', encoding='utf-8') as file: - file.write(f"\nSource Directory: {i_dir}\n--{e}\n") + file.write(f"\nSource Directory: {i_dir}\n--{e}\n") def send_report(self): ''' Send the generated report to specified recipients ''' From a94f2c0545744b0c63a41e1c017f7186b5eb75d5 Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Mon, 24 Nov 2025 15:00:23 -0800 Subject: [PATCH 12/19] Email implementation --- triage_package/triage_tool.py | 59 ++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/triage_package/triage_tool.py b/triage_package/triage_tool.py index 80c38f4..103d992 100755 --- a/triage_package/triage_tool.py +++ b/triage_package/triage_tool.py @@ -21,12 +21,10 @@ import shutil import configparser import smtplib -#import argparse -#from email.mime.text import MIMEText from email.message import EmailMessage import re from datetime import datetime, timezone, timedelta -#import glob +import glob import threading import socket import psutil @@ -62,6 +60,7 @@ def load_config(self, config): if self.config["Report"]["email_alerts"].lower().strip() == "true": self.email_alerts = True self.target_email = self.config["Report"]["instrument_master_email"] + self.sender_email = self.config["Report"]["sender_email"] else: self.email_alerts = False self.r_path = self.config["Report"]["report_path"] @@ -282,22 +281,46 @@ def grab_science_image(self): file.write(f"\nSource Directory: {i_dir}\n--{e}\n") def send_report(self): - ''' Send the generated report to specified recipients ''' - #send message using smtplib - #format message + """Send the generated report to specified recipients.""" + + # Create email msg = EmailMessage() - msg ['Subject'] = f'' #pylint: disable = W1309 - msg['From'] = '' - msg['To'] = self.target_email + msg['Subject'] = f'Gecko Report {self.utc_date}' + msg['From'] = self.sender_email # replace with actual sender + msg['To'] = self.target_email # can be comma-separated string or list + + # Email body + msg.set_content("Please see attached report images.") + + #.txt file first + with open(self.report_name, 'rb') as f: + msg.add_attachment( + f.read(), + maintype='text', + subtype='plain', + filename=os.path.basename(self.report_name) + ) + + #tar.gz file next + tar_file = f"{self.reports_path}/gecko_{self.utc_date}.tar.gz" + with open(tar_file, 'rb') as f: + msg.add_attachment( + f.read(), + maintype='application', + subtype='gzip', + filename=os.path.basename(tar_file) + ) - #image_files = glob.glob(os.path.join(self.reports_path,'**', '*.png'), recursive=True) + # Attach PNG images recursively from the reports_path + image_files = glob.glob(os.path.join(self.reports_path, '**', '*.png'), recursive=True) + for file in image_files: + with open(file, 'rb') as fp: + img_data = fp.read() + filename = os.path.basename(file) + msg.add_attachment(img_data, maintype='image', subtype='png', filename=filename) - # TODO: test add attatchment. - msg.add_attachment() - #for file in image_files: - # with open(file,'rb') as fp: - # img_data = fp.read() - # msg.add_attachment(img_data, maintype='image', subtype='png') + # Send email using local SMTP server + with smtplib.SMTP('localhost') as sender: + sender.send_message(msg) - sender = smtplib.SMTP('localhost') - sender.quit() + print(f"Report sent to {self.target_email}") From 9f2b153e61c0f89c607473480a4f9a7d859fd2bd Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Mon, 24 Nov 2025 15:01:57 -0800 Subject: [PATCH 13/19] Email implementation --- gecko | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gecko b/gecko index d44408c..9c9f803 100755 --- a/gecko +++ b/gecko @@ -43,7 +43,7 @@ def run_triage(): gecko.take_screenshots() gecko.grab_science_image() gecko.compress_report() - #gecko.send_report() + gecko.send_report() if __name__ == "__main__": run_triage() From 10bee510391cdb7efd326fb4ff0862f79a1d8d87 Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Mon, 24 Nov 2025 15:13:28 -0800 Subject: [PATCH 14/19] Email implementation --- triage_package/triage.ini | 4 +++- triage_package/triage_tool.py | 12 +++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/triage_package/triage.ini b/triage_package/triage.ini index 3e143f9..62a860d 100644 --- a/triage_package/triage.ini +++ b/triage_package/triage.ini @@ -14,7 +14,9 @@ os_version = #Enable or disable email alerts email_alerts=True #***Does Nothing with this info right now*** #Email address to send alerts to -instrument_master_email="" #***Does Nothing with this info right now*** +instrument_master_email= #***Does Nothing with this info right now*** +sender_email = #sender email +sender_password = #Where to store generated reports (full path) report_path= #/home/user/dir/reports diff --git a/triage_package/triage_tool.py b/triage_package/triage_tool.py index 103d992..f5375b9 100755 --- a/triage_package/triage_tool.py +++ b/triage_package/triage_tool.py @@ -61,6 +61,7 @@ def load_config(self, config): self.email_alerts = True self.target_email = self.config["Report"]["instrument_master_email"] self.sender_email = self.config["Report"]["sender_email"] + self.sender_password = self.config["Report"]["sender_password"] else: self.email_alerts = False self.r_path = self.config["Report"]["report_path"] @@ -319,8 +320,13 @@ def send_report(self): filename = os.path.basename(file) msg.add_attachment(img_data, maintype='image', subtype='png', filename=filename) - # Send email using local SMTP server - with smtplib.SMTP('localhost') as sender: - sender.send_message(msg) + ## Send email using local SMTP server + #with smtplib.SMTP('localhost') as sender: + # sender.send_message(msg) + + # Connect to Gmail SMTP server + with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp: + smtp.login(self.sender_email, self.sender_password) # use an App Password + smtp.send_message(msg) print(f"Report sent to {self.target_email}") From 23caff1d8720dc4a216370a499fa0861fe43c448 Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Mon, 24 Nov 2025 15:14:35 -0800 Subject: [PATCH 15/19] Email implementation --- triage_package/triage_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/triage_package/triage_tool.py b/triage_package/triage_tool.py index f5375b9..234a103 100755 --- a/triage_package/triage_tool.py +++ b/triage_package/triage_tool.py @@ -325,7 +325,7 @@ def send_report(self): # sender.send_message(msg) # Connect to Gmail SMTP server - with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp: + with smtplib.SMTP_SSL('smtp.outlook.com', 465) as smtp: smtp.login(self.sender_email, self.sender_password) # use an App Password smtp.send_message(msg) From a946b8e6908b47e8e7f39814359c06dfc856ea22 Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Mon, 24 Nov 2025 15:52:02 -0800 Subject: [PATCH 16/19] Email implementation --- gecko | 3 ++- triage_package/triage.ini | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gecko b/gecko index 9c9f803..8d89483 100755 --- a/gecko +++ b/gecko @@ -43,7 +43,8 @@ def run_triage(): gecko.take_screenshots() gecko.grab_science_image() gecko.compress_report() - gecko.send_report() + if gecko.email_alerts: + gecko.send_report() if __name__ == "__main__": run_triage() diff --git a/triage_package/triage.ini b/triage_package/triage.ini index 62a860d..291f519 100644 --- a/triage_package/triage.ini +++ b/triage_package/triage.ini @@ -12,11 +12,11 @@ os_version = [Report] #Enable or disable email alerts -email_alerts=True #***Does Nothing with this info right now*** +email_alerts=False #***Does Nothing with this info right now*** #Email address to send alerts to instrument_master_email= #***Does Nothing with this info right now*** sender_email = #sender email -sender_password = +sender_password = #Does nothing with this yet #Where to store generated reports (full path) report_path= #/home/user/dir/reports From 1ad35fb7b8a03758ff27490c4a3b5ec4c412065e Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Fri, 5 Dec 2025 10:51:53 -0800 Subject: [PATCH 17/19] Baseline user friendly version of gecko --- gecko | 47 +++++++++++++++++---- triage_package/triage.ini | 77 ++++++++++++++++++++++++----------- triage_package/triage_tool.py | 65 ++++++++++++++++++++++++----- 3 files changed, 149 insertions(+), 40 deletions(-) diff --git a/gecko b/gecko index 8d89483..1e8372c 100755 --- a/gecko +++ b/gecko @@ -19,32 +19,65 @@ import argparse from triage_package.triage_tool import Triagetools #Fill out section then run like a CLI executable!############### -CONFIG_PATH = "/home/elijahab/general-dev/gecko/triage_package/triage.ini" +CONFIG_PATH = "triage_package/triage.ini" ################################################################ def run_triage(): '''Executable file for use on the Command line''' parser = argparse.ArgumentParser(description="Gecko command-line tool") - parser.add_argument( + + parser = argparse.ArgumentParser( + description=""" + Gecko Triage Tool + + A command-line utility to collect system and application logs, take screenshots, + grab science images, and optionally send a compressed report via email. + + You can either: + 1) Initialize the application (--initialize) to configure settings. + 2) Run a triage report (--message "description") to gather logs and send a report. + """, + formatter_class=argparse.RawTextHelpFormatter + ) + + # Mutually exclusive: either initialize or provide a message + group = parser.add_mutually_exclusive_group(required=True) + + group.add_argument( + "-init","--initialize", + action="store_true", + help="Initialize the application" + ) + + group.add_argument( "-m", "--message", type=str, - help="Commit message or description" + help="Run the triage workflow with a message describing the issue. " + "Example: --message 'GPU overheating issue observed today'" ) + args = parser.parse_args() - if args.message: - print(f"Message received: {args.message}") - gecko = Triagetools(config=CONFIG_PATH,message = args.message) - gecko.gather_system_info() + if args.initialize: + gecko = Triagetools(config=CONFIG_PATH, init=True) + print('\nInitialization complete!') + print('You can now run the GUI or use:\n gecko -m "your issue message" \n') + elif args.message: + gecko = Triagetools(config=CONFIG_PATH, message=args.message) + print(f"Running triage workflow for message: {args.message}\n") + gecko.gather_system_info() gecko.gather_logs() gecko.comb_logs() gecko.take_screenshots() gecko.grab_science_image() gecko.compress_report() + if gecko.email_alerts: gecko.send_report() + print("\nTriage workflow complete!") + if __name__ == "__main__": run_triage() diff --git a/triage_package/triage.ini b/triage_package/triage.ini index 291f519..f2f5946 100644 --- a/triage_package/triage.ini +++ b/triage_package/triage.ini @@ -1,33 +1,64 @@ -# Configuration file for Triage Tool -# Only change valus not names of parameters [System] -#***Does Nothing with this info right now*** -os = +help_text = + This section will have ask you to provide your os and version to take not of in the report. + (EXAMPLES) + os: ubuntu + os_version: 24.04 + + +os = ubuntu os_version = +initialized = -#Path to log file locations [Logs] -#Example: system = /var/log/syslog -#system = /home/path/to/logging/blue_cal_gammavac.log +help_text = + Please provide the path to your logs and science directory + The logs and science directory should be composed of dated dir in UTC format for the program to iterate through. + (EXAMPLES) In this example latest is symliked to the most current date + logs_dir: /data/latest/logs/ + science_dir: /data/latest/ + + +logs_dir = +science_dir = [Report] -#Enable or disable email alerts -email_alerts=False #***Does Nothing with this info right now*** -#Email address to send alerts to -instrument_master_email= #***Does Nothing with this info right now*** -sender_email = #sender email -sender_password = #Does nothing with this yet -#Where to store generated reports (full path) -report_path= #/home/user/dir/reports +help_text = + This section will ask for details on the reports. If you would not like email notifications, you can just provide a reports path. + (EXAMPLES) + email_alerts: false + instrument_master_email: eng@observatory.edu + sender_email: hello@gmail.com + sender_password: password + report_path: /home/user/dir/reports + + +email_alerts = +instrument_master_email = +sender_email = +sender_password = +report_path = [Machine] -#Threshold for alerting on high CPU usage (in percentage) -cpu_threshold=85 #***Does Nothing with this info right now*** -#Threshold for alerting on high Memory usage (in percentage) -memory_threshold=90 #***Does Nothing with this info right now*** +help_text = + Please provice percentage thresholds to monitor in the machine system. + (EXAMPLES) + cpu_threshold: 85 + memory_threshold: 90 + + +cpu_threshold = 75 +memory_threshold = 88 [VNC] -#IP address or host of session -host = #host.provider.com host.iden.edu -password = #Password1234 -vnc_sessions = #1,2,3,4,5,12 +help_text = + Please provide VNC details: + (EXAMPLES) + host: host.provider.com/host.iden.edu + password: Password1234 + vnc_sessions: 1,2,3,4,5,12 + + +host = +password = +vnc_sessions = diff --git a/triage_package/triage_tool.py b/triage_package/triage_tool.py index 234a103..425128c 100755 --- a/triage_package/triage_tool.py +++ b/triage_package/triage_tool.py @@ -17,6 +17,7 @@ -Elijah Anakalea-Buckley ''' import os +import sys import tarfile import shutil import configparser @@ -33,7 +34,7 @@ class Triagetools(object): """Triage tool for bug catching and error reporting""" - def __init__(self, config: str, message:str = ""): + def __init__(self, config: str, message:str = "", init = False): # Grab UTC, and load config file self.config_file = config self.utc_date = datetime.now(timezone.utc) @@ -43,27 +44,71 @@ def __init__(self, config: str, message:str = ""): self.message = message self.time_pattern = r"^(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?)" + # Create a ConfigParser object + self.config = configparser.ConfigParser() + + # Load config file if called + if init is True: + self.initialize(self.config_file) + else: + self.load_config(self.config_file) + + def initialize(self, config): + ''' Initialize and configure the application ''' + if not os.path.exists(config): + raise FileNotFoundError(f"Configuration file {config} not found") + self.config.read(config) - # Load config file - self.load_config(self.config_file) + #Iterate through prompts and populate ini file + for section in self.config.sections(): + print(f"\n\n====== Initializing Section: {section} ======") + for key, value in self.config[section].items(): + if key == "initialized": + continue + if key == "help_text": + help_text = value.replace("\\n", "\n") + print(help_text) + self.config[section][key] = f"{help_text}\n" + continue + # Remove inline comments (anything after '#') + clean_value = value.split('#')[0].strip() + # Prompt user; show current value if it exists + prompt = f"Enter value for '{key}'" + if clean_value: + prompt += f" (current: {clean_value})" + prompt += ": " + user_input = input(prompt).strip() + + # If user presses enter, keep existing value + if user_input == "": + user_input = clean_value + + # Save updated value + self.config[section][key] = user_input + + #set initialized to true for later execution and write + self.config["System"]["initialized"] = "true" + with open(self.config_file, "w", encoding="utf-8") as config_file: + self.config.write(config_file) def load_config(self, config): ''' Load the configuration file ''' if not os.path.exists(config): raise FileNotFoundError(f"Configuration file {config} not found") - - # Create a ConfigParser object - self.config = configparser.ConfigParser() self.config.read(config) + initialized = config.getboolean("System", "initialized") + + if not initialized: + print("\nYou have not Initialized this application! Please run: 'gecko -init'\n") + sys.exit(0) + #Report Section(report file name and current UTCdate folder) - if self.config["Report"]["email_alerts"].lower().strip() == "true": - self.email_alerts = True + self.email_alerts = config.getboolean("Report", "email_alerts") + if self.email_alerts: self.target_email = self.config["Report"]["instrument_master_email"] self.sender_email = self.config["Report"]["sender_email"] self.sender_password = self.config["Report"]["sender_password"] - else: - self.email_alerts = False self.r_path = self.config["Report"]["report_path"] self.log_dir = self.config["Logs"]["logs_dir"] self.science_dir = self.config["Logs"]["science_dir"] From ecf9c18ed2fa860cf7aeab7fd98c8bed6ad00b23 Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Fri, 5 Dec 2025 11:22:03 -0800 Subject: [PATCH 18/19] Updated readme --- README.md | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 2e6b77d..48ffe24 100644 --- a/README.md +++ b/README.md @@ -61,35 +61,36 @@ To install any optional dependencies, such as development dependencies, use: pip install -e .[dev] ``` -### Filling Out Config File +## Making the executable(optional) + +Run this in your terminal to create the gecko executable: -## Logs -Logs will have the path to any and all logs you would like to comb upon execution. This includes the system logs on more . This tool with comb the last 24 hours for ERRORS or WARNINGS assuming the log timestamps are formatted as ```bash -time_pattern = r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})" +cd gecko +chmod +x gecko ``` -You can change this format the the source code if you are familiar with python. -Example Log Path: +### Filling Out Config File + +You can run the command below and you will be prompted with questions to fill out the config file. Read promps and answer accordingly: + ```bash -FAM_Logs = ~/LOGS/data/devices/FAM/FAM.log +python3 gecko -init +(or ./gecko -init) ``` -## System -This holds the information for your Linux system and version. --TODO: Implent cross platform utility legacy system compatability +### Make Executable avaiable anywhere in the system + +Symlink the executable file to bin: -## VNC -Your VNC credentials and all of the sessions the engineers may want screenshots of. ```bash -host = "hostname.outlook.com" -password = "Hello_Sky" -vnc_sessions = 1,2,3,4 +sudo ln -s /path/to/repo/Gecko/gecko /usr/local/bin/gecko ``` -## Report -Fill out contact information and path to report storage +### Generate a report -## Executing from anywhere -Make sure your global python enviornment has all dependices and move executable into bin +You can now generate gecko reports from any terminal on the machine! (assuming python is available globally) +```bash +./gecko -m "" +``` \ No newline at end of file From a7da9df258ad2db082fc7143e17aaddfdb2dc8de Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Fri, 12 Dec 2025 17:17:08 -0800 Subject: [PATCH 19/19] Slight changes to time pattern management for logs, reporting os info, and printing helper_text --- gecko | 4 ++-- triage_package/triage.ini | 8 +++++--- triage_package/triage_tool.py | 13 ++++++++++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/gecko b/gecko index 1e8372c..144a7f6 100755 --- a/gecko +++ b/gecko @@ -74,8 +74,8 @@ def run_triage(): gecko.grab_science_image() gecko.compress_report() - if gecko.email_alerts: - gecko.send_report() + #if gecko.email_alerts: + # gecko.send_report() print("\nTriage workflow complete!") diff --git a/triage_package/triage.ini b/triage_package/triage.ini index f2f5946..839b5ec 100644 --- a/triage_package/triage.ini +++ b/triage_package/triage.ini @@ -26,18 +26,20 @@ science_dir = help_text = This section will ask for details on the reports. If you would not like email notifications, you can just provide a reports path. (EXAMPLES) + report_path: /home/user/dir/reports + time_pattern: ^(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?) email_alerts: false instrument_master_email: eng@observatory.edu sender_email: hello@gmail.com sender_password: password - report_path: /home/user/dir/reports - + +report_path = +time_pattern = ^(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?) email_alerts = instrument_master_email = sender_email = sender_password = -report_path = [Machine] help_text = diff --git a/triage_package/triage_tool.py b/triage_package/triage_tool.py index 425128c..33d89d5 100755 --- a/triage_package/triage_tool.py +++ b/triage_package/triage_tool.py @@ -66,9 +66,8 @@ def initialize(self, config): if key == "initialized": continue if key == "help_text": - help_text = value.replace("\\n", "\n") - print(help_text) - self.config[section][key] = f"{help_text}\n" + print(value.rstrip()) + print() # extra blank line continue # Remove inline comments (anything after '#') clean_value = value.split('#')[0].strip() @@ -117,6 +116,12 @@ def load_config(self, config): os.mkdir(f"{self.reports_path}") self.report_name = f"{self.reports_path}/gecko_report_{self.utc_time}.txt" self.regex_pattern = r"^.*error.*$|^.*warning.*$" + log_time_pattern = self.config["Reports"]["time_pattern"] + try: + # document the regex + self.time_pattern = re.compile(log_time_pattern) + except re.error as e: + raise ValueError(f"Invalid regex in config: {e}") #Machine Section self.cpu_threshold = self.config["Machine"]["cpu_threshold"] @@ -157,9 +162,11 @@ def gather_system_info(self): temps = psutil.sensors_temperatures() #Dict # Memory virtual_memory = psutil.virtual_memory() # + system_str = f"{self.os} :: {self.os_version}" with open(self.report_name, 'a', encoding='utf-8') as file: file.write("\n\n=====System Information=====\n") + file.write(f'{system_str}\n') file.write(f'Logical CPUs: {cpu_count_logical}\n') file.write(f'Physical CPUs: {cpu_count_physical}\n') file.write('Detailed CPU Usage:\n')