diff --git a/README.md b/README.md index 48ffe24..d4a30b6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,41 @@ -# python-package-template -This is a template on how to package a simple Python project +# Gecko Triage tool +## What is Gecko? + +Gecko is a command-line triage and diagnostics tool designed to help operators and engineers quickly collect useful debugging information when an instrument or system encounters a problem. + +Instead of manually gathering logs, screenshots, and science images, Gecko automates the process and produces a single packaged report that can be reviewed or shared with the development and operations teams. + +Gecko is especially useful during: + +- Instrument failures or unexpected behavior +- Software crashes or lockups +- Data acquisition issues +- Hardware communication problems +- On-sky observing anomalies + +--- + +### What Gecko Can Collect + +When run in triage mode, Gecko can automatically gather: + +- System and application log files +- Instrument-specific telemetry or diagnostic outputs +- Screenshots of the current system state +- Science images or recent exposure data (if configured) +- A compressed report bundle for archiving or emailing + +--- + +### Why Use Gecko? + +Gecko provides a consistent and repeatable way to capture critical debugging context at the moment an issue occurs. This reduces downtime and helps teams diagnose problems faster, without requiring users to manually locate and send multiple files. + +Once initialized, generating a triage report is as simple as: + +```bash +./gecko -u "your.name" -m "Description of the issue" +``` ## Table of Contents @@ -9,7 +45,7 @@ This is a template on how to package a simple Python project 4. Building Your Package 5. Publishing to PyPI -## Installation +### Installation To install the package in editable mode (ideal for development), follow these steps: @@ -79,6 +115,8 @@ python3 gecko -init (or ./gecko -init) ``` +[Initialization Guide](triage_package/INIT_GUIDE.md) + ### Make Executable avaiable anywhere in the system Symlink the executable file to bin: diff --git a/gecko b/gecko index a83be4b..a031271 100755 --- a/gecko +++ b/gecko @@ -42,53 +42,55 @@ def run_triage(): ) # Mutually exclusive: either initialize or provide a message - group = parser.add_mutually_exclusive_group(required=True) + #group = parser.add_mutually_exclusive_group(required=True) - group.add_argument( + parser.add_argument( "-init","--initialize", action="store_true", help="Initialize the application" ) - group.add_argument( + parser.add_argument( "-m", "--message", type=str, help="Run the triage workflow with a message describing the issue. " "Example: --message 'GPU overheating issue observed today'" ) - group.add_argument( - "-u", "-user", - action="store_true", + parser.add_argument( + "-u", "--user", + type=str, help="Input username to better track issues" ) args = parser.parse_args() 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') + if not args.message and not args.user: + 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 and args.user: - user_message = f"USER: {args.user}\nMESSAGE: {args.message}" - gecko = Triagetools(config=CONFIG_PATH, message=user_message) - print(f"Running triage workflow for user: {args.user}\nwith the 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 not args.initialize: + user_message = f"USER: {args.user}\nMESSAGE: {args.message}" + gecko = Triagetools(config=CONFIG_PATH, message=user_message) + print(f"Running triage workflow for user: {args.user}\nwith the 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!") else: print("\n\nYou need to include a user and a message in your report.") - print("example: ./gecko -u 'Steven.B' -m 'One of the channels looked weird " - "and I was unable to get any more data'") + print("example: \n./gecko -u 'Steven.B' -m 'One of the channels looked weird " + "and I was unable to get any more data'\n\n") if __name__ == "__main__": diff --git a/triage_package/INIT_GUIDE.md b/triage_package/INIT_GUIDE.md new file mode 100644 index 0000000..2960b9e --- /dev/null +++ b/triage_package/INIT_GUIDE.md @@ -0,0 +1,54 @@ +Gecko Initialization Guide (`-init`) +==================================== + +Gecko requires a configuration file before it can generate full triage reports. +The initialization process (`-init`) walks the user through this configuration step-by-step by asking questions and saving responses into an `.ini` file. + +This guide explains what happens during initialization, what information is required, and what each section of the config file controls. + +Overview: What Does `-init` Do? +------------------------------- + +Running:: + + ./gecko -init + +will prompt you with questions that need to be answered in order to create an accurate config/ini file for your reporting. + +Example Inputs +-------------- + +Each section will include a block of text with example inputs to simplify the configuration process. + +Important Sections +------------------ + +***Primary Sections*** + +Boolean Options:: + + email_alerts: false + +Who will receive the report email:: + + recipient_email: eng@observatory.edu + +Required Directories (must exist):: + + report_path: /home/user/dir/reports + logs_dir: /data/latest/logs/ + science_dir: /data/latest/ + +Notes +----- + +- All paths listed in the configuration must already exist on the system. Gecko will not create them automatically. +- Ensure the email address is valid if email alerts are enabled. +- Boolean options accept only ``true`` or ``false``. +- After initialization, you can manually edit the `.ini` file if needed. + +Summary +------- + +The ``-init`` process is designed to simplify setup by guiding the user through necessary configurations. +Once completed, Gecko can generate triage reports accurately using the saved configuration. diff --git a/triage_package/triage.ini b/triage_package/triage.ini index 2a2b249..8006a19 100644 --- a/triage_package/triage.ini +++ b/triage_package/triage.ini @@ -30,16 +30,16 @@ help_text = time_pattern: ^(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?) email_alerts: false recipient_email: eng@observatory.edu - sender_email: hello@gmail.com - sender_password: password + machine_name: VenusServer +# sender_password: password report_path = time_pattern = ^(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?) email_alerts = recipient_email = -sender_email = -sender_password = +machine_name = +#sender_password = [Machine] help_text = @@ -52,15 +52,15 @@ help_text = cpu_threshold = memory_threshold = -[VNC] -help_text = - Please provide VNC details: - (EXAMPLES) - host: host.provider.com/host.iden.edu - password: Password1234 - vnc_sessions: 1,2,3,4,5,12 +#[VNC] +#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 = +#host = +#password = +#vnc_sessions = diff --git a/triage_package/triage_tool.py b/triage_package/triage_tool.py index 1dfa2e7..d19784c 100755 --- a/triage_package/triage_tool.py +++ b/triage_package/triage_tool.py @@ -28,10 +28,10 @@ import re from datetime import datetime, timezone, timedelta import glob -import threading -import socket +import threading #pylint: disable=W0611 +import socket #pylint: disable=W0611 import psutil -from vncdotool import api +from vncdotool import api #pylint: disable=W0611 class Triagetools(object): """Triage tool for bug catching and error reporting""" @@ -45,6 +45,7 @@ def __init__(self, config: str, message:str = "", init = False): self.cutoff = datetime.now().replace(tzinfo=None) - timedelta(hours=24) self.message = message self.time_pattern = r"^(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?)" + self.tar_filename = "" # Create a ConfigParser object self.config = configparser.ConfigParser() @@ -108,8 +109,8 @@ def load_config(self, config): self.email_alerts = self.config.getboolean("Report", "email_alerts") if self.email_alerts: self.target_email = self.config["Report"]["recipient_email"] - self.sender_email = self.config["Report"]["sender_email"] - self.sender_password = self.config["Report"]["sender_password"] + self.machine_name = self.config["Report"]["machine_name"] + #self.sender_password = self.config["Report"]["sender_password"] self.r_path = self.config["Report"]["report_path"] self.log_dir = self.config["Logs"]["logs_dir"] self.science_dir = self.config["Logs"]["science_dir"] @@ -200,7 +201,7 @@ def take_screenshots(self): try: out = self.capture_session(s) print(f"Captured Session: {s} -> {out}") - except Exception as e: + except Exception as e: #pylint: disable=W0718 print(f"Failed to capture -> {s}: {e}") def capture_session(self, session): @@ -294,8 +295,24 @@ def comb_logs(self): def compress_report(self): '''Compresses report file into a tar.gz format to be emailed''' #Tar file and add it to message - 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}")) + #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}")) + + filename = f"{self.reports_path}/gecko_{self.utc_date}" + filename = filename.replace(" ", "_").replace('+','') + self.tar_filename = filename.replace(':', '').replace('.', '_') + ".tar.gz" + with tarfile.open(self.tar_filename, "w:gz") as tar: + for root, dirs, files in os.walk(self.reports_path): + for file in files: + + # Only include files containing utc_time + if self.utc_time in file or ".log" in file: + full_path = os.path.join(root, file) + + # Keep relative structure inside tar + arcname = os.path.relpath(full_path, self.reports_path) + + tar.add(full_path, arcname=arcname) def grab_science_image(self): '''Grabs most recent science image(s) to include in triage''' @@ -337,11 +354,12 @@ def send_report(self): # Create email msg = EmailMessage() msg['Subject'] = f'Gecko Report {self.utc_date}' - msg['From'] = self.sender_email # replace with actual sender + msg['From'] = self.machine_name # 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.") + body = f"\n{self.message}\n\n Unzip tar file to see logs, pngs, etc\n" + msg.set_content(body) #.txt file first with open(self.report_name, 'rb') as f: @@ -353,30 +371,49 @@ def send_report(self): ) #tar.gz file next - tar_file = f"{self.reports_path}/gecko_{self.utc_date}.tar.gz" - with open(tar_file, 'rb') as f: + with open(self.tar_filename, 'rb') as f: msg.add_attachment( f.read(), maintype='application', subtype='gzip', - filename=os.path.basename(tar_file) + filename=os.path.basename(self.tar_filename) ) # 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) + #image_files = glob.glob(os.path.join(self.reports_path, '**', f'*{self.utc_time}.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) ## 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.outlook.com', 465) as smtp: - smtp.login(self.sender_email, self.sender_password) # use an App Password - smtp.send_message(msg) + #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) + #print(f"Report sent to {self.target_email}") + + #for file in image_files: + # with open(file, "rb") as fp: + # msg.add_attachment( + # fp.read(), + # maintype="image", + # subtype="png", + # filename=os.path.basename(file) + # ) + + # Send using local sendmail instead of SMTP + try: + subprocess.run( + ["/usr/sbin/sendmail", "-t", "-oi"], + input=msg.as_bytes(), + check=True + ) + print(f"Report sent to {self.target_email}") - print(f"Report sent to {self.target_email}") + except Exception as e: #pylint: disable=W0718 + print("Failed to send report:", e)