#!/usr/bin/python

# -*- coding: utf-8 -*-
################################################################################
# Copyright (c) 2019 University of Utah Student Computing Labs.
# All Rights Reserved.
#
# Author: <NAME>
# Creation Date: Oct 2018
# Last Updated: March 2019
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby granted,
# provided that the above copyright notice appears in all copies and
# that both that copyright notice and this permission notice appear
# in supporting documentation, and that the name of The University
# of Utah not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission. This software is supplied as is without expressed or
# implied warranties of any kind.
################################################################################

import os
import sys
import socket
import logging
import plistlib
import subprocess
import tkMessageBox
import Tkinter as tk
import traceback as tb
import xml.etree.cElementTree as ET

blade_runner_dir = os.path.dirname(os.path.dirname(__file__))
sys.path.insert(0, os.path.join(blade_runner_dir, "dependencies"))
sys.path.insert(0, os.path.join(blade_runner_dir, "slack"))
sys.path.insert(0, blade_runner_dir)

from blade_runner.jamf_pro.jss_doc import JssDoc
from blade_runner.views.main_view import MainView
from blade_runner.user_actions import user_actions
from blade_runner.jamf_pro.computer import Computer
from blade_runner.jamf_pro.jss_server import JssServer
from blade_runner.windows.stall_window import StallWindow
from blade_runner.controllers.controller import Controller
from blade_runner.jamf_pro.params import SearchParams, VerifyParams
from blade_runner.controllers.search_controller import SearchController
from blade_runner.controllers.dual_verify_controller import DualVerifyController
from blade_runner.controllers.verification_controller import VerificationController
from blade_runner.dependencies.management_tools.slack import IncomingWebhooksSender as IWS


# TODO Interface with a Trello board and dynamically create lists for the DEP
# TODO BUG: If spaces are entered in the asset/barcode inputs the program quits.

logging.getLogger(__name__).addHandler(logging.NullHandler())


class MainController(Controller):
    """Blade-Runner's main controller."""

    def __init__(self, root, server, slack_data, verify_params, search_params, doc_settings):
        """Initialize the main controller.

        Args:
            root: The root Tk window.
            server (JssServer): The JSS server to connect to.
            slack_data (dict): Slack configuration data.
            verify_params (dict): Verification parameters configuration data.
            search_params (dict): Search parameters configuration data.
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        self.logger = logging.getLogger(__name__)
        # Set report callback so that a Tk window will appear with the exception message when an exception occurs.
        self._root = root
        self._root.report_callback_exception = self._exception_messagebox
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # JSS server
        self._jss_server = server
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Stores information about the computer
        self._computer = Computer()
        self._computer.serial_number = self._computer.get_serial()
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Used to exit out of a function if the user cancels.
        self._proceed = True
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set views
        self._main_view = None
        self._entry_view = None
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set controllers
        self._search_controller = None
        self._verify_controller = None
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set whether or not a JSSDoc will be printed to a printer.
        self._doc_settings = doc_settings
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set configuration data
        self._offboard_config = None
        self._slack_data = slack_data
        self.verify_params = VerifyParams(verify_params)
        self.search_params = SearchParams(search_params)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Save Verify/SearchParams original input
        self.verify_params_input = verify_params
        self.search_params_input = search_params
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set path to config directory that contains the configuration files.

        app_root_dir = os.path.abspath(__file__)
        for i in range(3):
            app_root_dir = os.path.dirname(app_root_dir)

        self.app_root_dir = app_root_dir
        self._config_dir = os.path.join(self.app_root_dir, "config")
        self._blade_runner_dir = os.path.join(self.app_root_dir, "blade_runner")
        self._slack_dir = os.path.join(self._blade_runner_dir, "slack")
        self._offboard_configs_dir = os.path.join(self._config_dir, "offboard_configs")
        self._secure_erase_dir = os.path.join(self._blade_runner_dir, "secure_erase")

    def _exception_messagebox(self, exc, value, traceback):
        """Displays a message box with the accompanying exception message whenver an exception occurs.

        Args:
            exc: Exception received.
            value: Value of exception.
            traceback: Traceback from exception.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        try:
            # Grab the last value in the value tuple. This is normally the message.
            message = "{}".format(value[-1])
            # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
            # Display a message box containing the exception's message.
            tkMessageBox.showerror('Exception', message)
        except IndexError:
            tkMessageBox.showerror('Exception', value)
        finally:
            self.logger.error("".join(tb.format_exception(exc, value, traceback)))

    def run(self):
        """Start Blade-Runner.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Create the main view.
        self._main_view = MainView(self._root, self)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set it to the middle of the screen.
        self._set_to_middle(self._main_view)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Refocus on the window.
        self.refocus()
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Make this window the main loop.
        self._main_view.mainloop()

    def secure_erase(self):
        """Runs secure_erase.sh, which runs secure_erase_internals, in a new terminal window.

        Returns:
            void
        """
        subprocess.check_output(["open", "-n", "-a", "Terminal", os.path.join(self._secure_erase_dir, "secure_erase.sh")])

    def terminate(self):
        """Terminates Blade-Runner.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Destroy the main view and the root window.
        self._main_view.destroy()
        self._root.destroy()

    def restart(self):
        """Restart Blade-Runner without quiting the entire program.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Create a new Computer object.
        self._computer = Computer()
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Create a new VerifyParams object.
        self.verify_params = VerifyParams(self.verify_params_input)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Create a new SearchParams object.
        self.search_params = SearchParams(self.search_params_input)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        self.logger.info("Blade-Runner reset.")

    def populate_config_combobox(self):
        """Populates the main view's combobox with available configuration files.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Get the XML config files from the config directory
        offboard_configs = self._get_offboard_configs(self._offboard_configs_dir)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Fill the main view's combobox with the XML files.
        self._main_view.combobox.config(values=offboard_configs)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set the initial choice to the first XML file in the list.
        self._main_view.combobox.current(0)

    def _get_offboard_configs(self, dir):
        """Gets the offboard XML configuration files from a given directory.

        Args:
            dir (str): Path to config directory.

        Returns:
            list: XML configuration files.
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Get a list of the files in the config directory.
        files = os.listdir(dir)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Get all XML files in the directory.
        offboard_configs = []
        for file in files:
            if file.endswith(".xml") and file[0] != "." and file[0] != "~":
                offboard_configs.append(file)
        return offboard_configs

    def save_offboard_config(self, offboard_config):
        """Save the offboard config as an XML string.

        Args:
            offboard_config (str): File name, not the file path.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Parse XML file into an XML tree.
        xml_file = os.path.join(self._offboard_configs_dir, offboard_config)
        xml_tree = ET.parse(xml_file)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Get the root of the tree.
        xml_root = xml_tree.getroot()
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Create offboard xml string and store it.
        self._offboard_config = ET.tostring(xml_root)

    def search_sequence(self, input_type):
        """Determines the search window sequence based on the input type and previous search params.

        Args:
            input_type (str): Data identifier.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Process the input type and handle the JSS search.
        self._main(input_type)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # For every search parameter enabled
        for param in self.search_params:
            # If search parameter hasn't been searched yet.
            if not self.search_params.was_searched(param):
                # If user hasn't canceled operation and no match was found
                if self._proceed is True and self._computer.jss_id is None:
                    # Process the input type and handle the JSS search.
                    self._main(param)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # All search params have been searched and Blade-Runner has finished processing the computer.
        # Restart Blade-Runner.
        self.restart()

    def _main(self, input_type):
        """Handles search events.

        Args:
            input_type (str): Data identifier.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Open the search view for the input type.
        self._open_search_view(input_type)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # If the user canceled the search view, proceed will be false, Blade-Runner will restart, and function returns.
        if self._proceed is False:
            self.restart()
            return
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Since the computer object has been filled with user input data for the input type, search the JSS.
        self._dynamic_search(self._computer, input_type)
        self.search_params.set_searched(input_type)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # If the search resulted in a match...
        if self._computer.jss_id:
            # Set match
            self.search_params.set_match(input_type)
            # Open view  so user can verify inputted data against JSS data.
            self._open_dual_verify_view()
            # If user didn't cancel operation...
            if self._proceed:
                # Get managed status of computer
                managed = self._jss_server.get_managed_status(self._computer.jss_id)
                self.logger.debug("Management status: {}".format(managed))
                # If managed status is false, re-enroll computer to set managed status to true. This enables the
                # JSS record to be modified
                if managed == 'false':
                    msg = "Enrolling to change managed status to true to enable modification of the Jamf Pro record."
                    self.logger.debug(msg)
                    # Open a stall window, enroll the computer, and close the stall window.
                    enroll_window = StallWindow(self._main_view, self._jss_server.enroll_computer, msg, process=True)
                    if not enroll_window.proceed:
                        return
            else:
                return
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Otherwise, if all input types have been searched...
        elif self.search_params.all_searched():
            # Open view  so user can verify inputted data.
            self._open_verify_view()
            # If user didn't cancel operation...
            if self._proceed:
                msg = "Enrolling because no Jamf Pro ID exists for this computer."
                self.logger.debug(msg)
                # Open a stall window, enroll the computer, and close the stall window.
                enroll_window = StallWindow(self._main_view, self._jss_server.enroll_computer, msg, process=True)
                if not enroll_window.proceed:
                    return
                # Since JSS ID has now been created, retrieve it.
                self._computer.jss_id = self._jss_server.match(self._computer.serial_number)
                self.logger.debug("Jamf Pro ID after enrolling: {}".format(self._computer.jss_id))
            else:
                return
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Otherwise, return.
        elif self._computer.jss_id is None:
            return
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Push computer's identity fields to the JSS.
        self._jss_server.push_identity_fields(self._computer)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Offboard computer
        self._offboard()
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Send slack message and start slackify daemon according to slack data config.
        self._slack_handler(self._slack_data)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Create and print JssDoc according to settings.
        self._jss_doc_handler(self._computer, self._jss_server, self._doc_settings)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        self.logger.info("Blade Runner successfully finished.")
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Display success message box.
        tkMessageBox.showinfo("Finished", "Blade-Runner successfully finished.")

    def _dynamic_search(self, computer, input_type):
        """Dynamically search the JSS with the Computer attribute that matches the input type.

        Args:
            computer (Computer): Stores information about the computer.
            input_type (str): Data identifier.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Use the computer object to set the search parameter.
        if input_type == "barcode_1":
            search_param = computer.barcode_1
        elif input_type == "barcode_2":
            search_param = computer.barcode_2
        elif input_type == "asset_tag":
            search_param = computer.asset_tag
        elif input_type == "serial_number":
            search_param = computer.serial_number
        else:
            raise SystemExit("The input type \"{}\" supplied from the button is not supported.".format(input_type))
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Search the JSS.
        computer.jss_id = self._jss_server.match(search_param)

    def _open_search_view(self, input_type):
        """Open the search view. Saves user input in computer object.

        Args:
            input_type (str): Data identifier.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # If serial number, save serial number to computer object. This allows the serial to be displayed before
        # submitting it for a search.
        if input_type == "serial_number":
            self._computer.serial_number = self._computer.get_serial()
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Open the search controller/view. Inputs given to the view will be saved in the computer object.
        self._search_controller = SearchController(self._main_view, self._computer, input_type)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Wait for search view to close.
        self._main_view.wait_window(window=self._search_controller.entry_view)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # If the user canceled the operation, proceed will be false.
        self._proceed = self._search_controller.proceed

    def _open_verify_view(self):
        """Opens a VerifyView window.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        self._verify_controller = VerificationController(self._main_view, self._computer, self.verify_params,
                                                         self.search_params)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Wait for entry view window to close.
        self._main_view.wait_window(window=self._verify_controller.entry_view)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # After entry view window has been closed through the "submit" button, proceed will be True, and the barcode,
        # asset, and name fields of the computer object will be updated.
        self._proceed = self._verify_controller.proceed

    def _open_dual_verify_view(self):
        """Opens a DualVerifyView window.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Open DualVerifyWindow/Controller
        self._verify_controller = DualVerifyController(self._main_view, self._computer, self.verify_params,
                                                       self._jss_server)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Wait for entry view window to close.
        self._main_view.wait_window(window=self._verify_controller.entry_view)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Store exit status of the view.
        self._proceed = self._verify_controller.proceed

    def _offboard(self):
        """Offboard computer object with the offboard config.

        Returns:

        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Update the JSS record according to the implementation of the user.
        user_actions.update_offboard_config(self)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Push the offboard config to the JSS to offboard the computer.
        self._jss_server.push_xml_str(self._offboard_config, self._computer.jss_id)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Get the new managed status and make sure it's false
        post_managed_status = self._jss_server.get_managed_status(self._computer.jss_id)
        self.logger.debug("post_managed_status: {}".format(post_managed_status))
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Make sure managed status is false. If not, exit.
        if post_managed_status != 'false':
            msg = "Managed status was not false after offboarding. Aborting."
            self.logger.error(msg)
            self.restart()
            raise SystemError(msg)

    def _slack_handler(self, slack_data):
        """Handles all Slack related items: sends Slack message and starts Slackify daemon.

        Args:
            slack_data (dict): Slack configuration.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Notify slack channel that offboard tool has finished.
        if slack_data['slack_enabled'] == "True":
            message = slack_data['default_message']
            message = user_actions.update_slack_message(self, message)
            self.send_slack_message(message)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Start Slackify daemon
        if slack_data['slackify_daemon_enabled'] == 'True':
            self.logger.debug("Starting Slackify reminder daemon.")
            self.start_slackify_reminder_dameon()

    def _jss_doc_handler(self, computer, jss_server, doc_settings):
        """Creates a document from the predefined setup in JssDoc and prints the document
        to the default printer if print_doc is True.

        Args:
            computer (Computer): Stores information about the computer. Only used for JSS ID and "incorrect fields".
            jss_server (JssServer): JSS server to query.
            print_doc (bool): Prints document to default printer if True.

        Returns:

        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Prepare document for printing
        doc = JssDoc(jss_server, computer)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Create html document from settings in JssDoc.
        doc.create_html()
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Convert HTML document into a PDF document.
        doc.html_to_pdf()
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # If document printing is enabled, print to the default printer.
        # if print_doc:
        #     doc.print_pdf_to_default()
        if doc_settings['print'].lower() == "true":
            doc.print_pdf_to_default()

    def send_slack_message(self, message):
        """Sends a Slack message to a specified channel and Slack url.

        Args:
            message (str): Message to be sent to the Slack channel.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Get the current IP
        current_ip = socket.gethostbyname(socket.gethostname())
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set up the Slack bot
        slack_bot = IWS(self._slack_data['slack_url'], bot_name=current_ip, channel=self._slack_data['slack_channel'])
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Send the Slack message
        slack_bot.send_message(message)

    def start_slackify_reminder_dameon(self):
        """Start the Slackify reminder daemon.

        Notes:
            The Slackify reminder daemon can't be imported into this script and run directly. In order for it to
            run as it's own detached process after this script finishes, it must be run through a subprocess call.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set up and run the command.
        cmd = ['/usr/bin/python', '-m', 'blade_runner.slack.slackify_reminder_daemon']
        subprocess.Popen(cmd, stderr=subprocess.STDOUT)

    def refocus(self):
        """Set the focus to the Python application.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set up the command
        select_window = ['/usr/bin/osascript', '-e',
                         'tell application "Finder" to set frontmost of process "Python" to true']
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        try:  # set focus to the main view.
            subprocess.check_output(select_window)
        except subprocess.CalledProcessError:
            self.logger.debug("Setting frontmost of process Python to true failed.")

    def open_config(self, config_id):
        """Opens the specified configuration file in its default application.

        Args:
            config_id (str): Denotes which path to use.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Determine the path to the configuration file.
        if config_id == "slack":
            path = "slack_configs/slack.plist"
        elif config_id == "offboard":
            path = "offboard_configs/"
        elif config_id == "verify":
            path = "verify_params_configs/verify_params.plist"
        elif config_id == "search":
            path = "search_params_configs/search_params.plist"
        elif config_id == "config":
            path = ""
        elif config_id == "print":
            path = "print_config/print.plist"
        elif config_id == "python_bin":
            path = "python_bin_config/python_bin.plist"
        elif config_id == "jamf_pro":
            path = "jamf_pro_configs/jamf_pro.plist"
        else:
            self.logger.warn("No configuration ID matches \"{}\"".format(config_id))
            raise SystemError("No configuration ID matches \"{}\"".format(config_id))
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Open the file in its default application
        subprocess.check_output(["open", os.path.join(self._config_dir, path)])

    def cat_readme(self):
        """Outputs the text of README.md.

        Returns:
            Text of README.md.
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set the path to README.md.
        readme = os.path.join(self.app_root_dir, "README.md")
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Read README.md contents and return them as a single string.
        with open(readme, "r") as f:
            return f.read()


def main():
    # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
    # Ensure script is being run as root.
    if os.geteuid() != 0:
        raise SystemExit("Blade Runner must be run as root.")
    # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
    # Set up logging values.
    fmt = '%(asctime)s %(process)d: %(levelname)8s: %(name)s.%(funcName)s: %(message)s'
    script_name = os.path.splitext(os.path.basename(__file__))[0]
    log_dir = os.path.join(os.path.expanduser("~"), "Library/Logs/Blade Runner")
    filepath = os.path.join(log_dir, script_name + ".log")

    # Create directory for log if it DNE.
    try:
        os.mkdir(log_dir)
    except OSError as e:
        if e.errno != 17:
            raise e

    # Ensure that the owner is the logged in user.
    subprocess.check_output(['chown', '-R', os.getlogin(), log_dir])

    # Set up and get logger.
    logging.basicConfig(level=logging.DEBUG, format=fmt, filemode='a', filename=filepath)
    logger = logging.getLogger(script_name)
    # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
    # Create main root window.
    root = tk.Tk()
    root.withdraw()
    # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
    # Set up parent folder of config.
    abs_file_path = os.path.abspath(__file__)
    blade_runner_dir = abs_file_path
    for i in range(3):
        blade_runner_dir = os.path.dirname(blade_runner_dir)
    # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
    # Read from jss config plist and set up the Jamf Pro server
    jss_server_plist = os.path.join(blade_runner_dir, "config/jamf_pro_configs/jamf_pro.plist")
    jss_server_data = plistlib.readPlist(jss_server_plist)
    jss_server = JssServer(**jss_server_data)
    logger.debug(jss_server._jss_url)

    # Read from Slack config plist to set up Slack notifications
    slack_plist = os.path.join(blade_runner_dir, "config/slack_configs/slack.plist")
    slack_data = plistlib.readPlist(slack_plist)

    # Read from verify params plist to set up verification parameters
    verify_config = os.path.join(blade_runner_dir, "config/verify_params_configs/verify_params.plist")
    verify_params = plistlib.readPlist(verify_config)

    # Read from search params plist to set up search parameters.
    search_params_config = os.path.join(blade_runner_dir, "config/search_params_configs/search_params.plist")
    search_params = plistlib.readPlist(search_params_config)

    # Read from print config to enable or disable printing.
    print_config = os.path.join(blade_runner_dir, "config/print_config/print.plist")
    print_settings = plistlib.readPlist(print_config)
    # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
    # Run the application
    app = MainController(root, jss_server, slack_data, verify_params, search_params, print_settings)
    app.run()
    logger.info("Blade Runner exiting.")


if __name__ == "__main__":
    main()



#!/usr/bin/python

# -*- coding: utf-8 -*-
################################################################################
# Copyright (c) 2019 University of Utah Student Computing Labs.
# All Rights Reserved.
#
# Author: <NAME>
# Creation Date: Oct 2018
# Last Updated: March 2019
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby granted,
# provided that the above copyright notice appears in all copies and
# that both that copyright notice and this permission notice appear
# in supporting documentation, and that the name of The University
# of Utah not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission. This software is supplied as is without expressed or
# implied warranties of any kind.
################################################################################

import os
import sys
import socket
import logging
import plistlib
import subprocess
import tkMessageBox
import Tkinter as tk
import traceback as tb
import xml.etree.cElementTree as ET

blade_runner_dir = os.path.dirname(os.path.dirname(__file__))
sys.path.insert(0, os.path.join(blade_runner_dir, "dependencies"))
sys.path.insert(0, os.path.join(blade_runner_dir, "slack"))
sys.path.insert(0, blade_runner_dir)

from blade_runner.jamf_pro.jss_doc import JssDoc
from blade_runner.views.main_view import MainView
from blade_runner.user_actions import user_actions
from blade_runner.jamf_pro.computer import Computer
from blade_runner.jamf_pro.jss_server import JssServer
from blade_runner.windows.stall_window import StallWindow
from blade_runner.controllers.controller import Controller
from blade_runner.jamf_pro.params import SearchParams, VerifyParams
from blade_runner.controllers.search_controller import SearchController
from blade_runner.controllers.dual_verify_controller import DualVerifyController
from blade_runner.controllers.verification_controller import VerificationController
from blade_runner.dependencies.management_tools.slack import IncomingWebhooksSender as IWS


# TODO Interface with a Trello board and dynamically create lists for the DEP
# TODO BUG: If spaces are entered in the asset/barcode inputs the program quits.

logging.getLogger(__name__).addHandler(logging.NullHandler())


class MainController(Controller):
    """Blade-Runner's main controller."""

    def __init__(self, root, server, slack_data, verify_params, search_params, doc_settings):
        """Initialize the main controller.

        Args:
            root: The root Tk window.
            server (JssServer): The JSS server to connect to.
            slack_data (dict): Slack configuration data.
            verify_params (dict): Verification parameters configuration data.
            search_params (dict): Search parameters configuration data.
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        self.logger = logging.getLogger(__name__)
        # Set report callback so that a Tk window will appear with the exception message when an exception occurs.
        self._root = root
        self._root.report_callback_exception = self._exception_messagebox
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # JSS server
        self._jss_server = server
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Stores information about the computer
        self._computer = Computer()
        self._computer.serial_number = self._computer.get_serial()
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Used to exit out of a function if the user cancels.
        self._proceed = True
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set views
        self._main_view = None
        self._entry_view = None
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set controllers
        self._search_controller = None
        self._verify_controller = None
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set whether or not a JSSDoc will be printed to a printer.
        self._doc_settings = doc_settings
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set configuration data
        self._offboard_config = None
        self._slack_data = slack_data
        self.verify_params = VerifyParams(verify_params)
        self.search_params = SearchParams(search_params)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Save Verify/SearchParams original input
        self.verify_params_input = verify_params
        self.search_params_input = search_params
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set path to config directory that contains the configuration files.

        app_root_dir = os.path.abspath(__file__)
        for i in range(3):
            app_root_dir = os.path.dirname(app_root_dir)

        self.app_root_dir = app_root_dir
        self._config_dir = os.path.join(self.app_root_dir, "config")
        self._blade_runner_dir = os.path.join(self.app_root_dir, "blade_runner")
        self._slack_dir = os.path.join(self._blade_runner_dir, "slack")
        self._offboard_configs_dir = os.path.join(self._config_dir, "offboard_configs")
        self._secure_erase_dir = os.path.join(self._blade_runner_dir, "secure_erase")

    def _exception_messagebox(self, exc, value, traceback):
        """Displays a message box with the accompanying exception message whenver an exception occurs.

        Args:
            exc: Exception received.
            value: Value of exception.
            traceback: Traceback from exception.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        try:
            # Grab the last value in the value tuple. This is normally the message.
            message = "{}".format(value[-1])
            # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
            # Display a message box containing the exception's message.
            tkMessageBox.showerror('Exception', message)
        except IndexError:
            tkMessageBox.showerror('Exception', value)
        finally:
            self.logger.error("".join(tb.format_exception(exc, value, traceback)))

    def run(self):
        """Start Blade-Runner.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Create the main view.
        self._main_view = MainView(self._root, self)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set it to the middle of the screen.
        self._set_to_middle(self._main_view)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Refocus on the window.
        self.refocus()
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Make this window the main loop.
        self._main_view.mainloop()

    def secure_erase(self):
        """Runs secure_erase.sh, which runs secure_erase_internals, in a new terminal window.

        Returns:
            void
        """
        subprocess.check_output(["open", "-n", "-a", "Terminal", os.path.join(self._secure_erase_dir, "secure_erase.sh")])

    def terminate(self):
        """Terminates Blade-Runner.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Destroy the main view and the root window.
        self._main_view.destroy()
        self._root.destroy()

    def restart(self):
        """Restart Blade-Runner without quiting the entire program.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Create a new Computer object.
        self._computer = Computer()
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Create a new VerifyParams object.
        self.verify_params = VerifyParams(self.verify_params_input)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Create a new SearchParams object.
        self.search_params = SearchParams(self.search_params_input)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        self.logger.info("Blade-Runner reset.")

    def populate_config_combobox(self):
        """Populates the main view's combobox with available configuration files.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Get the XML config files from the config directory
        offboard_configs = self._get_offboard_configs(self._offboard_configs_dir)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Fill the main view's combobox with the XML files.
        self._main_view.combobox.config(values=offboard_configs)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set the initial choice to the first XML file in the list.
        self._main_view.combobox.current(0)

    def _get_offboard_configs(self, dir):
        """Gets the offboard XML configuration files from a given directory.

        Args:
            dir (str): Path to config directory.

        Returns:
            list: XML configuration files.
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Get a list of the files in the config directory.
        files = os.listdir(dir)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Get all XML files in the directory.
        offboard_configs = []
        for file in files:
            if file.endswith(".xml") and file[0] != "." and file[0] != "~":
                offboard_configs.append(file)
        return offboard_configs

    def save_offboard_config(self, offboard_config):
        """Save the offboard config as an XML string.

        Args:
            offboard_config (str): File name, not the file path.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Parse XML file into an XML tree.
        xml_file = os.path.join(self._offboard_configs_dir, offboard_config)
        xml_tree = ET.parse(xml_file)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Get the root of the tree.
        xml_root = xml_tree.getroot()
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Create offboard xml string and store it.
        self._offboard_config = ET.tostring(xml_root)

    def search_sequence(self, input_type):
        """Determines the search window sequence based on the input type and previous search params.

        Args:
            input_type (str): Data identifier.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Process the input type and handle the JSS search.
        self._main(input_type)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # For every search parameter enabled
        for param in self.search_params:
            # If search parameter hasn't been searched yet.
            if not self.search_params.was_searched(param):
                # If user hasn't canceled operation and no match was found
                if self._proceed is True and self._computer.jss_id is None:
                    # Process the input type and handle the JSS search.
                    self._main(param)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # All search params have been searched and Blade-Runner has finished processing the computer.
        # Restart Blade-Runner.
        self.restart()

    def _main(self, input_type):
        """Handles search events.

        Args:
            input_type (str): Data identifier.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Open the search view for the input type.
        self._open_search_view(input_type)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # If the user canceled the search view, proceed will be false, Blade-Runner will restart, and function returns.
        if self._proceed is False:
            self.restart()
            return
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Since the computer object has been filled with user input data for the input type, search the JSS.
        self._dynamic_search(self._computer, input_type)
        self.search_params.set_searched(input_type)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # If the search resulted in a match...
        if self._computer.jss_id:
            # Set match
            self.search_params.set_match(input_type)
            # Open view  so user can verify inputted data against JSS data.
            self._open_dual_verify_view()
            # If user didn't cancel operation...
            if self._proceed:
                # Get managed status of computer
                managed = self._jss_server.get_managed_status(self._computer.jss_id)
                self.logger.debug("Management status: {}".format(managed))
                # If managed status is false, re-enroll computer to set managed status to true. This enables the
                # JSS record to be modified
                if managed == 'false':
                    msg = "Enrolling to change managed status to true to enable modification of the Jamf Pro record."
                    self.logger.debug(msg)
                    # Open a stall window, enroll the computer, and close the stall window.
                    enroll_window = StallWindow(self._main_view, self._jss_server.enroll_computer, msg, process=True)
                    if not enroll_window.proceed:
                        return
            else:
                return
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Otherwise, if all input types have been searched...
        elif self.search_params.all_searched():
            # Open view  so user can verify inputted data.
            self._open_verify_view()
            # If user didn't cancel operation...
            if self._proceed:
                msg = "Enrolling because no Jamf Pro ID exists for this computer."
                self.logger.debug(msg)
                # Open a stall window, enroll the computer, and close the stall window.
                enroll_window = StallWindow(self._main_view, self._jss_server.enroll_computer, msg, process=True)
                if not enroll_window.proceed:
                    return
                # Since JSS ID has now been created, retrieve it.
                self._computer.jss_id = self._jss_server.match(self._computer.serial_number)
                self.logger.debug("Jamf Pro ID after enrolling: {}".format(self._computer.jss_id))
            else:
                return
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Otherwise, return.
        elif self._computer.jss_id is None:
            return
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Push computer's identity fields to the JSS.
        self._jss_server.push_identity_fields(self._computer)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Offboard computer
        self._offboard()
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Send slack message and start slackify daemon according to slack data config.
        self._slack_handler(self._slack_data)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Create and print JssDoc according to settings.
        self._jss_doc_handler(self._computer, self._jss_server, self._doc_settings)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        self.logger.info("Blade Runner successfully finished.")
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Display success message box.
        tkMessageBox.showinfo("Finished", "Blade-Runner successfully finished.")

    def _dynamic_search(self, computer, input_type):
        """Dynamically search the JSS with the Computer attribute that matches the input type.

        Args:
            computer (Computer): Stores information about the computer.
            input_type (str): Data identifier.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Use the computer object to set the search parameter.
        if input_type == "barcode_1":
            search_param = computer.barcode_1
        elif input_type == "barcode_2":
            search_param = computer.barcode_2
        elif input_type == "asset_tag":
            search_param = computer.asset_tag
        elif input_type == "serial_number":
            search_param = computer.serial_number
        else:
            raise SystemExit("The input type \"{}\" supplied from the button is not supported.".format(input_type))
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Search the JSS.
        computer.jss_id = self._jss_server.match(search_param)

    def _open_search_view(self, input_type):
        """Open the search view. Saves user input in computer object.

        Args:
            input_type (str): Data identifier.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # If serial number, save serial number to computer object. This allows the serial to be displayed before
        # submitting it for a search.
        if input_type == "serial_number":
            self._computer.serial_number = self._computer.get_serial()
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Open the search controller/view. Inputs given to the view will be saved in the computer object.
        self._search_controller = SearchController(self._main_view, self._computer, input_type)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Wait for search view to close.
        self._main_view.wait_window(window=self._search_controller.entry_view)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # If the user canceled the operation, proceed will be false.
        self._proceed = self._search_controller.proceed

    def _open_verify_view(self):
        """Opens a VerifyView window.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        self._verify_controller = VerificationController(self._main_view, self._computer, self.verify_params,
                                                         self.search_params)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Wait for entry view window to close.
        self._main_view.wait_window(window=self._verify_controller.entry_view)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # After entry view window has been closed through the "submit" button, proceed will be True, and the barcode,
        # asset, and name fields of the computer object will be updated.
        self._proceed = self._verify_controller.proceed

    def _open_dual_verify_view(self):
        """Opens a DualVerifyView window.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Open DualVerifyWindow/Controller
        self._verify_controller = DualVerifyController(self._main_view, self._computer, self.verify_params,
                                                       self._jss_server)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Wait for entry view window to close.
        self._main_view.wait_window(window=self._verify_controller.entry_view)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Store exit status of the view.
        self._proceed = self._verify_controller.proceed

    def _offboard(self):
        """Offboard computer object with the offboard config.

        Returns:

        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Update the JSS record according to the implementation of the user.
        user_actions.update_offboard_config(self)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Push the offboard config to the JSS to offboard the computer.
        self._jss_server.push_xml_str(self._offboard_config, self._computer.jss_id)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Get the new managed status and make sure it's false
        post_managed_status = self._jss_server.get_managed_status(self._computer.jss_id)
        self.logger.debug("post_managed_status: {}".format(post_managed_status))
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Make sure managed status is false. If not, exit.
        if post_managed_status != 'false':
            msg = "Managed status was not false after offboarding. Aborting."
            self.logger.error(msg)
            self.restart()
            raise SystemError(msg)

    def _slack_handler(self, slack_data):
        """Handles all Slack related items: sends Slack message and starts Slackify daemon.

        Args:
            slack_data (dict): Slack configuration.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Notify slack channel that offboard tool has finished.
        if slack_data['slack_enabled'] == "True":
            message = slack_data['default_message']
            message = user_actions.update_slack_message(self, message)
            self.send_slack_message(message)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Start Slackify daemon
        if slack_data['slackify_daemon_enabled'] == 'True':
            self.logger.debug("Starting Slackify reminder daemon.")
            self.start_slackify_reminder_dameon()

    def _jss_doc_handler(self, computer, jss_server, doc_settings):
        """Creates a document from the predefined setup in JssDoc and prints the document
        to the default printer if print_doc is True.

        Args:
            computer (Computer): Stores information about the computer. Only used for JSS ID and "incorrect fields".
            jss_server (JssServer): JSS server to query.
            print_doc (bool): Prints document to default printer if True.

        Returns:

        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Prepare document for printing
        doc = JssDoc(jss_server, computer)
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Create html document from settings in JssDoc.
        doc.create_html()
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Convert HTML document into a PDF document.
        doc.html_to_pdf()
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # If document printing is enabled, print to the default printer.
        # if print_doc:
        #     doc.print_pdf_to_default()
        if doc_settings['print'].lower() == "true":
            doc.print_pdf_to_default()

    def send_slack_message(self, message):
        """Sends a Slack message to a specified channel and Slack url.

        Args:
            message (str): Message to be sent to the Slack channel.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Get the current IP
        current_ip = socket.gethostbyname(socket.gethostname())
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set up the Slack bot
        slack_bot = IWS(self._slack_data['slack_url'], bot_name=current_ip, channel=self._slack_data['slack_channel'])
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Send the Slack message
        slack_bot.send_message(message)

    def start_slackify_reminder_dameon(self):
        """Start the Slackify reminder daemon.

        Notes:
            The Slackify reminder daemon can't be imported into this script and run directly. In order for it to
            run as it's own detached process after this script finishes, it must be run through a subprocess call.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set up and run the command.
        cmd = ['/usr/bin/python', '-m', 'blade_runner.slack.slackify_reminder_daemon']
        subprocess.Popen(cmd, stderr=subprocess.STDOUT)

    def refocus(self):
        """Set the focus to the Python application.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set up the command
        select_window = ['/usr/bin/osascript', '-e',
                         'tell application "Finder" to set frontmost of process "Python" to true']
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        try:  # set focus to the main view.
            subprocess.check_output(select_window)
        except subprocess.CalledProcessError:
            self.logger.debug("Setting frontmost of process Python to true failed.")

    def open_config(self, config_id):
        """Opens the specified configuration file in its default application.

        Args:
            config_id (str): Denotes which path to use.

        Returns:
            void
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Determine the path to the configuration file.
        if config_id == "slack":
            path = "slack_configs/slack.plist"
        elif config_id == "offboard":
            path = "offboard_configs/"
        elif config_id == "verify":
            path = "verify_params_configs/verify_params.plist"
        elif config_id == "search":
            path = "search_params_configs/search_params.plist"
        elif config_id == "config":
            path = ""
        elif config_id == "print":
            path = "print_config/print.plist"
        elif config_id == "python_bin":
            path = "python_bin_config/python_bin.plist"
        elif config_id == "jamf_pro":
            path = "jamf_pro_configs/jamf_pro.plist"
        else:
            self.logger.warn("No configuration ID matches \"{}\"".format(config_id))
            raise SystemError("No configuration ID matches \"{}\"".format(config_id))
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Open the file in its default application
        subprocess.check_output(["open", os.path.join(self._config_dir, path)])

    def cat_readme(self):
        """Outputs the text of README.md.

        Returns:
            Text of README.md.
        """
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Set the path to README.md.
        readme = os.path.join(self.app_root_dir, "README.md")
        # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
        # Read README.md contents and return them as a single string.
        with open(readme, "r") as f:
            return f.read()


def main():
    # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
    # Ensure script is being run as root.
    if os.geteuid() != 0:
        raise SystemExit("Blade Runner must be run as root.")
    # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
    # Set up logging values.
    fmt = '%(asctime)s %(process)d: %(levelname)8s: %(name)s.%(funcName)s: %(message)s'
    script_name = os.path.splitext(os.path.basename(__file__))[0]
    log_dir = os.path.join(os.path.expanduser("~"), "Library/Logs/Blade Runner")
    filepath = os.path.join(log_dir, script_name + ".log")

    # Create directory for log if it DNE.
    try:
        os.mkdir(log_dir)
    except OSError as e:
        if e.errno != 17:
            raise e

    # Ensure that the owner is the logged in user.
    subprocess.check_output(['chown', '-R', os.getlogin(), log_dir])

    # Set up and get logger.
    logging.basicConfig(level=logging.DEBUG, format=fmt, filemode='a', filename=filepath)
    logger = logging.getLogger(script_name)
    # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
    # Create main root window.
    root = tk.Tk()
    root.withdraw()
    # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
    # Set up parent folder of config.
    abs_file_path = os.path.abspath(__file__)
    blade_runner_dir = abs_file_path
    for i in range(3):
        blade_runner_dir = os.path.dirname(blade_runner_dir)
    # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
    # Read from jss config plist and set up the Jamf Pro server
    jss_server_plist = os.path.join(blade_runner_dir, "config/jamf_pro_configs/jamf_pro.plist")
    jss_server_data = plistlib.readPlist(jss_server_plist)
    jss_server = JssServer(**jss_server_data)
    logger.debug(jss_server._jss_url)

    # Read from Slack config plist to set up Slack notifications
    slack_plist = os.path.join(blade_runner_dir, "config/slack_configs/slack.plist")
    slack_data = plistlib.readPlist(slack_plist)

    # Read from verify params plist to set up verification parameters
    verify_config = os.path.join(blade_runner_dir, "config/verify_params_configs/verify_params.plist")
    verify_params = plistlib.readPlist(verify_config)

    # Read from search params plist to set up search parameters.
    search_params_config = os.path.join(blade_runner_dir, "config/search_params_configs/search_params.plist")
    search_params = plistlib.readPlist(search_params_config)

    # Read from print config to enable or disable printing.
    print_config = os.path.join(blade_runner_dir, "config/print_config/print.plist")
    print_settings = plistlib.readPlist(print_config)
    # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
    # Run the application
    app = MainController(root, jss_server, slack_data, verify_params, search_params, print_settings)
    app.run()
    logger.info("Blade Runner exiting.")


if __name__ == "__main__":
    main()



#!/usr/bin/python # -*- coding: utf-8 -*- ################################################################################ # Copyright (c) 2019 University of Utah Student Computing Labs. # All Rights Reserved. # # Author: <NAME> # Creation Date: Oct 2018 # Last Updated: March 2019 # # Permission to use, copy, modify, and distribute this software and # its documentation for any purpose and without fee is hereby granted, # provided that the above copyright notice appears in all copies and # that both that copyright notice and this permission notice appear # in supporting documentation, and that the name of The University # of Utah not be used in advertising or publicity pertaining to # distribution of the software without specific, written prior # permission. This software is supplied as is without expressed or # implied warranties of any kind. ################################################################################ # TODO Interface with a Trello board and dynamically create lists for the DEP # TODO BUG: If spaces are entered in the asset/barcode inputs the program quits. Blade-Runner's main controller. Initialize the main controller.          Args:             root: The root Tk window.             server (JssServer): The JSS server to connect to.             slack_data (dict): Slack configuration data.             verify_params (dict): Verification parameters configuration data.             search_params (dict): Search parameters configuration data. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Set report callback so that a Tk window will appear with the exception message when an exception occurs. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # JSS server # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Stores information about the computer # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Used to exit out of a function if the user cancels. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Set views # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Set controllers # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Set whether or not a JSSDoc will be printed to a printer. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Set configuration data # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Save Verify/SearchParams original input # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Set path to config directory that contains the configuration files. Displays a message box with the accompanying exception message whenver an exception occurs.          Args:             exc: Exception received.             value: Value of exception.             traceback: Traceback from exception.          Returns:             void # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Grab the last value in the value tuple. This is normally the message. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Display a message box containing the exception's message. Start Blade-Runner.          Returns:             void # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Create the main view. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Set it to the middle of the screen. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Refocus on the window. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Make this window the main loop. Runs secure_erase.sh, which runs secure_erase_internals, in a new terminal window.          Returns:             void Terminates Blade-Runner.          Returns:             void # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Destroy the main view and the root window. Restart Blade-Runner without quiting the entire program.          Returns:             void # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Create a new Computer object. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Create a new VerifyParams object. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Create a new SearchParams object. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> Populates the main view's combobox with available configuration files.          Returns:             void # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Get the XML config files from the config directory # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Fill the main view's combobox with the XML files. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Set the initial choice to the first XML file in the list. Gets the offboard XML configuration files from a given directory.          Args:             dir (str): Path to config directory.          Returns:             list: XML configuration files. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Get a list of the files in the config directory. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Get all XML files in the directory. Save the offboard config as an XML string.          Args:             offboard_config (str): File name, not the file path.          Returns:             void # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Parse XML file into an XML tree. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Get the root of the tree. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Create offboard xml string and store it. Determines the search window sequence based on the input type and previous search params.          Args:             input_type (str): Data identifier.          Returns:             void # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Process the input type and handle the JSS search. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # For every search parameter enabled # If search parameter hasn't been searched yet. # If user hasn't canceled operation and no match was found # Process the input type and handle the JSS search. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # All search params have been searched and Blade-Runner has finished processing the computer. # Restart Blade-Runner. Handles search events.          Args:             input_type (str): Data identifier.          Returns:             void # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Open the search view for the input type. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # If the user canceled the search view, proceed will be false, Blade-Runner will restart, and function returns. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Since the computer object has been filled with user input data for the input type, search the JSS. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # If the search resulted in a match... # Set match # Open view  so user can verify inputted data against JSS data. # If user didn't cancel operation... # Get managed status of computer # If managed status is false, re-enroll computer to set managed status to true. This enables the # JSS record to be modified # Open a stall window, enroll the computer, and close the stall window. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Otherwise, if all input types have been searched... # Open view  so user can verify inputted data. # If user didn't cancel operation... # Open a stall window, enroll the computer, and close the stall window. # Since JSS ID has now been created, retrieve it. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Otherwise, return. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Push computer's identity fields to the JSS. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Offboard computer # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Send slack message and start slackify daemon according to slack data config. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Create and print JssDoc according to settings. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Display success message box. Dynamically search the JSS with the Computer attribute that matches the input type.          Args:             computer (Computer): Stores information about the computer.             input_type (str): Data identifier.          Returns:             void # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Use the computer object to set the search parameter. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Search the JSS. Open the search view. Saves user input in computer object.          Args:             input_type (str): Data identifier.          Returns:             void # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # If serial number, save serial number to computer object. This allows the serial to be displayed before # submitting it for a search. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Open the search controller/view. Inputs given to the view will be saved in the computer object. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Wait for search view to close. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # If the user canceled the operation, proceed will be false. Opens a VerifyView window.          Returns:             void # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Wait for entry view window to close. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # After entry view window has been closed through the "submit" button, proceed will be True, and the barcode, # asset, and name fields of the computer object will be updated. Opens a DualVerifyView window.          Returns:             void # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Open DualVerifyWindow/Controller # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Wait for entry view window to close. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Store exit status of the view. Offboard computer object with the offboard config.          Returns: # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Update the JSS record according to the implementation of the user. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Push the offboard config to the JSS to offboard the computer. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Get the new managed status and make sure it's false # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Make sure managed status is false. If not, exit. Handles all Slack related items: sends Slack message and starts Slackify daemon.          Args:             slack_data (dict): Slack configuration.          Returns:             void # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Notify slack channel that offboard tool has finished. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Start Slackify daemon Creates a document from the predefined setup in JssDoc and prints the document         to the default printer if print_doc is True.          Args:             computer (Computer): Stores information about the computer. Only used for JSS ID and "incorrect fields".             jss_server (JssServer): JSS server to query.             print_doc (bool): Prints document to default printer if True.          Returns: # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Prepare document for printing # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Create html document from settings in JssDoc. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Convert HTML document into a PDF document. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # If document printing is enabled, print to the default printer. # if print_doc: #     doc.print_pdf_to_default() Sends a Slack message to a specified channel and Slack url.          Args:             message (str): Message to be sent to the Slack channel.          Returns:             void # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Get the current IP # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Set up the Slack bot # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Send the Slack message Start the Slackify reminder daemon.          Notes:             The Slackify reminder daemon can't be imported into this script and run directly. In order for it to             run as it's own detached process after this script finishes, it must be run through a subprocess call.          Returns:             void # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Set up and run the command. Set the focus to the Python application.          Returns:             void # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Set up the command # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # set focus to the main view. Opens the specified configuration file in its default application.          Args:             config_id (str): Denotes which path to use.          Returns:             void # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Determine the path to the configuration file. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Open the file in its default application Outputs the text of README.md.          Returns:             Text of README.md. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Set the path to README.md. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Read README.md contents and return them as a single string. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Ensure script is being run as root. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Set up logging values. # Create directory for log if it DNE. # Ensure that the owner is the logged in user. # Set up and get logger. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Create main root window. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Set up parent folder of config. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Read from jss config plist and set up the Jamf Pro server # Read from Slack config plist to set up Slack notifications # Read from verify params plist to set up verification parameters # Read from search params plist to set up search parameters. # Read from print config to enable or disable printing. # <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> # Run the application