예제 #1
0
    def __init__(self):
        # Initialize mac table stuff.
        #self._macsqlite = SqliteMac()
        #self._macsqlite.recreate_update_data_table()
        self.utilcmds = utilcmds.UtilCmds()

        self._catalog_directory = \
            os.path.join(settings.AgentDirectory, 'catalogs')

        self._updates_plist = \
            os.path.join(settings.TempDirectory, 'updates.plist')

        if not os.path.isdir(self._catalog_directory):
            os.mkdir(self._catalog_directory)

        self.pkg_installer = PkgInstaller()
        self.dmg_installer = DmgInstaller()
        self.plist = PlistInterface()
        self.updates_catalog = UpdatesCatalog(
            self._catalog_directory,
            os.path.join(settings.TempDirectory, 'updates_catalog.json'))
예제 #2
0
    def __init__(self):
        # Initialize mac table stuff.
        #self._macsqlite = SqliteMac()
        #self._macsqlite.recreate_update_data_table()
        self.utilcmds = utilcmds.UtilCmds()

        self._catalog_directory = \
            os.path.join(settings.AgentDirectory, 'catalogs')

        self._updates_plist = \
            os.path.join(settings.TempDirectory, 'updates.plist')

        if not os.path.isdir(self._catalog_directory):
            os.mkdir(self._catalog_directory)

        self.pkg_installer = PkgInstaller()
        self.dmg_installer = DmgInstaller()
        self.plist = PlistInterface()
        self.updates_catalog = UpdatesCatalog(
            self._catalog_directory,
            os.path.join(settings.TempDirectory, 'updates_catalog.json')
        )
예제 #3
0
class MacOpHandler():

    def __init__(self):
        # Initialize mac table stuff.
        #self._macsqlite = SqliteMac()
        #self._macsqlite.recreate_update_data_table()
        self.utilcmds = utilcmds.UtilCmds()

        self._catalog_directory = \
            os.path.join(settings.AgentDirectory, 'catalogs')

        self._updates_plist = \
            os.path.join(settings.TempDirectory, 'updates.plist')

        if not os.path.isdir(self._catalog_directory):
            os.mkdir(self._catalog_directory)

        self.pkg_installer = PkgInstaller()
        self.dmg_installer = DmgInstaller()
        self.plist = PlistInterface()
        self.updates_catalog = UpdatesCatalog(
            self._catalog_directory,
            os.path.join(settings.TempDirectory, 'updates_catalog.json')
        )

    def get_installed_applications(self):
        """Parses the output from
        the 'system_profiler -xml SPApplicationsDataType' command.
        """

        logger.info("Getting installed applications.")

        installed_apps = []

        try:

            cmd = ['/usr/sbin/system_profiler',
                   '-xml',
                   'SPApplicationsDataType']

            output, _error = self.utilcmds.run_command(cmd)

            app_data = self.plist.read_plist_string(output)

            for apps in app_data:

                for app in apps['_items']:

                    app_inst = None

                    try:

                        # Skip app, no name.
                        if not '_name' in app:
                            continue

                        app_name = app['_name']
                        app_version = app.get('version', '')
                        app_date = app.get('lastModified', '')

                        app_inst = CreateApplication.create(
                            app_name,
                            app_version,
                            '',  # description
                            [],  # file_data
                            [],  # dependencies
                            '',  # support_url
                            '',  # vendor_severity
                            '',  # file_size
                            '',  # vendor_id,
                            '',  # vendor_name
                            app_date,  # install_date
                            None,  # release_date
                            True,  # installed
                            "",  # repo
                            "no",  # reboot_required
                            "yes"  # TODO: check if app is uninstallable
                        )

                    except Exception as e:
                        logger.error("Error verifying installed application."
                                     "Skipping.")
                        logger.exception(e)

                    if app_inst:
                        installed_apps.append(app_inst)

            installed_apps.extend(
                ThirdPartyManager.get_supported_installs()
            )

        except Exception as e:
            logger.error("Error verifying installed applications.")
            logger.exception(e)

        logger.info('Done.')

        return installed_apps

    def _get_installed_app(self, name, installed_apps):
        for app in installed_apps:
            if app.name == name:
                return app

        return CreateApplication.null_application()

    def _get_installed_apps(self, name_list):
        installed_apps = self.get_installed_applications()

        app_list = []
        found = 0
        total = len(name_list)

        for app in installed_apps:
            if found >= total:
                break

            if app.name in name_list:
                app_list.append(app)
                found += 1

        return app_list

    def get_installed_updates(self):
        """
        Parses the /Library/Receipts/InstallHistory.plist file looking for
        'Software Update' as the process name.
        """

        logger.info("Getting installed updates.")

        install_history = '/Library/Receipts/InstallHistory.plist'
        installed_updates = []

        try:

            if os.path.exists(install_history):

                app_data = self.plist.read_plist(install_history)

                for app in app_data:

                    app_inst = None

                    try:

                        if app.get('processName') == 'Software Update':

                            if not 'displayName' in app:
                                continue

                            app_name = app['displayName']

                            app_name = app.get('displayName', '')
                            app_version = app.get('displayVersion', '')
                            app_date = app.get('date', '')

                            app_inst = CreateApplication.create(
                                app_name,
                                app_version,
                                '',  # description
                                [],  # file_data
                                [],  # dependencies
                                '',  # support_url
                                '',  # vendor_severity
                                '',  # file_size
                                # vendor_id
                                hashlib.sha256(
                                    app_name.encode('utf-8') + app_version)
                                .hexdigest(),
                                'Apple',  # vendor_name
                                app_date,  # install_date
                                None,  # release_date
                                True,  # installed
                                "",  # repo
                                "no",  # reboot_required
                                "yes"  # TODO: check if app is uninstallable
                            )

                    except Exception as e:

                        logger.error("Error verifying installed update."
                                     "Skipping.")
                        logger.exception(e)

                    if app_inst:
                        installed_updates.append(app_inst)

        except Exception as e:
            logger.error("Error verifying installed updates.")
            logger.exception(e)

        logger.info('Done.')

        return installed_updates

    @staticmethod
    def _strip_body_tags(html):
        s = BodyHTMLStripper()
        s.feed(html)
        return s.get_data()

    def _get_softwareupdate_data(self):
        cmd = ['/usr/sbin/softwareupdate', '-l', '-f', self._updates_plist]

        # Little trick to hide the command's output from terminal.
        with open(os.devnull, 'w') as dev_null:
            subprocess.call(cmd, stdout=dev_null, stderr=dev_null)

        cmd = ['/bin/cat', self._updates_plist]
        output, _ = self.utilcmds.run_command(cmd)

        return output

    def create_apps_from_plist_dicts(self, app_dicts):
        applications = []

        for app_dict in app_dicts:
            try:
                # Skip app, no name.
                if not 'name' in app_dict:
                    continue

                app_name = app_dict['name']

                release_date = self._get_package_release_date(app_name)
                file_data = self._get_file_data(app_name)

                dependencies = []

                app_inst = CreateApplication.create(
                    app_name,
                    app_dict['version'],

                    # Just in case there's HTML, strip it out
                    MacOpHandler._strip_body_tags(app_dict['description'])
                    # and get rid of newlines.
                    .replace('\n', ''),

                    file_data,  # file_data
                    dependencies,
                    '',  # support_url
                    '',  # vendor_severity
                    '',  # file_size
                    app_dict['productKey'],  # vendor_id
                    'Apple',  # vendor_name
                    None,  # install_date
                    release_date,  # release_date
                    False,  # installed
                    '',  # repo
                    app_dict['restartRequired'].lower(),  # reboot_required
                    'yes'  # TODO: check if app is uninstallable
                )

                applications.append(app_inst)
                #self._add_update_data(
                #    app_inst.name,
                #    app_dict['restartRequired']
                #)

            except Exception as e:
                logger.error(
                    "Failed to create an app instance for: {0}"
                    .format(app_dict['name'])
                )
                logger.exception(e)

        return applications

    def get_available_updates(self):
        """
        Uses the softwareupdate OS X app to see what updates are available.
        @return: Nothing
        """

        logger.info("Getting available updates.")

        try:

            logger.debug("Downloading catalogs.")
            self._download_catalogs()
            logger.debug("Done downloading catalogs.")

            logger.debug("Getting softwareupdate data.")
            avail_data = self._get_softwareupdate_data()
            logger.debug("Done getting softwareupdate data.")

            logger.debug("Crunching available updates data.")
            plist_app_dicts = \
                self.plist.get_plist_app_dicts_from_string(avail_data)

            self.updates_catalog.create_updates_catalog(plist_app_dicts)

            available_updates = \
                self.create_apps_from_plist_dicts(plist_app_dicts)

            logger.info('Done getting available updates.')

            return available_updates

        except Exception as e:
            logger.error("Could not get available updates.")
            logger.exception(e)

            return []

    def _get_list_difference(self, list_a, list_b):
        """
        Returns the difference of of list_a and list_b.
        (aka) What's in list_a that isn't in list_b
        """
        set_a = set(list_a)
        set_b = set(list_b)

        return set_a.difference(set_b)

    def _get_apps_to_delete(self, old_install_list, new_install_list):

        difference = self._get_list_difference(
            old_install_list, new_install_list
        )

        apps_to_delete = []
        for app in difference:
            root = {}
            root['name'] = app.name
            root['version'] = app.version

            apps_to_delete.append(root)

        return apps_to_delete

    def _get_apps_to_add(self, old_install_list, new_install_list):

        difference = self._get_list_difference(
            new_install_list, old_install_list
        )

        apps_to_add = []
        for app in difference:
            apps_to_add.append(app.to_dict())

        return apps_to_add

    def _get_apps_to_add_and_delete(self, old_install_list,
                                    new_install_list=None):

        if not new_install_list:
            new_install_list = self.get_installed_applications()

        apps_to_delete = self._get_apps_to_delete(
            old_install_list, new_install_list
        )

        apps_to_add = self._get_apps_to_add(
            old_install_list, new_install_list
        )

        return apps_to_add, apps_to_delete

    def _get_app_encoding(self, name, install_list):
        updated_app = self._get_installed_app(name, install_list)
        app_encoding = updated_app.to_dict()

        return app_encoding

    def install_update(self, install_data, update_dir=None):
        """
        Install OS X updates.

        Returns:

            Installation result

        """

        # Use to get the apps to be removed on the server side
        old_install_list = self.get_installed_applications()

        success = 'false'
        error = RvError.UpdatesNotFound
        restart = 'false'
        app_encoding = CreateApplication.null_application().to_dict()
        apps_to_delete = []
        apps_to_add = []

        if not update_dir:
            update_dir = settings.UpdatesDirectory

        #update_data = self._macsqlite.get_update_data(
        #    install_data.name
        #)

        if install_data.downloaded:
            success, error = self.pkg_installer.install(install_data)

            if success != 'true':
                logger.debug(
                    "Failed to install update {0}. success:{1}, error:{2}"
                    .format(install_data.name, success, error)
                )
                # Let the OS take care of downloading and installing.
                success, error = \
                    self.pkg_installer.complete_softwareupdate(install_data)

        else:
            logger.debug(("Downloaded = False for: {0} calling "
                         "complete_softwareupdate.")
                         .format(install_data.name))

            success, error = \
                self.pkg_installer.complete_softwareupdate(install_data)

        if success == 'true':
            #restart = update_data.get(UpdateDataColumn.NeedsRestart, 'false')
            restart = self._get_reboot_required(install_data.name)

            new_install_list = self.get_installed_applications()

            app_encoding = self._get_app_encoding(
                install_data.name, new_install_list
            )

            apps_to_add, apps_to_delete = self._get_apps_to_add_and_delete(
                old_install_list, new_install_list
            )

        return InstallResult(
            success,
            error,
            restart,
            app_encoding,
            apps_to_delete,
            apps_to_add
        )

    def _install_third_party_pkg(self, pkgs, proc_niceness):
        success = 'false'
        error = 'Could not install pkgs.'

        if pkgs:
            # TODO(urgent): what to do with multiple pkgs?
            for pkg in pkgs:
                success, error = self.pkg_installer.installer(pkg)

        return success, error

    def _get_app_names_from_paths(self, app_bundle_paths):
        app_bundles = [app.split('/')[-1] for app in app_bundle_paths]

        app_names = [app_bundle.split('.app')[0] for app_bundle in app_bundles]

        return app_names

    def _install_third_party_dmgs(self, dmgs, proc_niceness):
        success = 'false'
        error = 'Could not install from dmg.'
        app_names = []

        for dmg in dmgs:
            try:
                dmg_mount = os.path.join('/Volumes', dmg.split('/')[-1])

                if not self.dmg_installer.mount_dmg(dmg, dmg_mount):
                    raise Exception(
                        "Failed to get mount point for: {0}".format(dmg)
                    )

                logger.debug("Custom App Mount: {0}".format(dmg_mount))

                pkgs = glob.glob(os.path.join(dmg_mount, '*.pkg'))
                dmg_app_bundles = glob.glob(os.path.join(dmg_mount, '*.app'))

                if pkgs:
                    success, error = self._install_third_party_pkg(
                        pkgs, proc_niceness
                    )

                elif dmg_app_bundles:
                    app_names.extend(
                        self._get_app_names_from_paths(dmg_app_bundles)
                    )

                    for app in dmg_app_bundles:
                        success, error = \
                            self.dmg_installer.app_bundle_install(app)

            except Exception as e:
                logger.error("Failed installing dmg: {0}".format(dmg))
                logger.exception(e)

                success = 'false'

                # TODO: if one dmg fails on an update, should the rest also be
                # stopped from installing?

                break

            finally:
                if dmg_mount:
                    self.dmg_installer.eject_dmg(dmg_mount)

        return success, error, app_names

    def _separate_important_info(self, info):
        """
        Parses info which looks like:
        """

        info = info.split('\n')
        info = [x.split('=') for x in info]

        # Cleaning up both the key and the value
        info = {ele[0].strip(): ele[1].strip() for ele in info
                if len(ele) == 2}

        no_quotes = r'"(.*)"'

        info_dict = {}

        try:
            app_name = info['kMDItemDisplayName']
            app_version = info['kMDItemVersion']
            app_size = info['kMDItemFSSize']
        except KeyError as ke:
            return {}

        no_quote_name = re.search(no_quotes, app_name)
        if no_quote_name:
            app_name = no_quote_name.group(1)

        no_quote_version = re.search(no_quotes, app_version)
        if no_quote_version:
            app_version = no_quote_version.group(1)

        no_quote_size = re.search(no_quotes, app_size)
        if no_quote_size:
            app_size = no_quote_size.group(1)

        info_dict['name'] = app_name
        info_dict['version'] = app_version
        info_dict['size'] = app_size

        return info_dict

    def _get_app_bundle_info(self, app_bundle_path):
        try:
            info_dict = {}

            info_plist_path = \
                os.path.join(app_bundle_path, 'Contents', 'Info.plist')

            plist_dict = self.plist.read_plist(info_plist_path)

            info_dict['name'] = plist_dict['CFBundleName']
            info_dict['version'] = plist_dict['CFBundleShortVersionString']

            #cmd = ['du', '-s', app_bundle_path]
            #output, err = self.utilcmds.run_command(cmd)

            #try:
            #    size = output.split('\t')[0]
            #except Exception as e:
            #    size = 0

            #info_dict['size'] = size

            return info_dict
        except Exception:
            return {}

    def _create_app_from_bundle_info(self, app_bundle_names):
        app_instances = []

        for app_name in app_bundle_names:
            ## TODO: path for installing app bundles is hardcoded for now
            #app_path = os.path.join('/Applications', app_name + '.app')
            #cmd = ['mdls', app_path]

            #output, result = self.utilcmds.run_command(cmd)

            ## TODO(urgent): don't use the mdls module, it also runs on an OS X
            ## timer it seems. Meaning the applications meta data can't be read
            ## before a certain period of time.

            #for i in range(5):
            #    info_dict = self._separate_important_info(output)

            #    if info_dict:
            #        # We're good, we got the info. Break out and let this do
            #        # its thing.
            #        break

            #    # Give the OS some time to gather the data
            #    logger.debug("Sleeping for 5.")
            #    time.sleep(5)

            #if not info_dict:
            #    logger.error(
            #        "Could not get metadata for application: {0}"
            #        .format(app_name)
            #    )
            #    continue

            # TODO(urgent): stop hardcoding the path
            app_bundle_path = os.path.join('/Applications', app_name + '.app')

            info_dict = self._get_app_bundle_info(app_bundle_path)

            if not info_dict:
                logger.exception(
                    "Failed to gather metadata for: {0}".format(app_name)
                )

                continue

            app_inst = CreateApplication.create(
                info_dict['name'],
                info_dict['version'],
                '',  # description
                [],  # file_data
                [],  # dependencies
                '',  # support_url
                '',  # vendor_severity
                '',  # file_size
                '',  # vendor_id,
                '',  # vendor_name
                int(time.time()),  # install_date
                None,  # release_date
                True,  # installed
                "",  # repo
                "no",  # reboot_required
                "yes"  # TODO: check if app is uninstallable
            )

            app_instances.append(app_inst)

        return app_instances

    def install_supported_apps(self, install_data, update_dir=None):

        old_install_list = self.get_installed_applications()

        success = 'false'
        error = 'Failed to install application.'
        restart = 'false'
        #app_encoding = []
        apps_to_delete = []
        apps_to_add = []

        if not install_data.downloaded:
            error = 'Failed to download packages.'

            return InstallResult(
                success,
                error,
                restart,
                "{}",
                apps_to_delete,
                apps_to_add
            )

        if not update_dir:
            update_dir = settings.UpdatesDirectory

        try:
            pkgs = glob.glob(
                os.path.join(update_dir, "%s/*.pkg" % install_data.id)
            )
            dmgs = glob.glob(
                os.path.join(update_dir, "%s/*.dmg" % install_data.id)
            )

            if pkgs:
                success, error = self._install_third_party_pkg(pkgs)

                if success == 'true':
                    #app_encoding = self._get_app_encoding(install_data.name)

                    apps_to_add, apps_to_delete = \
                        self._get_apps_to_add_and_delete(old_install_list)

            elif dmgs:
                success, error, app_names = self._install_third_party_dmgs(
                    dmgs, install_data.proc_niceness
                )

                if success == 'true':

                    apps_to_add, apps_to_delete = \
                        self._get_apps_to_add_and_delete(old_install_list)

                    # OSX may not index these newly installed applications
                    # in time, therefore the information gathering has to be
                    # done manually.
                    if app_names:
                        newly_installed = \
                            self._create_app_from_bundle_info(app_names)

                        apps_to_add.extend(
                            [app.to_dict() for app in newly_installed]
                        )

                    # TODO(urgent): figure out how to get apps_to_delete
                    # for dmgs with app bundles

        except Exception as e:
            logger.error("Failed to install: {0}".format(install_data.name))
            logger.exception(e)

        return InstallResult(
            success,
            error,
            restart,
            "{}",
            apps_to_delete,
            apps_to_add
        )

    def install_custom_apps(self, install_data, update_dir=None):
        return self.install_supported_apps(install_data, update_dir)

    def install_agent_update(
        self, install_data, operation_id, update_dir=None
    ):
        success = 'false'
        error = ''

        if update_dir is None:
            update_dir = settings.UpdatesDirectory

        if install_data.downloaded:
            update_dir = os.path.join(update_dir, install_data.id)
            dmgs = glob.glob(os.path.join(update_dir, "*.dmg"))

            path_of_update = [dmg for dmg in dmgs
                              if re.search(r'vfagent.*\.dmg', dmg.lower())]

            if path_of_update:
                path_of_update = path_of_update[0]

                agent_updater = updater.Updater()

                extra_cmds = ['--operationid', operation_id,
                              '--appid', install_data.id]

                success, error = agent_updater.update(
                    path_of_update, extra_cmds
                )

            else:
                logger.error(
                    "Could not find update in: {0}".format(update_dir)
                )
                error = 'Could not find update.'

        else:

            logger.debug("{0} was not downloaded. Returning false."
                         .format(install_data.name))

            error = "Update not downloaded."

        return InstallResult(
            success,
            error,
            'false',
            "{}",
            [],
            []
        )

    def _known_special_order(self, packages):
        """Orders a list of packages.

        Some packages need to be installed in a certain order.
        This method helps with that by ordering known packages.

        Args:
            - packages: List of packages to order.

        Returns:

            - A list of ordered packages.
        """

        # First implementation of this method is a hack. Only checks for
        # 'repair' because of known issues when trying to update Safari with
        # its two packages. The 'repair' package has to be installed first.

        ordered_packages = []

        for pkg in packages:

            if 'repair' in pkg.lower():

                ordered_packages.insert(0, pkg)

            else:

                ordered_packages.append(pkg)

        return ordered_packages

    def uninstall_application(self, uninstall_data):
        """ Uninstalls applications in the /Applications directory. """

        success = 'false'
        error = 'Failed to uninstall application.'
        restart = 'false'
        #data = []

        uninstallable_app_bundles = os.listdir('/Applications')

        app_bundle = uninstall_data.name + ".app"

        if app_bundle not in uninstallable_app_bundles:
            error = ("{0} is not an app bundle. Currently only app bundles are"
                     " uninstallable.".format(uninstall_data.name))

        else:

            uninstaller = Uninstaller()

            success, error = uninstaller.remove(uninstall_data.name)

        logger.info('Done attempting to uninstall app.')

        return UninstallResult(success, error, restart)

    def _add_update_data(self, name, restart):

        if restart == 'YES':
            restart = 'true'
        else:
            restart = 'false'

        self._macsqlite.add_update_data(name, restart)

    def _to_timestamp(self, d):
        """
        Helper method to convert datetime to a UTC timestamp.
        @param d: datetime.datetime object
        @return: a UTC/Unix timestamp string
        """
        return time.mktime(d.timetuple())

    def _download_catalogs(self):

        catalog_urls = [
            'http://swscan.apple.com/content/catalogs/index.sucatalog',
            'http://swscan.apple.com/content/catalogs/index-1.sucatalog',
            'http://swscan.apple.com/content/catalogs/others/index-leopard.merged-1.sucatalog',
            'http://swscan.apple.com/content/catalogs/others/index-leopard-snowleopard.merged-1.sucatalog',
            'http://swscan.apple.com/content/catalogs/others/index-lion-snowleopard-leopard.merged-1.sucatalog',
            'http://swscan.apple.com/content/catalogs/others/index-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog',
            'http://swscan.apple.com/content/catalogs/others/index-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog'
        ]

        for url in catalog_urls:
            filename = url.split('/')[-1]  # with file extension.
            try:
                urllib.urlretrieve(
                    url, os.path.join(self._catalog_directory, filename)
                )
            except Exception as e:
                logger.error("Could not download sucatalog %s." % filename)
                logger.exception(e)

    def _get_package_release_date(self, app_name):
        """ Checks the updates catalog (JSON) to get release date for app. """

        return self.updates_catalog.get_release_date(app_name)

    def _get_file_data(self, app_name):
        """ Checks the updates catalog (JSON) to get file_data for app. """

        return self.updates_catalog.get_file_data(app_name)

    def _get_reboot_required(self, app_name):
        return self.updates_catalog.get_reboot_required(app_name)

    def recreate_tables(self):
        pass  # self._macsqlite.recreate_update_data_table()
예제 #4
0
class MacOpHandler():
    def __init__(self):
        # Initialize mac table stuff.
        #self._macsqlite = SqliteMac()
        #self._macsqlite.recreate_update_data_table()
        self.utilcmds = utilcmds.UtilCmds()

        self._catalog_directory = \
            os.path.join(settings.AgentDirectory, 'catalogs')

        self._updates_plist = \
            os.path.join(settings.TempDirectory, 'updates.plist')

        if not os.path.isdir(self._catalog_directory):
            os.mkdir(self._catalog_directory)

        self.pkg_installer = PkgInstaller()
        self.dmg_installer = DmgInstaller()
        self.plist = PlistInterface()
        self.updates_catalog = UpdatesCatalog(
            self._catalog_directory,
            os.path.join(settings.TempDirectory, 'updates_catalog.json'))

    def get_installed_applications(self):
        """Parses the output from
        the 'system_profiler -xml SPApplicationsDataType' command.
        """

        logger.info("Getting installed applications.")

        installed_apps = []

        try:

            cmd = [
                '/usr/sbin/system_profiler', '-xml', 'SPApplicationsDataType'
            ]

            output, _error = self.utilcmds.run_command(cmd)

            app_data = self.plist.read_plist_string(output)

            for apps in app_data:

                for app in apps['_items']:

                    app_inst = None

                    try:

                        # Skip app, no name.
                        if not '_name' in app:
                            continue

                        app_name = app['_name']
                        app_version = app.get('version', '')
                        app_date = app.get('lastModified', '')

                        app_inst = CreateApplication.create(
                            app_name,
                            app_version,
                            '',  # description
                            [],  # file_data
                            [],  # dependencies
                            '',  # support_url
                            '',  # vendor_severity
                            '',  # file_size
                            '',  # vendor_id,
                            '',  # vendor_name
                            app_date,  # install_date
                            None,  # release_date
                            True,  # installed
                            "",  # repo
                            "no",  # reboot_required
                            "yes"  # TODO: check if app is uninstallable
                        )

                    except Exception as e:
                        logger.error("Error verifying installed application."
                                     "Skipping.")
                        logger.exception(e)

                    if app_inst:
                        installed_apps.append(app_inst)

            installed_apps.extend(ThirdPartyManager.get_supported_installs())

        except Exception as e:
            logger.error("Error verifying installed applications.")
            logger.exception(e)

        logger.info('Done.')

        return installed_apps

    def _get_installed_app(self, name, installed_apps):
        for app in installed_apps:
            if app.name == name:
                return app

        return CreateApplication.null_application()

    def _get_installed_apps(self, name_list):
        installed_apps = self.get_installed_applications()

        app_list = []
        found = 0
        total = len(name_list)

        for app in installed_apps:
            if found >= total:
                break

            if app.name in name_list:
                app_list.append(app)
                found += 1

        return app_list

    def get_installed_updates(self):
        """
        Parses the /Library/Receipts/InstallHistory.plist file looking for
        'Software Update' as the process name.
        """

        logger.info("Getting installed updates.")

        install_history = '/Library/Receipts/InstallHistory.plist'
        installed_updates = []

        try:

            if os.path.exists(install_history):

                app_data = self.plist.read_plist(install_history)

                for app in app_data:

                    app_inst = None

                    try:

                        if app.get('processName') == 'Software Update':

                            if not 'displayName' in app:
                                continue

                            app_name = app['displayName']

                            app_name = app.get('displayName', '')
                            app_version = app.get('displayVersion', '')
                            app_date = app.get('date', '')

                            app_inst = CreateApplication.create(
                                app_name,
                                app_version,
                                '',  # description
                                [],  # file_data
                                [],  # dependencies
                                '',  # support_url
                                '',  # vendor_severity
                                '',  # file_size
                                # vendor_id
                                hashlib.sha256(
                                    app_name.encode('utf-8') +
                                    app_version).hexdigest(),
                                'Apple',  # vendor_name
                                app_date,  # install_date
                                None,  # release_date
                                True,  # installed
                                "",  # repo
                                "no",  # reboot_required
                                "yes"  # TODO: check if app is uninstallable
                            )

                    except Exception as e:

                        logger.error("Error verifying installed update."
                                     "Skipping.")
                        logger.exception(e)

                    if app_inst:
                        installed_updates.append(app_inst)

        except Exception as e:
            logger.error("Error verifying installed updates.")
            logger.exception(e)

        logger.info('Done.')

        return installed_updates

    @staticmethod
    def _strip_body_tags(html):
        s = BodyHTMLStripper()
        s.feed(html)
        return s.get_data()

    def _get_softwareupdate_data(self):
        cmd = ['/usr/sbin/softwareupdate', '-l', '-f', self._updates_plist]

        # Little trick to hide the command's output from terminal.
        with open(os.devnull, 'w') as dev_null:
            subprocess.call(cmd, stdout=dev_null, stderr=dev_null)

        cmd = ['/bin/cat', self._updates_plist]
        output, _ = self.utilcmds.run_command(cmd)

        return output

    def create_apps_from_plist_dicts(self, app_dicts):
        applications = []

        for app_dict in app_dicts:
            try:
                # Skip app, no name.
                if not 'name' in app_dict:
                    continue

                app_name = app_dict['name']

                release_date = self._get_package_release_date(app_name)
                file_data = self._get_file_data(app_name)

                dependencies = []

                app_inst = CreateApplication.create(
                    app_name,
                    app_dict['version'],

                    # Just in case there's HTML, strip it out
                    MacOpHandler._strip_body_tags(app_dict['description'])
                    # and get rid of newlines.
                    .replace('\n', ''),
                    file_data,  # file_data
                    dependencies,
                    '',  # support_url
                    '',  # vendor_severity
                    '',  # file_size
                    app_dict['productKey'],  # vendor_id
                    'Apple',  # vendor_name
                    None,  # install_date
                    release_date,  # release_date
                    False,  # installed
                    '',  # repo
                    app_dict['restartRequired'].lower(),  # reboot_required
                    'yes'  # TODO: check if app is uninstallable
                )

                applications.append(app_inst)
                #self._add_update_data(
                #    app_inst.name,
                #    app_dict['restartRequired']
                #)

            except Exception as e:
                logger.error(
                    "Failed to create an app instance for: {0}".format(
                        app_dict['name']))
                logger.exception(e)

        return applications

    def get_available_updates(self):
        """
        Uses the softwareupdate OS X app to see what updates are available.
        @return: Nothing
        """

        logger.info("Getting available updates.")

        try:

            logger.debug("Downloading catalogs.")
            self._download_catalogs()
            logger.debug("Done downloading catalogs.")

            logger.debug("Getting softwareupdate data.")
            avail_data = self._get_softwareupdate_data()
            logger.debug("Done getting softwareupdate data.")

            logger.debug("Crunching available updates data.")
            plist_app_dicts = \
                self.plist.get_plist_app_dicts_from_string(avail_data)

            self.updates_catalog.create_updates_catalog(plist_app_dicts)

            available_updates = \
                self.create_apps_from_plist_dicts(plist_app_dicts)

            logger.info('Done getting available updates.')

            return available_updates

        except Exception as e:
            logger.error("Could not get available updates.")
            logger.exception(e)

            return []

    def _get_list_difference(self, list_a, list_b):
        """
        Returns the difference of of list_a and list_b.
        (aka) What's in list_a that isn't in list_b
        """
        set_a = set(list_a)
        set_b = set(list_b)

        return set_a.difference(set_b)

    def _get_apps_to_delete(self, old_install_list, new_install_list):

        difference = self._get_list_difference(old_install_list,
                                               new_install_list)

        apps_to_delete = []
        for app in difference:
            root = {}
            root['name'] = app.name
            root['version'] = app.version

            apps_to_delete.append(root)

        return apps_to_delete

    def _get_apps_to_add(self, old_install_list, new_install_list):

        difference = self._get_list_difference(new_install_list,
                                               old_install_list)

        apps_to_add = []
        for app in difference:
            apps_to_add.append(app.to_dict())

        return apps_to_add

    def _get_apps_to_add_and_delete(self,
                                    old_install_list,
                                    new_install_list=None):

        if not new_install_list:
            new_install_list = self.get_installed_applications()

        apps_to_delete = self._get_apps_to_delete(old_install_list,
                                                  new_install_list)

        apps_to_add = self._get_apps_to_add(old_install_list, new_install_list)

        return apps_to_add, apps_to_delete

    def _get_app_encoding(self, name, install_list):
        updated_app = self._get_installed_app(name, install_list)
        app_encoding = updated_app.to_dict()

        return app_encoding

    def install_update(self, install_data, update_dir=None):
        """
        Install OS X updates.

        Returns:

            Installation result

        """

        # Use to get the apps to be removed on the server side
        old_install_list = self.get_installed_applications()

        success = 'false'
        error = RvError.UpdatesNotFound
        restart = 'false'
        app_encoding = CreateApplication.null_application().to_dict()
        apps_to_delete = []
        apps_to_add = []

        if not update_dir:
            update_dir = settings.UpdatesDirectory

        #update_data = self._macsqlite.get_update_data(
        #    install_data.name
        #)

        if install_data.downloaded:
            success, error = self.pkg_installer.install(install_data)

            if success != 'true':
                logger.debug(
                    "Failed to install update {0}. success:{1}, error:{2}".
                    format(install_data.name, success, error))
                # Let the OS take care of downloading and installing.
                success, error = \
                    self.pkg_installer.complete_softwareupdate(install_data)

        else:
            logger.debug(
                ("Downloaded = False for: {0} calling "
                 "complete_softwareupdate.").format(install_data.name))

            success, error = \
                self.pkg_installer.complete_softwareupdate(install_data)

        if success == 'true':
            #restart = update_data.get(UpdateDataColumn.NeedsRestart, 'false')
            restart = self._get_reboot_required(install_data.name)

            new_install_list = self.get_installed_applications()

            app_encoding = self._get_app_encoding(install_data.name,
                                                  new_install_list)

            apps_to_add, apps_to_delete = self._get_apps_to_add_and_delete(
                old_install_list, new_install_list)

        return InstallResult(success, error, restart, app_encoding,
                             apps_to_delete, apps_to_add)

    def _install_third_party_pkg(self, pkgs, proc_niceness):
        success = 'false'
        error = 'Could not install pkgs.'

        if pkgs:
            # TODO(urgent): what to do with multiple pkgs?
            for pkg in pkgs:
                success, error = self.pkg_installer.installer(pkg)

        return success, error

    def _get_app_names_from_paths(self, app_bundle_paths):
        app_bundles = [app.split('/')[-1] for app in app_bundle_paths]

        app_names = [app_bundle.split('.app')[0] for app_bundle in app_bundles]

        return app_names

    def _install_third_party_dmgs(self, dmgs, proc_niceness):
        success = 'false'
        error = 'Could not install from dmg.'
        app_names = []

        for dmg in dmgs:
            try:
                dmg_mount = os.path.join('/Volumes', dmg.split('/')[-1])

                if not self.dmg_installer.mount_dmg(dmg, dmg_mount):
                    raise Exception(
                        "Failed to get mount point for: {0}".format(dmg))

                logger.debug("Custom App Mount: {0}".format(dmg_mount))

                pkgs = glob.glob(os.path.join(dmg_mount, '*.pkg'))
                dmg_app_bundles = glob.glob(os.path.join(dmg_mount, '*.app'))

                if pkgs:
                    success, error = self._install_third_party_pkg(
                        pkgs, proc_niceness)

                elif dmg_app_bundles:
                    app_names.extend(
                        self._get_app_names_from_paths(dmg_app_bundles))

                    for app in dmg_app_bundles:
                        success, error = \
                            self.dmg_installer.app_bundle_install(app)

            except Exception as e:
                logger.error("Failed installing dmg: {0}".format(dmg))
                logger.exception(e)

                success = 'false'

                # TODO: if one dmg fails on an update, should the rest also be
                # stopped from installing?

                break

            finally:
                if dmg_mount:
                    self.dmg_installer.eject_dmg(dmg_mount)

        return success, error, app_names

    def _separate_important_info(self, info):
        """
        Parses info which looks like:
        """

        info = info.split('\n')
        info = [x.split('=') for x in info]

        # Cleaning up both the key and the value
        info = {
            ele[0].strip(): ele[1].strip()
            for ele in info if len(ele) == 2
        }

        no_quotes = r'"(.*)"'

        info_dict = {}

        try:
            app_name = info['kMDItemDisplayName']
            app_version = info['kMDItemVersion']
            app_size = info['kMDItemFSSize']
        except KeyError as ke:
            return {}

        no_quote_name = re.search(no_quotes, app_name)
        if no_quote_name:
            app_name = no_quote_name.group(1)

        no_quote_version = re.search(no_quotes, app_version)
        if no_quote_version:
            app_version = no_quote_version.group(1)

        no_quote_size = re.search(no_quotes, app_size)
        if no_quote_size:
            app_size = no_quote_size.group(1)

        info_dict['name'] = app_name
        info_dict['version'] = app_version
        info_dict['size'] = app_size

        return info_dict

    def _get_app_bundle_info(self, app_bundle_path):
        try:
            info_dict = {}

            info_plist_path = \
                os.path.join(app_bundle_path, 'Contents', 'Info.plist')

            plist_dict = self.plist.read_plist(info_plist_path)

            info_dict['name'] = plist_dict['CFBundleName']
            info_dict['version'] = plist_dict['CFBundleShortVersionString']

            #cmd = ['du', '-s', app_bundle_path]
            #output, err = self.utilcmds.run_command(cmd)

            #try:
            #    size = output.split('\t')[0]
            #except Exception as e:
            #    size = 0

            #info_dict['size'] = size

            return info_dict
        except Exception:
            return {}

    def _create_app_from_bundle_info(self, app_bundle_names):
        app_instances = []

        for app_name in app_bundle_names:
            ## TODO: path for installing app bundles is hardcoded for now
            #app_path = os.path.join('/Applications', app_name + '.app')
            #cmd = ['mdls', app_path]

            #output, result = self.utilcmds.run_command(cmd)

            ## TODO(urgent): don't use the mdls module, it also runs on an OS X
            ## timer it seems. Meaning the applications meta data can't be read
            ## before a certain period of time.

            #for i in range(5):
            #    info_dict = self._separate_important_info(output)

            #    if info_dict:
            #        # We're good, we got the info. Break out and let this do
            #        # its thing.
            #        break

            #    # Give the OS some time to gather the data
            #    logger.debug("Sleeping for 5.")
            #    time.sleep(5)

            #if not info_dict:
            #    logger.error(
            #        "Could not get metadata for application: {0}"
            #        .format(app_name)
            #    )
            #    continue

            # TODO(urgent): stop hardcoding the path
            app_bundle_path = os.path.join('/Applications', app_name + '.app')

            info_dict = self._get_app_bundle_info(app_bundle_path)

            if not info_dict:
                logger.exception(
                    "Failed to gather metadata for: {0}".format(app_name))

                continue

            app_inst = CreateApplication.create(
                info_dict['name'],
                info_dict['version'],
                '',  # description
                [],  # file_data
                [],  # dependencies
                '',  # support_url
                '',  # vendor_severity
                '',  # file_size
                '',  # vendor_id,
                '',  # vendor_name
                int(time.time()),  # install_date
                None,  # release_date
                True,  # installed
                "",  # repo
                "no",  # reboot_required
                "yes"  # TODO: check if app is uninstallable
            )

            app_instances.append(app_inst)

        return app_instances

    def install_supported_apps(self, install_data, update_dir=None):

        old_install_list = self.get_installed_applications()

        success = 'false'
        error = 'Failed to install application.'
        restart = 'false'
        #app_encoding = []
        apps_to_delete = []
        apps_to_add = []

        if not install_data.downloaded:
            error = 'Failed to download packages.'

            return InstallResult(success, error, restart, "{}", apps_to_delete,
                                 apps_to_add)

        if not update_dir:
            update_dir = settings.UpdatesDirectory

        try:
            pkgs = glob.glob(
                os.path.join(update_dir, "%s/*.pkg" % install_data.id))
            dmgs = glob.glob(
                os.path.join(update_dir, "%s/*.dmg" % install_data.id))

            if pkgs:
                success, error = self._install_third_party_pkg(pkgs)

                if success == 'true':
                    #app_encoding = self._get_app_encoding(install_data.name)

                    apps_to_add, apps_to_delete = \
                        self._get_apps_to_add_and_delete(old_install_list)

            elif dmgs:
                success, error, app_names = self._install_third_party_dmgs(
                    dmgs, install_data.proc_niceness)

                if success == 'true':

                    apps_to_add, apps_to_delete = \
                        self._get_apps_to_add_and_delete(old_install_list)

                    # OSX may not index these newly installed applications
                    # in time, therefore the information gathering has to be
                    # done manually.
                    if app_names:
                        newly_installed = \
                            self._create_app_from_bundle_info(app_names)

                        apps_to_add.extend(
                            [app.to_dict() for app in newly_installed])

                    # TODO(urgent): figure out how to get apps_to_delete
                    # for dmgs with app bundles

        except Exception as e:
            logger.error("Failed to install: {0}".format(install_data.name))
            logger.exception(e)

        return InstallResult(success, error, restart, "{}", apps_to_delete,
                             apps_to_add)

    def install_custom_apps(self, install_data, update_dir=None):
        return self.install_supported_apps(install_data, update_dir)

    def install_agent_update(self,
                             install_data,
                             operation_id,
                             update_dir=None):
        success = 'false'
        error = ''

        if update_dir is None:
            update_dir = settings.UpdatesDirectory

        if install_data.downloaded:
            update_dir = os.path.join(update_dir, install_data.id)
            dmgs = glob.glob(os.path.join(update_dir, "*.dmg"))

            path_of_update = [
                dmg for dmg in dmgs
                if re.search(r'vfagent.*\.dmg', dmg.lower())
            ]

            if path_of_update:
                path_of_update = path_of_update[0]

                agent_updater = updater.Updater()

                extra_cmds = [
                    '--operationid', operation_id, '--appid', install_data.id
                ]

                success, error = agent_updater.update(path_of_update,
                                                      extra_cmds)

            else:
                logger.error(
                    "Could not find update in: {0}".format(update_dir))
                error = 'Could not find update.'

        else:

            logger.debug("{0} was not downloaded. Returning false.".format(
                install_data.name))

            error = "Update not downloaded."

        return InstallResult(success, error, 'false', "{}", [], [])

    def _known_special_order(self, packages):
        """Orders a list of packages.

        Some packages need to be installed in a certain order.
        This method helps with that by ordering known packages.

        Args:
            - packages: List of packages to order.

        Returns:

            - A list of ordered packages.
        """

        # First implementation of this method is a hack. Only checks for
        # 'repair' because of known issues when trying to update Safari with
        # its two packages. The 'repair' package has to be installed first.

        ordered_packages = []

        for pkg in packages:

            if 'repair' in pkg.lower():

                ordered_packages.insert(0, pkg)

            else:

                ordered_packages.append(pkg)

        return ordered_packages

    def uninstall_application(self, uninstall_data):
        """ Uninstalls applications in the /Applications directory. """

        success = 'false'
        error = 'Failed to uninstall application.'
        restart = 'false'
        #data = []

        uninstallable_app_bundles = os.listdir('/Applications')

        app_bundle = uninstall_data.name + ".app"

        if app_bundle not in uninstallable_app_bundles:
            error = ("{0} is not an app bundle. Currently only app bundles are"
                     " uninstallable.".format(uninstall_data.name))

        else:

            uninstaller = Uninstaller()

            success, error = uninstaller.remove(uninstall_data.name)

        logger.info('Done attempting to uninstall app.')

        return UninstallResult(success, error, restart)

    def _add_update_data(self, name, restart):

        if restart == 'YES':
            restart = 'true'
        else:
            restart = 'false'

        self._macsqlite.add_update_data(name, restart)

    def _to_timestamp(self, d):
        """
        Helper method to convert datetime to a UTC timestamp.
        @param d: datetime.datetime object
        @return: a UTC/Unix timestamp string
        """
        return time.mktime(d.timetuple())

    def _download_catalogs(self):

        catalog_urls = [
            'http://swscan.apple.com/content/catalogs/index.sucatalog',
            'http://swscan.apple.com/content/catalogs/index-1.sucatalog',
            'http://swscan.apple.com/content/catalogs/others/index-leopard.merged-1.sucatalog',
            'http://swscan.apple.com/content/catalogs/others/index-leopard-snowleopard.merged-1.sucatalog',
            'http://swscan.apple.com/content/catalogs/others/index-lion-snowleopard-leopard.merged-1.sucatalog',
            'http://swscan.apple.com/content/catalogs/others/index-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog',
            'http://swscan.apple.com/content/catalogs/others/index-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog'
        ]

        for url in catalog_urls:
            filename = url.split('/')[-1]  # with file extension.
            try:
                urllib.urlretrieve(
                    url, os.path.join(self._catalog_directory, filename))
            except Exception as e:
                logger.error("Could not download sucatalog %s." % filename)
                logger.exception(e)

    def _get_package_release_date(self, app_name):
        """ Checks the updates catalog (JSON) to get release date for app. """

        return self.updates_catalog.get_release_date(app_name)

    def _get_file_data(self, app_name):
        """ Checks the updates catalog (JSON) to get file_data for app. """

        return self.updates_catalog.get_file_data(app_name)

    def _get_reboot_required(self, app_name):
        return self.updates_catalog.get_reboot_required(app_name)

    def recreate_tables(self):
        pass  # self._macsqlite.recreate_update_data_table()