示例#1
0
    def __init__(self):

        self.ptyexec_cmd = os.path.join(settings.BinDirectory, 'ptyexec')
        self.installer_cmd = '/usr/sbin/installer'
        self.softwareupdate_cmd = '/usr/sbin/softwareupdate'

        self.plist = PlistInterface()
示例#2
0
    def __init__(self):

        self.ptyexec_cmd = os.path.join(settings.BinDirectory, "ptyexec")
        self.installer_cmd = "/usr/sbin/installer"
        self.softwareupdate_cmd = "/usr/sbin/softwareupdate"

        self.plist = PlistInterface()
示例#3
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'))
示例#4
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')
        )
示例#5
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()
示例#6
0
class PkgInstaller:
    """ A class to install Mac OS X packages using '/usr/sbin/installer'.
    Helps with making the calls and parsing the output to get the results.
    """

    def __init__(self):

        self.ptyexec_cmd = os.path.join(settings.BinDirectory, "ptyexec")
        self.installer_cmd = "/usr/sbin/installer"
        self.softwareupdate_cmd = "/usr/sbin/softwareupdate"

        self.plist = PlistInterface()

    # Uses installer tool to install pkgs
    def installer(self, pkg, proc_niceness):

        installer_cmd = [
            "nice",
            "-n",
            CpuPriority.niceness_to_string(proc_niceness),
            self.installer_cmd,
            "-pkg",
            "%s" % pkg,
            "-target",
            "/",
        ]

        proc = subprocess.Popen(installer_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        raw_output, _stderr = proc.communicate()

        unknown_output = []
        success = "false"
        error = ""

        for output in raw_output.splitlines():

            logger.debug(output)

            # All known output begins with 'installer' so remove it if present.
            if output.find("installer:") == 0:
                output = output.partition(":")[2].strip()

            # Known successful output:
            # 'The upgrade was successful.'
            # 'The install was successful.'
            if "successful" in output:
                success = "true"
                error = ""
                break

            elif "Package name is" in output:
                continue

            # Similar output:
            # Installing at base path
            # Upgrading at base path
            elif "at base path" in output:
                continue

            else:
                # Assuming failure.
                unknown_output.append(output)
                error = ""

        if len(unknown_output) != 0:
            error = ". ".join([output for output in unknown_output])

        return success, error

    # Old code on how to use softwareupdate to install updates.
    def softwareupdate(self, update_name, proc_niceness):

        #  Need to wrap call to /usr/sbin/softwareupdate with a utility
        # that makes softwareupdate think it is connected to a tty-like
        # device so its output is unbuffered so we can get progress info
        # '-v' (verbose) option is available on OSX > 10.5

        cmd = [
            "nice",
            "-n",
            CpuPriority.niceness_to_string(proc_niceness),
            self.ptyexec_cmd,
            self.softwareupdate_cmd,
            "-v",
            "-i",
            update_name,
        ]
        logger.debug("Running softwareupdate: " + str(cmd))

        success = "false"
        error = ""

        if not os.path.exists(self.ptyexec_cmd):
            raise PtyExecMissingException(settings.BinPath)

        try:
            job = launchd.Job(cmd)
            job.start()
        except launchd.LaunchdJobException as e:
            error_message = "Error with launchd job (%s): %s" % (cmd, str(e))
            logger.error(error_message)
            logger.critical("Skipping softwareupdate run.")

            return "false", error_message

        while True:

            output = job.stdout.readline()

            if not output:
                if job.returncode() is not None:
                    break
                else:
                    # no data, but we're still running
                    # sleep a bit before checking for more output
                    time.sleep(2)
                    continue

            # Checking output to verify results.
            output = output.decode("UTF-8").strip()
            if output.startswith("Installed "):
                # 10.6 / 10.7 / 10.8 Successful install of package name.
                success = "true"
                error = ""
                break

            elif output.startswith("Done with"):
                success = "true"
                error = ""
                break

            #            elif output.startswith('Done '):
            #                # 10.5. Successful install of package name.
            #                install_successful = True

            elif "No such update" in output:
                # 10.8 When a package cannot be found.
                success = "false"
                error = "Update not found."
                break

            elif output.startswith("Error "):
                # 10.8 Error statement
                # Note: Checking for updates doesn't display the
                # 'Error' string when connection is down.
                if "Internet connection appears to be offline" in output:
                    error = "Could not download files."
                else:
                    error = output

                    success = "false"

                break

            elif "restart immediately" in output:
                # Ignore if output is indicating a restart. Exact line:
                # "You have installed one or more updates that requires that
                # you restart your computer.  Please restart immediately."
                continue

            elif output.startswith("Package failed"):
                success = "false"
                error = output
                logger.debug(error)
                break

            elif (
                output == ""
                or output.startswith("Progress")
                or output.startswith("Done")
                or output.startswith("Running package")
                or output.startswith("Copyright")
                or output.startswith("Software Update Tool")
                or output.startswith("Downloading")
                or output.startswith("Moving items into place")
                or output.startswith("Writing package receipts")
                or output.startswith("Removing old files")
                or output.startswith("Registering updated components")
                or output.startswith("Waiting for other installations")
                or output.startswith("Writing files")
                or output.startswith("Cleaning up")
                or output.startswith("Registering updated applications")
                or output.startswith("About")
                or output.startswith("Less than a minute")
            ):
                # Output to ignore
                continue

            elif (
                output.startswith("Checking packages")
                or output.startswith("Installing")
                or output.startswith("Optimizing system for installed software")
                or output.startswith("Waiting to install")
                or output.startswith("Validating packages")
                or output.startswith("Finding available software")
                or output.startswith("Downloaded")
            ):
                # Output to display
                logger.debug("softwareupdate: " + output)

            else:
                success = "false"
                error = "softwareupdate (unknown): " + output
                logger.debug(error)

        #        return_code = job.returncode()
        #        if return_code == 0:
        #            # get SoftwareUpdate's LastResultCode
        #            su_path = '/Library/Preferences/com.apple.SoftwareUpdate.plist'
        #            su_prefs = plist.convert_and_read_plist(su_path)
        #            last_result_code = su_prefs['LastResultCode'] or 0
        #            if last_result_code > 2:
        #                return_code = last_result_code

        logger.debug("Done with softwareupdate.")
        return success, error

    def _make_dir(self, dir_path):
        try:
            os.makedirs(dir_path)
        except OSError as ose:
            # Avoid throwing an error if path already exists
            if ose.errno != errno.EEXIST:
                logger.error("Failed to create directory: " + dir_path)
                logger.exception(ose)
                raise

    def _move_pkgs(self, install_data, app_plist_data):
        """ Move all pkgs in src to dest. """

        try:
            product_key = app_plist_data["productKey"]

            src = os.path.join(settings.UpdatesDirectory, install_data.id)
            dest = os.path.join("/Library/Updates", product_key)

            if not os.path.exists(dest):
                self._make_dir(dest)
                time.sleep(3)

            for _file in os.listdir(src):
                if _file.endswith(".pkg"):

                    su_pkg_path = os.path.join(dest, _file)
                    if os.path.exists(su_pkg_path):
                        os.remove(su_pkg_path)
                        logger.debug("Removed existing pkg from /Library/Updates: %s " % su_pkg_path)

                    src_pkg = os.path.join(src, _file)
                    shutil.move(src_pkg, dest)
                    logger.debug("Moved " + _file + " to: " + dest)

        except Exception as e:
            logger.error("Failed moving pkgs to /Library/Updates.")
            logger.exception(e)
            raise

    def _get_app_plist_data(self, install_data):
        app_plist_data = self.plist.get_app_dict_from_plist(
            os.path.join(settings.TempDirectory, "updates.plist"), install_data.name
        )

        return app_plist_data

    def _get_softwareupdate_name(self, app_plist_data):
        """
        Construct the name softwareupdate expects for installation out of the
        plist ignore key and the version with a dash in between.
        """
        try:
            ignore_key = app_plist_data["ignoreKey"]
            version = app_plist_data["version"]

            return "-".join([ignore_key, version])

        except Exception as e:
            logger.error("Failed constructing softwareupdate name argument.")
            logger.exception(e)
            raise Exception(e)

    def install(self, install_data):
        success = "false"
        error = "Failed to install: " + install_data.name

        try:
            app_plist_data = self._get_app_plist_data(install_data)

            self._move_pkgs(install_data, app_plist_data)

            update_name = self._get_softwareupdate_name(app_plist_data)
            success, error = self.softwareupdate(update_name, install_data.proc_niceness)

        except Exception as e:
            logger.error("Failed to install pkg: " + install_data.name)
            logger.exception(e)

        return success, error

    def _remove_productkey_dir(self, app_plist_data):
        try:
            product_key = app_plist_data["productKey"]
            product_key_dir = os.path.join("/Library/Updates/", product_key)

            if os.path.exists(product_key_dir):
                logger.debug("%s exists. Attempting to remove it." % product_key_dir)
                shutil.rmtree(product_key_dir)
                logger.debug("Removed: " + product_key_dir)

            return True

        except Exception as e:
            logger.error("Failed to remove directory")
            logger.exception(e)
            raise Exception(e)

        return False

    def complete_softwareupdate(self, install_data):
        """
        Removes the product key directory if it exists, and lets
        softwareupdate download and install on its own.
        """

        success = "false"
        error = "Failed to install: " + install_data.name

        try:
            app_plist_data = self._get_app_plist_data(install_data)

            for i in range(1, 3):
                remove_success = self._remove_productkey_dir(app_plist_data)

                if remove_success:
                    break

                time.sleep(5 * i)

            update_name = self._get_softwareupdate_name(app_plist_data)
            success, error = self.softwareupdate(update_name, install_data.proc_niceness)

        except Exception as e:
            logger.error("Failed to download/install pkg with softwareupdate: %s" % install_data.name)
            logger.exception(e)

        return success, error
示例#7
0
class UpdatesCatalog:
    def __init__(self, catalogs_dir, catalog_filename):
        self.catalogs_dir = catalogs_dir
        self.catalog_filename = catalog_filename
        self.plist = PlistInterface()

    def create_updates_catalog(self, update_apps):
        """
        Creates a catalog file which houses all the catalog information
        provided by the mac catalogs; Only for applications that need updates.
        """

        catalogs = glob.glob(os.path.join(self.catalogs_dir, '*.sucatalog'))

        try:
            key_and_app = {self.plist.get_product_key_from_app_dict(app): app
                           for app in update_apps}

            data = {}

            for catalog in catalogs:

                if len(key_and_app) == 0:
                    break

                su = self.plist.read_plist(catalog)

                for key in su.get('Products', {}).keys():

                    if len(key_and_app) == 0:
                        break

                    if key in key_and_app:
                        app_name = key_and_app[key]['name']

                        data[app_name] = su['Products'][key]

                        data[app_name]['PostDate'] = \
                            data[app_name]['PostDate'].strftime(
                                settings.DATE_FORMAT
                            )

                        reboot = self._get_reboot_required_from_data(
                            key_and_app[key]['restartRequired']
                        )
                        data[app_name]['reboot'] = reboot

                        # Stop checking for this key
                        del key_and_app[key]

            self.write_data(data)

        except Exception as e:
            logger.error("Could not create updates catalog.")
            logger.exception(e)

    def _get_reboot_required_from_data(self, reboot_string):
        reboot = 'false'

        if reboot_string == 'YES':
            reboot = 'true'

        return reboot

    def _get_file_json(self, file_path):
        with open(file_path, 'r') as _file:
            return json.load(_file)

    def get_all_data(self, ):
        """
        Returns a dictionary with all information in the updates catalog file.
        """
        return self._get_file_json(self.catalog_filename)

    def _write_file_json(self, json_data, file_path):
        with open(file_path, 'w') as _file:
            json.dump(json_data, _file, indent=4)

    def write_data(self, json_data):
        self._write_file_json(json_data, self.catalog_filename)

    def get_app_data(self, app_name):
        """
        Returns a dictionary of all the information specific to the product
        key.
        """
        return self.get_all_data().get(app_name, {})

    def get_reboot_required(self, app_name):
        reboot = 'no'

        try:
            app_data = self.get_app_data(app_name)
            reboot = app_data.get('reboot', 'no').lower()

        except Exception as e:
            logger.error(
                "Failed to get reboot required for {0}".format(app_name)
            )
            logger.exception(e)

        return reboot

    def get_release_date(self, app_name):
        """ Gets the release date of the application specified by app_name """

        release_date = ''

        try:
            app_data = self.get_app_data(app_name)
            release_date = str(app_data.get('PostDate', ''))

        except Exception as e:
            logger.error("Failed to get release date for {0}".format(app_name))
            logger.exception(e)

        return release_date

    #def get_dependencies(self, product_key):
    #    """
    #    Returns a list of dictionaries for all of the apps' dependencies with
    #    format:
    #        [
    #            {
    #              'name' : ... ,
    #              'version' : ... ,
    #              'app_id' : ...
    #            }
    #        ]
    #    """
    #
    #    dependencies = []
    #
    #    try:
    #        key_data = get_product_key_data(product_key)
    #
    #        for pkg in key_data.get('Packages', []):
    #            pass
    #
    #            dependencies.append()
    #
    #    except Exception as e:
    #        logger.error('Could not find dependencies for: {0}'.format(product_key))
    #        logger.exception(e)
    #
    #    return dependencies

    def get_file_data(self, app_name):
        """
        Returns urls and other info corresponding to the app with given
        app_name.
        """

        file_data = []

        try:
            app_data = self.get_app_data(app_name)

            for pkg in app_data.get('Packages', []):

                pkg_data = {}

                uri = pkg.get('URL', '')
                name = uri.split('/')[-1]

                pkg_data['file_name'] = name
                pkg_data['file_uri'] = uri
                pkg_data['file_size'] = pkg.get('Size', '')
                pkg_data['file_hash'] = ''

                file_data.append(pkg_data)

        except Exception as e:

            logger.error('Could not get file_data/release date.')
            logger.exception(e)

        return file_data
示例#8
0
 def __init__(self, catalogs_dir, catalog_filename):
     self.catalogs_dir = catalogs_dir
     self.catalog_filename = catalog_filename
     self.plist = PlistInterface()
示例#9
0
class PkgInstaller():
    """ A class to install Mac OS X packages using '/usr/sbin/installer'.
    Helps with making the calls and parsing the output to get the results.
    """
    def __init__(self):

        self.ptyexec_cmd = os.path.join(settings.BinDirectory, 'ptyexec')
        self.installer_cmd = '/usr/sbin/installer'
        self.softwareupdate_cmd = '/usr/sbin/softwareupdate'

        self.plist = PlistInterface()

    # Uses installer tool to install pkgs
    def installer(self, pkg, proc_niceness):

        installer_cmd = [
            'nice', '-n',
            CpuPriority.niceness_to_string(proc_niceness), self.installer_cmd,
            '-pkg',
            '%s' % pkg, '-target', '/'
        ]

        proc = subprocess.Popen(installer_cmd,
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE)
        raw_output, _stderr = proc.communicate()

        unknown_output = []
        success = 'false'
        error = ''

        for output in raw_output.splitlines():

            logger.debug(output)

            # All known output begins with 'installer' so remove it if present.
            if output.find('installer:') == 0:
                output = output.partition(':')[2].strip()

            # Known successful output:
            # 'The upgrade was successful.'
            # 'The install was successful.'
            if 'successful' in output:
                success = 'true'
                error = ''
                break

            elif 'Package name is' in output:
                continue

            # Similar output:
            # Installing at base path
            # Upgrading at base path
            elif 'at base path' in output:
                continue

            else:
                # Assuming failure.
                unknown_output.append(output)
                error = ''

        if len(unknown_output) != 0:
            error = ". ".join([output for output in unknown_output])

        return success, error

    # Old code on how to use softwareupdate to install updates.
    def softwareupdate(self, update_name, proc_niceness):

        #  Need to wrap call to /usr/sbin/softwareupdate with a utility
        # that makes softwareupdate think it is connected to a tty-like
        # device so its output is unbuffered so we can get progress info
        # '-v' (verbose) option is available on OSX > 10.5

        cmd = [
            'nice', '-n',
            CpuPriority.niceness_to_string(proc_niceness), self.ptyexec_cmd,
            self.softwareupdate_cmd, '-v', '-i', update_name
        ]
        logger.debug("Running softwareupdate: " + str(cmd))

        success = 'false'
        error = ''

        if not os.path.exists(self.ptyexec_cmd):
            raise PtyExecMissingException(settings.BinPath)

        try:
            job = launchd.Job(cmd)
            job.start()
        except launchd.LaunchdJobException as e:
            error_message = 'Error with launchd job (%s): %s' % (cmd, str(e))
            logger.error(error_message)
            logger.critical('Skipping softwareupdate run.')

            return 'false', error_message

        while True:

            output = job.stdout.readline()

            if not output:
                if job.returncode() is not None:
                    break
                else:
                    # no data, but we're still running
                    # sleep a bit before checking for more output
                    time.sleep(2)
                    continue

            # Checking output to verify results.
            output = output.decode('UTF-8').strip()
            if output.startswith('Installed '):
                # 10.6 / 10.7 / 10.8 Successful install of package name.
                success = 'true'
                error = ''
                break

            elif output.startswith('Done with'):
                success = 'true'
                error = ''
                break

            #            elif output.startswith('Done '):
            #                # 10.5. Successful install of package name.
            #                install_successful = True

            elif 'No such update' in output:
                # 10.8 When a package cannot be found.
                success = 'false'
                error = "Update not found."
                break

            elif output.startswith('Error '):
                # 10.8 Error statement
                # Note: Checking for updates doesn't display the
                # 'Error' string when connection is down.
                if "Internet connection appears to be offline" in output:
                    error = "Could not download files."
                else:
                    error = output

                    success = 'false'

                break

            elif 'restart immediately' in output:
                # Ignore if output is indicating a restart. Exact line:
                # "You have installed one or more updates that requires that
                # you restart your computer.  Please restart immediately."
                continue

            elif output.startswith('Package failed'):
                success = 'false'
                error = output
                logger.debug(error)
                break

            elif (output == '' or output.startswith('Progress')
                  or output.startswith('Done')
                  or output.startswith('Running package')
                  or output.startswith('Copyright')
                  or output.startswith('Software Update Tool')
                  or output.startswith('Downloading')
                  or output.startswith('Moving items into place')
                  or output.startswith('Writing package receipts')
                  or output.startswith('Removing old files')
                  or output.startswith('Registering updated components')
                  or output.startswith('Waiting for other installations')
                  or output.startswith('Writing files')
                  or output.startswith('Cleaning up')
                  or output.startswith('Registering updated applications')
                  or output.startswith('About')
                  or output.startswith('Less than a minute')):
                # Output to ignore
                continue

            elif (output.startswith('Checking packages')
                  or output.startswith('Installing') or
                  output.startswith('Optimizing system for installed software')
                  or output.startswith('Waiting to install')
                  or output.startswith('Validating packages')
                  or output.startswith('Finding available software')
                  or output.startswith('Downloaded')):
                # Output to display
                logger.debug('softwareupdate: ' + output)

            else:
                success = 'false'
                error = "softwareupdate (unknown): " + output
                logger.debug(error)


#        return_code = job.returncode()
#        if return_code == 0:
#            # get SoftwareUpdate's LastResultCode
#            su_path = '/Library/Preferences/com.apple.SoftwareUpdate.plist'
#            su_prefs = plist.convert_and_read_plist(su_path)
#            last_result_code = su_prefs['LastResultCode'] or 0
#            if last_result_code > 2:
#                return_code = last_result_code

        logger.debug("Done with softwareupdate.")
        return success, error

    def _make_dir(self, dir_path):
        try:
            os.makedirs(dir_path)
        except OSError as ose:
            # Avoid throwing an error if path already exists
            if ose.errno != errno.EEXIST:
                logger.error("Failed to create directory: " + dir_path)
                logger.exception(ose)
                raise

    def _move_pkgs(self, install_data, app_plist_data):
        """ Move all pkgs in src to dest. """

        try:
            product_key = app_plist_data["productKey"]

            src = os.path.join(settings.UpdatesDirectory, install_data.id)
            dest = os.path.join('/Library/Updates', product_key)

            if not os.path.exists(dest):
                self._make_dir(dest)
                time.sleep(3)

            for _file in os.listdir(src):
                if _file.endswith(".pkg"):

                    su_pkg_path = os.path.join(dest, _file)
                    if os.path.exists(su_pkg_path):
                        os.remove(su_pkg_path)
                        logger.debug(
                            "Removed existing pkg from /Library/Updates: %s " %
                            su_pkg_path)

                    src_pkg = os.path.join(src, _file)
                    shutil.move(src_pkg, dest)
                    logger.debug("Moved " + _file + " to: " + dest)

        except Exception as e:
            logger.error("Failed moving pkgs to /Library/Updates.")
            logger.exception(e)
            raise

    def _get_app_plist_data(self, install_data):
        app_plist_data = self.plist.get_app_dict_from_plist(
            os.path.join(settings.TempDirectory, "updates.plist"),
            install_data.name)

        return app_plist_data

    def _get_softwareupdate_name(self, app_plist_data):
        """
        Construct the name softwareupdate expects for installation out of the
        plist ignore key and the version with a dash in between.
        """
        try:
            ignore_key = app_plist_data["ignoreKey"]
            version = app_plist_data["version"]

            return "-".join([ignore_key, version])

        except Exception as e:
            logger.error("Failed constructing softwareupdate name argument.")
            logger.exception(e)
            raise Exception(e)

    def install(self, install_data):
        success = 'false'
        error = "Failed to install: " + install_data.name

        try:
            app_plist_data = self._get_app_plist_data(install_data)

            self._move_pkgs(install_data, app_plist_data)

            update_name = self._get_softwareupdate_name(app_plist_data)
            success, error = self.softwareupdate(update_name,
                                                 install_data.proc_niceness)

        except Exception as e:
            logger.error("Failed to install pkg: " + install_data.name)
            logger.exception(e)

        return success, error

    def _remove_productkey_dir(self, app_plist_data):
        try:
            product_key = app_plist_data["productKey"]
            product_key_dir = os.path.join("/Library/Updates/", product_key)

            if os.path.exists(product_key_dir):
                logger.debug("%s exists. Attempting to remove it." %
                             product_key_dir)
                shutil.rmtree(product_key_dir)
                logger.debug("Removed: " + product_key_dir)

            return True

        except Exception as e:
            logger.error("Failed to remove directory")
            logger.exception(e)
            raise Exception(e)

        return False

    def complete_softwareupdate(self, install_data):
        """
        Removes the product key directory if it exists, and lets
        softwareupdate download and install on its own.
        """

        success = 'false'
        error = "Failed to install: " + install_data.name

        try:
            app_plist_data = self._get_app_plist_data(install_data)

            for i in range(1, 3):
                remove_success = self._remove_productkey_dir(app_plist_data)

                if remove_success:
                    break

                time.sleep(5 * i)

            update_name = self._get_softwareupdate_name(app_plist_data)
            success, error = self.softwareupdate(update_name,
                                                 install_data.proc_niceness)

        except Exception as e:
            logger.error(
                "Failed to download/install pkg with softwareupdate: %s" %
                install_data.name)
            logger.exception(e)

        return success, error
示例#10
0
class UpdatesCatalog:
    def __init__(self, catalogs_dir, catalog_filename):
        self.catalogs_dir = catalogs_dir
        self.catalog_filename = catalog_filename
        self.plist = PlistInterface()

    def create_updates_catalog(self, update_apps):
        """
        Creates a catalog file which houses all the catalog information
        provided by the mac catalogs; Only for applications that need updates.
        """

        catalogs = glob.glob(os.path.join(self.catalogs_dir, '*.sucatalog'))

        try:
            key_and_app = {
                self.plist.get_product_key_from_app_dict(app): app
                for app in update_apps
            }

            data = {}

            for catalog in catalogs:

                if len(key_and_app) == 0:
                    break

                su = self.plist.read_plist(catalog)

                for key in su.get('Products', {}).keys():

                    if len(key_and_app) == 0:
                        break

                    if key in key_and_app:
                        app_name = key_and_app[key]['name']

                        data[app_name] = su['Products'][key]

                        data[app_name]['PostDate'] = \
                            data[app_name]['PostDate'].strftime(
                                settings.DATE_FORMAT
                            )

                        reboot = self._get_reboot_required_from_data(
                            key_and_app[key]['restartRequired'])
                        data[app_name]['reboot'] = reboot

                        # Stop checking for this key
                        del key_and_app[key]

            self.write_data(data)

        except Exception as e:
            logger.error("Could not create updates catalog.")
            logger.exception(e)

    def _get_reboot_required_from_data(self, reboot_string):
        reboot = 'false'

        if reboot_string == 'YES':
            reboot = 'true'

        return reboot

    def _get_file_json(self, file_path):
        with open(file_path, 'r') as _file:
            return json.load(_file)

    def get_all_data(self, ):
        """
        Returns a dictionary with all information in the updates catalog file.
        """
        return self._get_file_json(self.catalog_filename)

    def _write_file_json(self, json_data, file_path):
        with open(file_path, 'w') as _file:
            json.dump(json_data, _file, indent=4)

    def write_data(self, json_data):
        self._write_file_json(json_data, self.catalog_filename)

    def get_app_data(self, app_name):
        """
        Returns a dictionary of all the information specific to the product
        key.
        """
        return self.get_all_data().get(app_name, {})

    def get_reboot_required(self, app_name):
        reboot = 'no'

        try:
            app_data = self.get_app_data(app_name)
            reboot = app_data.get('reboot', 'no').lower()

        except Exception as e:
            logger.error(
                "Failed to get reboot required for {0}".format(app_name))
            logger.exception(e)

        return reboot

    def get_release_date(self, app_name):
        """ Gets the release date of the application specified by app_name """

        release_date = ''

        try:
            app_data = self.get_app_data(app_name)
            release_date = str(app_data.get('PostDate', ''))

        except Exception as e:
            logger.error("Failed to get release date for {0}".format(app_name))
            logger.exception(e)

        return release_date

    #def get_dependencies(self, product_key):
    #    """
    #    Returns a list of dictionaries for all of the apps' dependencies with
    #    format:
    #        [
    #            {
    #              'name' : ... ,
    #              'version' : ... ,
    #              'app_id' : ...
    #            }
    #        ]
    #    """
    #
    #    dependencies = []
    #
    #    try:
    #        key_data = get_product_key_data(product_key)
    #
    #        for pkg in key_data.get('Packages', []):
    #            pass
    #
    #            dependencies.append()
    #
    #    except Exception as e:
    #        logger.error('Could not find dependencies for: {0}'.format(product_key))
    #        logger.exception(e)
    #
    #    return dependencies

    def get_file_data(self, app_name):
        """
        Returns urls and other info corresponding to the app with given
        app_name.
        """

        file_data = []

        try:
            app_data = self.get_app_data(app_name)

            for pkg in app_data.get('Packages', []):

                pkg_data = {}

                uri = pkg.get('URL', '')
                name = uri.split('/')[-1]

                pkg_data['file_name'] = name
                pkg_data['file_uri'] = uri
                pkg_data['file_size'] = pkg.get('Size', '')
                pkg_data['file_hash'] = ''

                file_data.append(pkg_data)

        except Exception as e:

            logger.error('Could not get file_data/release date.')
            logger.exception(e)

        return file_data
示例#11
0
 def __init__(self, catalogs_dir, catalog_filename):
     self.catalogs_dir = catalogs_dir
     self.catalog_filename = catalog_filename
     self.plist = PlistInterface()
示例#12
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()