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()
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()
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 __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') )
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()
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
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
def __init__(self, catalogs_dir, catalog_filename): self.catalogs_dir = catalogs_dir self.catalog_filename = catalog_filename self.plist = PlistInterface()
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
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
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()