From ba1bec40688df7f8e1cf51d136ca19ec1e5c31d4 Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Fri, 13 Mar 2026 14:31:09 -0700 Subject: [PATCH 1/2] Initial creation of flags for more user options, compression type and email options --- triage_package/triage.ini | 26 ++--- triage_package/triage_tool.py | 194 ++++++++++++++++++++++------------ 2 files changed, 132 insertions(+), 88 deletions(-) diff --git a/triage_package/triage.ini b/triage_package/triage.ini index 1ac7a87..047502d 100644 --- a/triage_package/triage.ini +++ b/triage_package/triage.ini @@ -28,19 +28,22 @@ help_text = (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 + email_alerts: true recipient_email: eng@observatory.edu -# sender_email: hello@gmail.com -# sender_password: password + sender_email: hello@gmail.com + sender_password: password report_path = time_pattern = ^(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?) email_alerts = +help_text = + If you ansered 0, no, false or off for email alerts, no answers for the email prompts will be relavant. + recipient_email = machine_name = -#sender_email = -#sender_password = +sender_email = +sender_password = [Machine] help_text = @@ -52,16 +55,3 @@ 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 - - -#host = -#password = -#vnc_sessions = diff --git a/triage_package/triage_tool.py b/triage_package/triage_tool.py index cbcee2c..2f9d172 100755 --- a/triage_package/triage_tool.py +++ b/triage_package/triage_tool.py @@ -44,7 +44,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.zip_filename = "" + self.filename = "" # Create a ConfigParser object self.config = configparser.ConfigParser() @@ -105,11 +105,18 @@ def load_config(self, config): sys.exit(0) #Report Section(report file name and current UTCdate folder) - self.email_alerts = self.config.getboolean("Report", "email_alerts") + self.email_alerts = self.config.getboolean("Report", "Send email alerts?") if self.email_alerts: + self.machine_email = self.config.getboolean("Report","Send email from machine?") + if not self.machine_email: + self.sender_email = self.config["Report"]["sender_email"] + self.sender_password = self.config["Report"]["sender_password"] + else: + self.machine_name = self.config["Report"]["machine_name"] self.target_email = self.config["Report"]["recipient_email"] - self.machine_name = self.config["Report"]["machine_name"] - #self.sender_password = self.config["Report"]["sender_password"] + self.include_images = self.config.getboolean("Report","Include images in email?") + self.attach_report = self.config.getboolean("Report","Include report in email?") + self.compression = (self.config["Reports"]["Compression type?(tar or zip)"]).lower() self.r_path = self.config["Report"]["report_path"] self.log_dir = self.config["Logs"]["logs_dir"] self.science_dir = self.config["Logs"]["science_dir"] @@ -133,12 +140,6 @@ def load_config(self, config): self.os = self.config["System"]["os"] self.os_version = self.config["System"]["os_version"] - #VNC information - #self.host = self.config["VNC"]["host"] - #self.password = self.config["VNC"]["password"] - #temp_sessions = self.config["VNC"]["vnc_sessions"] - #self.vnc_sessions = [int(num.strip()) for num in temp_sessions.split(',')] - #Put main message into the file with open(self.report_name, 'w', encoding='utf-8') as report_file: report_file.write("=========Reported Error From User==========\n") @@ -294,24 +295,62 @@ def comb_logs(self): def compress_report(self): """Compresses report files into a .zip format to be emailed""" - filename = f"{self.reports_path}/gecko_{self.utc_date}" - filename = filename.replace(" ", "_").replace("+", "") - self.zip_filename = filename.replace(":", "").replace(".", "_") + ".zip" + # 1. Define the base filename + base_name = f"{self.reports_path}/gecko_{self.utc_date}".replace(" ", "_").replace("+", "") + clean_name = base_name.replace(":", "").replace(".", "_") - with zipfile.ZipFile(self.zip_filename, "w", compression=zipfile.ZIP_DEFLATED) as zipf: - for root, dirs, files in os.walk(self.reports_path): # pylint: disable = W0612 - for file in files: + # 2. Handle the specific compression logic + if self.compression == "tar": + self.filename = clean_name + ".tar.gz" + context_manager = tarfile.open(self.filename, "w:gz") + else: + self.filename = clean_name + ".zip" + context_manager = zipfile.ZipFile(self.filename, "w", compression=zipfile.ZIP_DEFLATED) - # Only include files containing utc_time OR log files + # 3. Perform the actual archiving + with context_manager as archive: + for root, _, files in os.walk(self.reports_path): + for file in files: if self.utc_time in file or file.endswith(".log"): full_path = os.path.join(root, file) - - # Keep relative structure inside zip arcname = os.path.relpath(full_path, self.reports_path) - - zipf.write(full_path, arcname=arcname) - - print(f"Created ZIP file: {self.zip_filename}") + + # Use duck-typing: both objects have different methods for adding files + if isinstance(archive, tarfile.TarFile): + archive.add(full_path, arcname=arcname) + else: + archive.write(full_path, arcname=arcname) + + print(f"Created archive: {self.filename}") + #filename = f"{self.reports_path}/gecko_{self.utc_date}" + #filename = filename.replace(" ", "_").replace("+", "") + #if self.compression == "tar": + # # Clean up filename for the archive + # self.filename = filename.replace(":", "").replace(".", "_") + ".tar.gz" + # # Use "w:gz" for Write with Gzip compression + # with tarfile.open(self.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 OR log files + # if self.utc_time in file or file.endswith(".log"): + # full_path = os.path.join(root, file) + # # Keep relative structure inside tar + # arcname = os.path.relpath(full_path, self.reports_path) + # # In tarfile, use .add() instead of .write() + # tar.add(full_path, arcname=arcname) + # print(f"Created TAR.GZ file: {self.filename}") + #else: #make a .zip file + # self.filename = filename.replace(":", "").replace(".", "_") + ".zip" + # with zipfile.ZipFile(self.filename, "w", compression=zipfile.ZIP_DEFLATED) as zipf: + # for root, dirs, files in os.walk(self.reports_path): # pylint: disable = W0612 + # for file in files: + # # Only include files containing utc_time OR log files + # if self.utc_time in file or file.endswith(".log"): + # full_path = os.path.join(root, file) + # # Keep relative structure inside zip + # arcname = os.path.relpath(full_path, self.reports_path) + # zipf.write(full_path, arcname=arcname) + # print(f"Created ZIP file: {self.filename}") def grab_science_image(self): '''Grabs most recent science image(s) to include in triage''' @@ -355,15 +394,15 @@ def send_report(self): msg['Subject'] = f'Gecko Report {self.utc_date}' msg['From'] = self.machine_name # replace with actual sender msg['To'] = self.target_email # can be comma-separated string or list - zip_path = os.path.join(self.reports_path, self.zip_filename) + compressed_file_path = os.path.join(self.reports_path, self.filename) # Email body body = ( f"{self.message}\n\n" "To view logs and images:\n" - " 1. Download the attached .zip file\n" + " 1. Download the attached compressed file\n" " 2. Extract it\n\n" - f"Compressed report location:\n{zip_path}" + f"Compressed report location:\n{compressed_file_path}" ) msg.set_content(body) @@ -376,53 +415,68 @@ def send_report(self): filename=os.path.basename(self.report_name) ) - #.zip file next - #with open(self.zip_filename, 'rb') as f: - # msg.add_attachment( - # f.read(), - # maintype='application', - # subtype='gzip', - # filename=os.path.basename(self.zip_filename) - # ) + #compressed file next + # Use == for string comparison; 'is' checks object identity, which can fail for strings + if self.compression == "tar": + subtype = 'gzip' # Standard for .tar.gz + else: + subtype = 'zip' # Standard for .zip files + + if self.attach_report: + with open(self.filename, 'rb') as f: + file_data = f.read() + msg.add_attachment( + file_data, + maintype='application', + subtype=subtype, + filename=os.path.basename(self.filename) + ) # Attach PNG images recursively from the reports_path - #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) - #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}") + if self.include_images: + 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) + + # Connect to email provider SMTP to send + if not self.machine_email: + # Map of domains to their SMTP settings + smtp_settings = { + "gmail.com": ("smtp.gmail.com", 587), + "yahoo.com": ("smtp.mail.yahoo.com", 587), + "outlook.com": ("smtp-mail.outlook.com", 587), + "hotmail.com": ("smtp-mail.outlook.com", 587) + } + + # Extract domain from sender email (e.g., 'user@gmail.com' -> 'gmail.com') + domain = self.sender_email.split('@')[-1].lower() + + # Get settings or default to Outlook if not found + server_host, server_port = smtp_settings.get(domain, ("smtp-mail.outlook.com", 587)) + + try: + # Most providers now prefer SMTP + starttls() on port 587 + with smtplib.SMTP(server_host, server_port) as smtp: + smtp.starttls() # Upgrade the connection to secure + smtp.login(self.sender_email, self.sender_password) # Must be an App Password + smtp.send_message(msg) + print(f"Report successfully sent to {self.target_email}") + except Exception as e: + print(f"Failed to send email: {e}") + else: + try: + subprocess.run( + ["/usr/sbin/sendmail", "-t", "-oi"], + input=msg.as_bytes(), + check=True + ) + print(f"Report sent to {self.target_email}") - except Exception as e: #pylint: disable=W0718 - print("Failed to send report:", e) + except Exception as e: #pylint: disable=W0718 + print("Failed to send report:", e) def cleanup_reports_dir(self): """Clean up and rid of all files that are not a .zip file""" From d1fbf8ad36dfc6f85cb0ddcad9a620e9516040f3 Mon Sep 17 00:00:00 2001 From: Elijah A-B Date: Fri, 13 Mar 2026 15:33:09 -0700 Subject: [PATCH 2/2] Finished flag and needs to be tested after NGPS run or when another machine like it is available. can make sudo logs and test on a machine running VNC sooner given the time --- triage_package/triage.ini | 4 ++++ triage_package/triage_tool.py | 31 +------------------------------ 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/triage_package/triage.ini b/triage_package/triage.ini index 047502d..233e2a7 100644 --- a/triage_package/triage.ini +++ b/triage_package/triage.ini @@ -27,19 +27,23 @@ 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 + compression type: zip or tar time_pattern: ^(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?) email_alerts: true + machine_email: (true = send email as machine, false = provide email information) recipient_email: eng@observatory.edu sender_email: hello@gmail.com sender_password: password report_path = +compression = time_pattern = ^(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?) email_alerts = help_text = If you ansered 0, no, false or off for email alerts, no answers for the email prompts will be relavant. +machine_email = recipient_email = machine_name = sender_email = diff --git a/triage_package/triage_tool.py b/triage_package/triage_tool.py index 2f9d172..31d129f 100755 --- a/triage_package/triage_tool.py +++ b/triage_package/triage_tool.py @@ -116,7 +116,7 @@ def load_config(self, config): self.target_email = self.config["Report"]["recipient_email"] self.include_images = self.config.getboolean("Report","Include images in email?") self.attach_report = self.config.getboolean("Report","Include report in email?") - self.compression = (self.config["Reports"]["Compression type?(tar or zip)"]).lower() + self.compression = (self.config["Reports"]["compression"]).lower() self.r_path = self.config["Report"]["report_path"] self.log_dir = self.config["Logs"]["logs_dir"] self.science_dir = self.config["Logs"]["science_dir"] @@ -322,35 +322,6 @@ def compress_report(self): archive.write(full_path, arcname=arcname) print(f"Created archive: {self.filename}") - #filename = f"{self.reports_path}/gecko_{self.utc_date}" - #filename = filename.replace(" ", "_").replace("+", "") - #if self.compression == "tar": - # # Clean up filename for the archive - # self.filename = filename.replace(":", "").replace(".", "_") + ".tar.gz" - # # Use "w:gz" for Write with Gzip compression - # with tarfile.open(self.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 OR log files - # if self.utc_time in file or file.endswith(".log"): - # full_path = os.path.join(root, file) - # # Keep relative structure inside tar - # arcname = os.path.relpath(full_path, self.reports_path) - # # In tarfile, use .add() instead of .write() - # tar.add(full_path, arcname=arcname) - # print(f"Created TAR.GZ file: {self.filename}") - #else: #make a .zip file - # self.filename = filename.replace(":", "").replace(".", "_") + ".zip" - # with zipfile.ZipFile(self.filename, "w", compression=zipfile.ZIP_DEFLATED) as zipf: - # for root, dirs, files in os.walk(self.reports_path): # pylint: disable = W0612 - # for file in files: - # # Only include files containing utc_time OR log files - # if self.utc_time in file or file.endswith(".log"): - # full_path = os.path.join(root, file) - # # Keep relative structure inside zip - # arcname = os.path.relpath(full_path, self.reports_path) - # zipf.write(full_path, arcname=arcname) - # print(f"Created ZIP file: {self.filename}") def grab_science_image(self): '''Grabs most recent science image(s) to include in triage'''