mirror of
https://github.com/munki/munki.git
synced 2026-01-06 14:40:09 -06:00
Restructure appleupdates.py
This commit is contained in:
1
code/client/munkilib/appleupdates/__init__.py
Normal file
1
code/client/munkilib/appleupdates/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .core import *
|
||||
@@ -1,11 +1,5 @@
|
||||
#!/usr/bin/python
|
||||
# encoding: utf-8
|
||||
"""
|
||||
appleupdates.py
|
||||
|
||||
Utilities for dealing with Apple Software Update.
|
||||
|
||||
"""
|
||||
# Copyright 2009-2016 Greg Neagle.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -19,7 +13,12 @@ Utilities for dealing with Apple Software Update.
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
appleupdates.py
|
||||
|
||||
Utilities for dealing with Apple Software Update.
|
||||
|
||||
"""
|
||||
|
||||
import glob
|
||||
import gzip
|
||||
@@ -35,165 +34,52 @@ import urlparse
|
||||
# pylint: disable=E0611
|
||||
from Foundation import NSDate
|
||||
from Foundation import NSBundle
|
||||
from CoreFoundation import CFPreferencesAppValueIsForced
|
||||
from CoreFoundation import CFPreferencesCopyAppValue
|
||||
from CoreFoundation import CFPreferencesCopyKeyList
|
||||
from CoreFoundation import CFPreferencesCopyValue
|
||||
from CoreFoundation import CFPreferencesSetValue
|
||||
from CoreFoundation import CFPreferencesSynchronize
|
||||
from CoreFoundation import kCFPreferencesAnyUser
|
||||
#from CoreFoundation import kCFPreferencesCurrentUser
|
||||
from CoreFoundation import kCFPreferencesCurrentHost
|
||||
# pylint: enable=E0611
|
||||
|
||||
from . import catalogs
|
||||
from . import appledistutils
|
||||
from . import fetch
|
||||
from . import launchd
|
||||
from . import munkicommon
|
||||
from . import munkistatus
|
||||
from . import updatecheck
|
||||
from . import FoundationPlist
|
||||
from . import dist
|
||||
from . import su_prefs
|
||||
from . import sync
|
||||
|
||||
from ..updatecheck import catalogs
|
||||
|
||||
from .. import fetch
|
||||
from .. import launchd
|
||||
from .. import munkicommon
|
||||
from .. import munkistatus
|
||||
from .. import updatecheck
|
||||
from .. import FoundationPlist
|
||||
|
||||
|
||||
# Disable PyLint complaining about 'invalid' camelCase names
|
||||
# pylint: disable=C0103
|
||||
|
||||
|
||||
# Apple Software Update Catalog URLs.
|
||||
DEFAULT_CATALOG_URLS = {
|
||||
'10.6': ('http://swscan.apple.com/content/catalogs/others/'
|
||||
'index-leopard-snowleopard.merged-1.sucatalog'),
|
||||
'10.7': ('http://swscan.apple.com/content/catalogs/others/'
|
||||
'index-lion-snowleopard-leopard.merged-1.sucatalog'),
|
||||
'10.8': ('http://swscan.apple.com/content/catalogs/others/'
|
||||
'index-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog'),
|
||||
'10.9': ('https://swscan.apple.com/content/catalogs/others/'
|
||||
'index-10.9-mountainlion-lion-snowleopard-leopard.merged-1'
|
||||
'.sucatalog'),
|
||||
'10.10': ('https://swscan.apple.com/content/catalogs/others/'
|
||||
'index-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1'
|
||||
'.sucatalog'),
|
||||
'10.11': ('https://swscan.apple.com/content/catalogs/others/'
|
||||
'index-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard'
|
||||
'.merged-1.sucatalog'),
|
||||
'10.12': ('https://swscan.apple.com/content/catalogs/others/'
|
||||
'index-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard'
|
||||
'-leopard.merged-1.sucatalog')
|
||||
}
|
||||
|
||||
# Preference domain for Apple Software Update.
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN = 'com.apple.SoftwareUpdate'
|
||||
|
||||
# Apple's index of downloaded updates
|
||||
INDEX_PLIST = '/Library/Updates/index.plist'
|
||||
|
||||
# Path to the directory where local catalogs are stored, relative to
|
||||
# munkicommon.pref('ManagedInstallDir') + /swupd/mirror/.
|
||||
LOCAL_CATALOG_DIR_REL_PATH = 'content/catalogs/'
|
||||
|
||||
# The pristine, untouched, but potentially gzipped catalog.
|
||||
APPLE_DOWNLOAD_CATALOG_NAME = 'apple.sucatalog'
|
||||
|
||||
# The pristine, untouched, and extracted catalog.
|
||||
APPLE_EXTRACTED_CATALOG_NAME = 'apple_index.sucatalog'
|
||||
|
||||
# The catalog containing only applicable updates
|
||||
# This is used to replicate a subset of the software update
|
||||
# server data to our local cache.
|
||||
FILTERED_CATALOG_NAME = 'filtered_index.sucatalog'
|
||||
|
||||
# The catalog containing only updates to be downloaded and installed.
|
||||
# We use this one when downloading Apple updates.
|
||||
# In this case package URLs are still pointing to the
|
||||
# software update server so we can download them, but the rest of the
|
||||
# URLs point to our local cache.
|
||||
LOCAL_DOWNLOAD_CATALOG_NAME = 'local_download.sucatalog'
|
||||
|
||||
# Catalog with all URLs (including package URLs) pointed to local cache.
|
||||
# We use this one during install phase.
|
||||
# This causes softwareupdate -i -a to fail cleanly if we don't
|
||||
# have the required packages already downloaded.
|
||||
LOCAL_CATALOG_NAME = 'local_install.sucatalog'
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Class for domain specific exceptions."""
|
||||
|
||||
|
||||
class ReplicationError(Error):
|
||||
"""A custom error when replication fails."""
|
||||
|
||||
|
||||
class CatalogNotFoundError(Error):
|
||||
"""A catalog was not found."""
|
||||
|
||||
|
||||
class AppleUpdates(object):
|
||||
|
||||
"""Class to installation of Apple Software Updates within Munki.
|
||||
"""Class for installation of Apple Software Updates within Munki.
|
||||
|
||||
This class handles update detection, as well as downloading and installation
|
||||
of those updates.
|
||||
"""
|
||||
|
||||
RESTART_ACTIONS = ['RequireRestart', 'RecommendRestart']
|
||||
ORIGINAL_CATALOG_URL_KEY = '_OriginalCatalogURL'
|
||||
|
||||
def __init__(self):
|
||||
self._managed_install_dir = munkicommon.pref('ManagedInstallDir')
|
||||
self.apple_updates_plist = os.path.join(
|
||||
self._managed_install_dir, 'AppleUpdates.plist')
|
||||
|
||||
# fix things if somehow we died last time before resetting the
|
||||
# original CatalogURL
|
||||
os_version_tuple = munkicommon.getOsVersion(as_tuple=True)
|
||||
if os_version_tuple in [(10, 9), (10, 10)]:
|
||||
self._ResetOriginalCatalogURL()
|
||||
su_prefs.ResetOriginalCatalogURL()
|
||||
|
||||
real_cache_dir = os.path.join(self._managed_install_dir, 'swupd')
|
||||
if os.path.exists(real_cache_dir):
|
||||
if not os.path.isdir(real_cache_dir):
|
||||
munkicommon.display_error(
|
||||
'%s exists but is not a dir.', real_cache_dir)
|
||||
else:
|
||||
os.mkdir(real_cache_dir)
|
||||
|
||||
# symlink to work around an issue with paths containing spaces
|
||||
# in 10.8.2's SoftwareUpdate
|
||||
self.cache_dir = os.path.join('/tmp', 'munki_swupd_cache')
|
||||
try:
|
||||
if os.path.islink(self.cache_dir):
|
||||
# remove any pre-existing symlink
|
||||
os.unlink(self.cache_dir)
|
||||
if os.path.exists(self.cache_dir):
|
||||
# there should not be a file or directory at that path!
|
||||
# move it
|
||||
new_name = os.path.join(
|
||||
'/tmp', ('munki_swupd_cache_moved_%s'
|
||||
% time.strftime('%Y.%m.%d.%H.%M.%S')))
|
||||
os.rename(self.cache_dir, new_name)
|
||||
os.symlink(real_cache_dir, self.cache_dir)
|
||||
except (OSError, IOError) as err:
|
||||
# error in setting up the cache directories
|
||||
raise Error('Could not configure cache directory: %s' % err)
|
||||
|
||||
self.temp_cache_dir = os.path.join(self.cache_dir, 'mirror')
|
||||
self.local_catalog_dir = os.path.join(
|
||||
self.cache_dir, LOCAL_CATALOG_DIR_REL_PATH)
|
||||
|
||||
self.apple_updates_plist = os.path.join(
|
||||
self._managed_install_dir, 'AppleUpdates.plist')
|
||||
|
||||
self.apple_download_catalog_path = os.path.join(
|
||||
self.temp_cache_dir, APPLE_DOWNLOAD_CATALOG_NAME)
|
||||
|
||||
self.filtered_catalog_path = os.path.join(
|
||||
self.local_catalog_dir, FILTERED_CATALOG_NAME)
|
||||
self.local_catalog_path = os.path.join(
|
||||
self.local_catalog_dir, LOCAL_CATALOG_NAME)
|
||||
self.extracted_catalog_path = os.path.join(
|
||||
self.local_catalog_dir, APPLE_EXTRACTED_CATALOG_NAME)
|
||||
self.local_download_catalog_path = os.path.join(
|
||||
self.local_catalog_dir, LOCAL_DOWNLOAD_CATALOG_NAME)
|
||||
self.applesync = sync.AppleUpdateSync()
|
||||
|
||||
self._update_list_cache = None
|
||||
|
||||
@@ -210,270 +96,6 @@ class AppleUpdates(object):
|
||||
# pylint: disable=no-self-use
|
||||
munkicommon.display_status_major(message)
|
||||
|
||||
def _GetURLPath(self, full_url):
|
||||
"""Returns only the URL path.
|
||||
|
||||
Args:
|
||||
full_url: a str URL, complete with schema, domain, path, etc.
|
||||
Returns:
|
||||
The str path of the URL.
|
||||
"""
|
||||
# pylint: disable=no-self-use
|
||||
return urlparse.urlsplit(full_url)[2] # (schema, netloc, path, ...)
|
||||
|
||||
def RewriteURL(self, full_url):
|
||||
"""Rewrites a single URL to point to our local replica.
|
||||
|
||||
Args:
|
||||
full_url: a str URL, complete with schema, domain, path, etc.
|
||||
Returns:
|
||||
A str URL, rewritten if needed to point to the local cache.
|
||||
"""
|
||||
local_base_url = 'file://localhost' + urllib2.quote(self.cache_dir)
|
||||
if full_url.startswith(local_base_url):
|
||||
return full_url # url is already local, so just return it.
|
||||
return local_base_url + self._GetURLPath(full_url)
|
||||
|
||||
def RewriteProductURLs(self, product, rewrite_pkg_urls=False):
|
||||
"""Rewrites URLs in the product to point to our local cache.
|
||||
|
||||
Args:
|
||||
product: list, of dicts, product info. This dict is changed by
|
||||
this function.
|
||||
rewrite_pkg_urls: bool, default False, if True package URLs are
|
||||
rewritten, otherwise only MetadataURLs are rewritten.
|
||||
"""
|
||||
if 'ServerMetadataURL' in product:
|
||||
product['ServerMetadataURL'] = self.RewriteURL(
|
||||
product['ServerMetadataURL'])
|
||||
for package in product.get('Packages', []):
|
||||
if rewrite_pkg_urls and 'URL' in package:
|
||||
package['URL'] = self.RewriteURL(package['URL'])
|
||||
if 'MetadataURL' in package:
|
||||
package['MetadataURL'] = self.RewriteURL(
|
||||
package['MetadataURL'])
|
||||
distributions = product['Distributions']
|
||||
for dist_lang in distributions.keys():
|
||||
distributions[dist_lang] = self.RewriteURL(
|
||||
distributions[dist_lang])
|
||||
|
||||
def RewriteCatalogURLs(self, catalog, rewrite_pkg_urls=False):
|
||||
"""Rewrites URLs in a catalog to point to our local replica.
|
||||
|
||||
Args:
|
||||
rewrite_pkg_urls: Boolean, if True package URLs are rewritten,
|
||||
otherwise only MetadataURLs are rewritten.
|
||||
"""
|
||||
if not 'Products' in catalog:
|
||||
return
|
||||
|
||||
for product_key in catalog['Products'].keys():
|
||||
product = catalog['Products'][product_key]
|
||||
self.RewriteProductURLs(product, rewrite_pkg_urls=rewrite_pkg_urls)
|
||||
|
||||
def RetrieveURLToCacheDir(self, full_url, copy_only_if_missing=False):
|
||||
"""Downloads a URL and stores it in the same relative path on our
|
||||
filesystem. Returns a path to the replicated file.
|
||||
|
||||
Args:
|
||||
full_url: str, full URL to retrieve.
|
||||
copy_only_if_missing: boolean, True to copy only if the file is not
|
||||
already cached, False to copy regardless of existence in cache.
|
||||
Returns:
|
||||
String path to the locally cached file.
|
||||
"""
|
||||
relative_url = os.path.normpath(self._GetURLPath(full_url).lstrip('/'))
|
||||
local_file_path = os.path.join(self.cache_dir, relative_url)
|
||||
local_dir_path = os.path.dirname(local_file_path)
|
||||
if copy_only_if_missing and os.path.exists(local_file_path):
|
||||
return local_file_path
|
||||
if not os.path.exists(local_dir_path):
|
||||
try:
|
||||
os.makedirs(local_dir_path)
|
||||
except OSError as oserr:
|
||||
raise ReplicationError(oserr)
|
||||
try:
|
||||
self.GetSoftwareUpdateResource(
|
||||
full_url, local_file_path, resume=True)
|
||||
except fetch.Error as err:
|
||||
raise ReplicationError(err)
|
||||
return local_file_path
|
||||
|
||||
def GetSoftwareUpdateResource(self, url, destinationpath, resume=False):
|
||||
"""Gets item from Apple Software Update Server.
|
||||
|
||||
Args:
|
||||
url: str, URL of the resource to download.
|
||||
destinationpath: str, path of the destination to save the resource.
|
||||
resume: boolean, True to resume downloads, False to redownload.
|
||||
Returns:
|
||||
Boolean. True if a new download was required, False if the item was
|
||||
already in the local cache.
|
||||
"""
|
||||
# pylint: disable=no-self-use
|
||||
machine = munkicommon.getMachineFacts()
|
||||
darwin_version = os.uname()[2]
|
||||
# Set the User-Agent header to match that used by Apple's
|
||||
# softwareupdate client for better compatibility.
|
||||
user_agent_header = (
|
||||
"User-Agent: managedsoftwareupdate/%s Darwin/%s (%s) (%s)"
|
||||
% (machine['munki_version'], darwin_version,
|
||||
machine['arch'], machine['machine_model']))
|
||||
return fetch.getResourceIfChangedAtomically(
|
||||
url, destinationpath, custom_headers=[user_agent_header],
|
||||
resume=resume, follow_redirects=True)
|
||||
|
||||
def CacheUpdateMetadata(self):
|
||||
"""Copies ServerMetadata (.smd), Metadata (.pkm), and
|
||||
Distribution (.dist) files for the available updates to the local
|
||||
machine and writes a new sucatalog that refers to the local copies
|
||||
of these files."""
|
||||
catalog = FoundationPlist.readPlist(self.filtered_catalog_path)
|
||||
if not 'Products' in catalog:
|
||||
munkicommon.display_warning(
|
||||
'"Products" not found in %s', self.filtered_catalog_path)
|
||||
return
|
||||
|
||||
for product_key in catalog['Products'].keys():
|
||||
if munkicommon.stopRequested():
|
||||
break
|
||||
munkicommon.display_status_minor(
|
||||
'Caching metadata for product ID %s', product_key)
|
||||
product = catalog['Products'][product_key]
|
||||
if 'ServerMetadataURL' in product:
|
||||
self.RetrieveURLToCacheDir(
|
||||
product['ServerMetadataURL'], copy_only_if_missing=True)
|
||||
|
||||
for package in product.get('Packages', []):
|
||||
if munkicommon.stopRequested():
|
||||
break
|
||||
if 'MetadataURL' in package:
|
||||
munkicommon.display_status_minor(
|
||||
'Caching package metadata for product ID %s',
|
||||
product_key)
|
||||
self.RetrieveURLToCacheDir(
|
||||
package['MetadataURL'], copy_only_if_missing=True)
|
||||
# if 'URL' in package:
|
||||
# munkicommon.display_status_minor(
|
||||
# 'Caching package for product ID %s',
|
||||
# product_key)
|
||||
# self.RetrieveURLToCacheDir(
|
||||
# package['URL'], copy_only_if_missing=True)
|
||||
|
||||
distributions = product['Distributions']
|
||||
for dist_lang in distributions.keys():
|
||||
if munkicommon.stopRequested():
|
||||
break
|
||||
munkicommon.display_status_minor(
|
||||
'Caching %s distribution for product ID %s',
|
||||
dist_lang, product_key)
|
||||
dist_url = distributions[dist_lang]
|
||||
try:
|
||||
self.RetrieveURLToCacheDir(
|
||||
dist_url, copy_only_if_missing=True)
|
||||
except ReplicationError:
|
||||
munkicommon.display_warning(
|
||||
'Could not cache %s distribution for product ID %s',
|
||||
dist_lang, product_key)
|
||||
|
||||
if munkicommon.stopRequested():
|
||||
return
|
||||
|
||||
if not os.path.exists(self.local_catalog_dir):
|
||||
try:
|
||||
os.makedirs(self.local_catalog_dir)
|
||||
except OSError as oserr:
|
||||
raise ReplicationError(oserr)
|
||||
|
||||
# rewrite metadata URLs to point to local caches.
|
||||
self.RewriteCatalogURLs(catalog, rewrite_pkg_urls=False)
|
||||
FoundationPlist.writePlist(
|
||||
catalog, self.local_download_catalog_path)
|
||||
|
||||
# rewrite all URLs, including pkgs, to point to local caches.
|
||||
self.RewriteCatalogURLs(catalog, rewrite_pkg_urls=True)
|
||||
FoundationPlist.writePlist(catalog, self.local_catalog_path)
|
||||
|
||||
def _GetPreferredLocalization(self, list_of_localizations):
|
||||
'''Picks the best localization from a list of available
|
||||
localizations. Returns a single language/localization name.'''
|
||||
# pylint: disable=no-self-use
|
||||
localization_preferences = (
|
||||
munkicommon.pref('AppleSoftwareUpdateLanguages') or ['English'])
|
||||
preferred_langs = (
|
||||
NSBundle.preferredLocalizationsFromArray_forPreferences_(
|
||||
list_of_localizations, localization_preferences))
|
||||
if preferred_langs:
|
||||
return preferred_langs[0]
|
||||
|
||||
# first fallback, return en or English
|
||||
if 'English' in list_of_localizations:
|
||||
return 'English'
|
||||
elif 'en' in list_of_localizations:
|
||||
return 'en'
|
||||
|
||||
# if we get this far, just return the first language
|
||||
# in the list of available languages
|
||||
return list_of_localizations[0]
|
||||
|
||||
def GetDistributionForProductKey(
|
||||
self, product_key, sucatalog, language=None):
|
||||
'''Returns the path to a distibution file from /Library/Updates
|
||||
or the local cache for the given product_key. If language is
|
||||
defined it will try to retrieve that specific language, otherwise
|
||||
it will use the available languages and the value of the
|
||||
AppleSoftwareUpdateLanguages preference to return the "best"
|
||||
language of those available.'''
|
||||
try:
|
||||
catalog = FoundationPlist.readPlist(sucatalog)
|
||||
except FoundationPlist.NSPropertyListSerializationException:
|
||||
return None
|
||||
product = catalog.get('Products', {}).get(product_key, {})
|
||||
if product:
|
||||
distributions = product.get('Distributions', {})
|
||||
if distributions:
|
||||
available_languages = distributions.keys()
|
||||
if language:
|
||||
preferred_language = language
|
||||
else:
|
||||
preferred_language = self._GetPreferredLocalization(
|
||||
available_languages)
|
||||
url = distributions[preferred_language]
|
||||
# do we already have it in /Library/Updates?
|
||||
filename = os.path.basename(self._GetURLPath(url))
|
||||
dist_path = os.path.join(
|
||||
'/Library/Updates', product_key, filename)
|
||||
if os.path.exists(dist_path):
|
||||
return dist_path
|
||||
# look for it in the cache
|
||||
if url.startswith('file://localhost'):
|
||||
fileurl = url[len('file://localhost'):]
|
||||
dist_path = urllib2.unquote(fileurl)
|
||||
if os.path.exists(dist_path):
|
||||
return dist_path
|
||||
# we haven't downloaded this yet
|
||||
try:
|
||||
return self.RetrieveURLToCacheDir(
|
||||
url, copy_only_if_missing=True)
|
||||
except ReplicationError, err:
|
||||
munkicommon.display_error(
|
||||
'Could not retrieve %s: %s', url, err)
|
||||
return None
|
||||
|
||||
def _WriteFilteredCatalog(self, product_ids, catalog_path):
|
||||
"""Write out a sucatalog containing only the updates in product_ids.
|
||||
|
||||
Args:
|
||||
product_ids: list of str, ProductIDs.
|
||||
catalog_path: str, path of catalog to write.
|
||||
"""
|
||||
catalog = FoundationPlist.readPlist(self.extracted_catalog_path)
|
||||
product_ids = set(product_ids) # convert to set for O(1) lookups.
|
||||
for product_id in list(catalog.get('Products', [])):
|
||||
if product_id not in product_ids:
|
||||
del catalog['Products'][product_id]
|
||||
FoundationPlist.writePlist(catalog, catalog_path)
|
||||
|
||||
def IsRestartNeeded(self):
|
||||
"""Returns True if any update requires an restart."""
|
||||
try:
|
||||
@@ -521,7 +143,7 @@ class AppleUpdates(object):
|
||||
if os_version_tuple >= (10, 11):
|
||||
catalog_url = None
|
||||
else:
|
||||
catalog_url = self._GetAppleCatalogURL()
|
||||
catalog_url = self.applesync.GetAppleCatalogURL()
|
||||
|
||||
retcode = self._RunSoftwareUpdate(
|
||||
['-d', '-a'], catalog_url=catalog_url, stop_allowed=True)
|
||||
@@ -530,7 +152,7 @@ class AppleUpdates(object):
|
||||
return False
|
||||
# not sure all older OS X versions set LastSessionSuccessful, so
|
||||
# react only if it's explicitly set to False
|
||||
last_session_successful = self.GetSoftwareUpdatePref(
|
||||
last_session_successful = su_prefs.GetSoftwareUpdatePref(
|
||||
'LastSessionSuccessful')
|
||||
if last_session_successful is False:
|
||||
munkicommon.display_error(
|
||||
@@ -546,7 +168,7 @@ class AppleUpdates(object):
|
||||
"""
|
||||
|
||||
# first, try to get the list from com.apple.SoftwareUpdate preferences
|
||||
recommended_updates = self.GetSoftwareUpdatePref(
|
||||
recommended_updates = su_prefs.GetSoftwareUpdatePref(
|
||||
'RecommendedUpdates')
|
||||
if recommended_updates:
|
||||
return [item['Product Key'] for item in recommended_updates
|
||||
@@ -567,89 +189,6 @@ class AppleUpdates(object):
|
||||
"Error processing %s: %s", INDEX_PLIST, err)
|
||||
return []
|
||||
|
||||
def ExtractAndCopyDownloadedCatalog(self, _open=open):
|
||||
"""Copy the downloaded catalog to a new file, extracting if gzipped.
|
||||
|
||||
Args:
|
||||
_open: func, default builtin open(), open method for unit testing.
|
||||
"""
|
||||
if not os.path.exists(self.local_catalog_dir):
|
||||
try:
|
||||
os.makedirs(self.local_catalog_dir)
|
||||
except OSError as oserr:
|
||||
raise ReplicationError(oserr)
|
||||
|
||||
local_apple_sus_catalog = os.path.join(
|
||||
self.local_catalog_dir, APPLE_EXTRACTED_CATALOG_NAME)
|
||||
|
||||
f = _open(self.apple_download_catalog_path, 'rb')
|
||||
magic = f.read(2)
|
||||
contents = ''
|
||||
if magic == '\x1f\x8b': # File is gzip compressed.
|
||||
f.close() # Close the open handle first.
|
||||
f = gzip.open(self.apple_download_catalog_path, 'rb')
|
||||
else: # Hopefully a nice plain plist.
|
||||
f.seek(0)
|
||||
contents = f.read()
|
||||
f.close()
|
||||
f = _open(local_apple_sus_catalog, 'wb')
|
||||
f.write(contents)
|
||||
f.close()
|
||||
|
||||
def _GetAppleCatalogURL(self):
|
||||
"""Returns the catalog URL of the Apple SU catalog for the current Mac.
|
||||
|
||||
Returns:
|
||||
String catalog URL for the current Mac.
|
||||
Raises:
|
||||
CatalogNotFoundError: an Apple catalog was not found for this Mac.
|
||||
"""
|
||||
# Prefer Munki's preferences file.
|
||||
munkisuscatalog = munkicommon.pref('SoftwareUpdateServerURL')
|
||||
if munkisuscatalog:
|
||||
return munkisuscatalog
|
||||
|
||||
# Otherwise prefer MCX or /Library/Preferences/com.apple.SoftwareUpdate
|
||||
prefs_catalog_url = self.GetSoftwareUpdatePref('CatalogURL')
|
||||
if prefs_catalog_url:
|
||||
return prefs_catalog_url
|
||||
|
||||
# Finally, fall back to using a hard-coded url in DEFAULT_CATALOG_URLS.
|
||||
os_version = munkicommon.getOsVersion()
|
||||
catalog_url = DEFAULT_CATALOG_URLS.get(os_version, None)
|
||||
if catalog_url:
|
||||
return catalog_url
|
||||
|
||||
raise CatalogNotFoundError(
|
||||
'No default Software Update CatalogURL for: %s' % os_version)
|
||||
|
||||
def CacheAppleCatalog(self):
|
||||
"""Caches a local copy of the current Apple SUS catalog.
|
||||
|
||||
Raises:
|
||||
CatalogNotFoundError: a catalog was not found to cache.
|
||||
ReplicationError: there was an error making the cache directory.
|
||||
fetch.MunkiDownloadError: error downloading the catalog.
|
||||
"""
|
||||
try:
|
||||
catalog_url = self._GetAppleCatalogURL()
|
||||
except CatalogNotFoundError as err:
|
||||
munkicommon.display_error(unicode(err))
|
||||
raise
|
||||
if not os.path.exists(self.temp_cache_dir):
|
||||
try:
|
||||
os.makedirs(self.temp_cache_dir)
|
||||
except OSError as oserr:
|
||||
raise ReplicationError(oserr)
|
||||
msg = 'Checking Apple Software Update catalog...'
|
||||
self._ResetMunkiStatusAndDisplayMessage(msg)
|
||||
munkicommon.display_detail('Caching CatalogURL %s', catalog_url)
|
||||
try:
|
||||
dummy_file_changed = self.GetSoftwareUpdateResource(
|
||||
catalog_url, self.apple_download_catalog_path, resume=True)
|
||||
self.ExtractAndCopyDownloadedCatalog()
|
||||
except fetch.Error:
|
||||
raise
|
||||
|
||||
def InstalledApplePackagesHaveChanged(self):
|
||||
"""Generates a SHA-256 checksum of the info for all packages in the
|
||||
@@ -686,7 +225,8 @@ class AppleUpdates(object):
|
||||
Returns:
|
||||
Boolean. True if a force check is needed, False otherwise.
|
||||
"""
|
||||
new_hash = munkicommon.getsha256hash(self.apple_download_catalog_path)
|
||||
new_hash = munkicommon.getsha256hash(
|
||||
self.applesync.apple_download_catalog_path)
|
||||
if original_hash != new_hash:
|
||||
munkicommon.log('Apple update catalog has changed.')
|
||||
return True
|
||||
@@ -712,13 +252,15 @@ class AppleUpdates(object):
|
||||
Boolean. True if there are updates, False otherwise.
|
||||
"""
|
||||
before_hash = munkicommon.getsha256hash(
|
||||
self.apple_download_catalog_path)
|
||||
self.applesync.apple_download_catalog_path)
|
||||
|
||||
msg = 'Checking Apple Software Update catalog...'
|
||||
self._ResetMunkiStatusAndDisplayMessage(msg)
|
||||
try:
|
||||
self.CacheAppleCatalog()
|
||||
except CatalogNotFoundError:
|
||||
self.applesync.CacheAppleCatalog()
|
||||
except sync.CatalogNotFoundError:
|
||||
return False
|
||||
except (ReplicationError, fetch.Error) as err:
|
||||
except (sync.ReplicationError, fetch.Error) as err:
|
||||
munkicommon.display_warning(
|
||||
'Could not download Apple SUS catalog:')
|
||||
munkicommon.display_warning('\t%s', unicode(err))
|
||||
@@ -731,28 +273,25 @@ class AppleUpdates(object):
|
||||
'unchanged and we recently did a full check.')
|
||||
# return True if we have cached updates
|
||||
# False otherwise
|
||||
if self.GetSoftwareUpdateInfo():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return bool(self.GetSoftwareUpdateInfo())
|
||||
|
||||
if self.DownloadAvailableUpdates(): # Success; ready to install.
|
||||
munkicommon.set_pref('LastAppleSoftwareUpdateCheck', NSDate.date())
|
||||
product_ids = self.GetAvailableUpdateProductIDs()
|
||||
if not product_ids:
|
||||
# No updates found
|
||||
# TO-DO: clear metadata cache
|
||||
return False
|
||||
os_version_tuple = munkicommon.getOsVersion(as_tuple=True)
|
||||
if os_version_tuple < (10, 11):
|
||||
self._WriteFilteredCatalog(
|
||||
product_ids, self.filtered_catalog_path)
|
||||
try:
|
||||
self.CacheUpdateMetadata()
|
||||
except ReplicationError as err:
|
||||
munkicommon.display_warning(
|
||||
'Could not replicate software update metadata:')
|
||||
munkicommon.display_warning('\t%s', unicode(err))
|
||||
return False
|
||||
self.applesync.WriteFilteredCatalog(product_ids)
|
||||
try:
|
||||
self.applesync.CacheUpdateMetadata(product_ids)
|
||||
except sync.ReplicationError as err:
|
||||
munkicommon.display_warning(
|
||||
'Could not replicate software update metadata:')
|
||||
munkicommon.display_warning('\t%s', unicode(err))
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
# Download error, allow check again soon.
|
||||
@@ -813,7 +352,7 @@ class AppleUpdates(object):
|
||||
apple_updates = []
|
||||
|
||||
# first, try to get the list from com.apple.SoftwareUpdate preferences
|
||||
recommended_updates = self.GetSoftwareUpdatePref(
|
||||
recommended_updates = su_prefs.GetSoftwareUpdatePref(
|
||||
'RecommendedUpdates')
|
||||
if recommended_updates:
|
||||
for item in recommended_updates:
|
||||
@@ -846,52 +385,45 @@ class AppleUpdates(object):
|
||||
munkicommon.display_error(
|
||||
"Error parsing %s: %s", INDEX_PLIST, err)
|
||||
|
||||
if product_keys:
|
||||
os_version_tuple = munkicommon.getOsVersion(as_tuple=True)
|
||||
if os_version_tuple < (10, 11):
|
||||
sucatalog = self.local_catalog_path
|
||||
else:
|
||||
# use the cached Apple catalog
|
||||
sucatalog = self.extracted_catalog_path
|
||||
for product_key in product_keys:
|
||||
if not self.UpdateDownloaded(product_key):
|
||||
munkicommon.display_warning(
|
||||
'Product %s does not appear to be downloaded',
|
||||
product_key)
|
||||
continue
|
||||
localized_dist = self.GetDistributionForProductKey(
|
||||
product_key, sucatalog)
|
||||
if not localized_dist:
|
||||
munkicommon.display_warning(
|
||||
'No dist file for product %s', product_key)
|
||||
continue
|
||||
if (not recommended_updates and
|
||||
not localized_dist.endswith('English.dist')):
|
||||
# we need the English versions of some of the data
|
||||
# see (https://groups.google.com/d/msg/munki-dev/
|
||||
# _5HdMyy3kKU/YFxqslayDQAJ)
|
||||
english_dist = self.GetDistributionForProductKey(
|
||||
product_key, sucatalog, 'English')
|
||||
if english_dist:
|
||||
english_su_info = appledistutils.parse_su_dist(
|
||||
english_dist)
|
||||
su_info = appledistutils.parse_su_dist(localized_dist)
|
||||
su_info['productKey'] = product_key
|
||||
if su_info['name'] == '':
|
||||
su_info['name'] = product_key
|
||||
if product_key in update_display_names:
|
||||
su_info['apple_product_name'] = (
|
||||
update_display_names[product_key])
|
||||
elif english_su_info:
|
||||
su_info['apple_product_name'] = (
|
||||
english_su_info['apple_product_name'])
|
||||
if product_key in update_versions:
|
||||
su_info['version_to_install'] = (
|
||||
update_versions[product_key])
|
||||
elif english_su_info:
|
||||
su_info['version_to_install'] = (
|
||||
english_su_info['version_to_install'])
|
||||
apple_updates.append(su_info)
|
||||
for product_key in product_keys:
|
||||
if not self.UpdateDownloaded(product_key):
|
||||
munkicommon.display_warning(
|
||||
'Product %s does not appear to be downloaded',
|
||||
product_key)
|
||||
continue
|
||||
localized_dist = self.applesync.GetDistributionForProductKey(
|
||||
product_key)
|
||||
if not localized_dist:
|
||||
munkicommon.display_warning(
|
||||
'No dist file for product %s', product_key)
|
||||
continue
|
||||
if (not recommended_updates and
|
||||
not localized_dist.endswith('English.dist')):
|
||||
# we need the English versions of some of the data
|
||||
# see (https://groups.google.com/d/msg/munki-dev/
|
||||
# _5HdMyy3kKU/YFxqslayDQAJ)
|
||||
english_dist = self.applesync.GetDistributionForProductKey(
|
||||
product_key, 'English')
|
||||
if english_dist:
|
||||
english_su_info = dist.parse_su_dist(
|
||||
english_dist)
|
||||
su_info = dist.parse_su_dist(localized_dist)
|
||||
su_info['productKey'] = product_key
|
||||
if su_info['name'] == '':
|
||||
su_info['name'] = product_key
|
||||
if product_key in update_display_names:
|
||||
su_info['apple_product_name'] = (
|
||||
update_display_names[product_key])
|
||||
elif english_su_info:
|
||||
su_info['apple_product_name'] = (
|
||||
english_su_info['apple_product_name'])
|
||||
if product_key in update_versions:
|
||||
su_info['version_to_install'] = (
|
||||
update_versions[product_key])
|
||||
elif english_su_info:
|
||||
su_info['version_to_install'] = (
|
||||
english_su_info['version_to_install'])
|
||||
apple_updates.append(su_info)
|
||||
|
||||
return apple_updates
|
||||
|
||||
@@ -958,123 +490,6 @@ class AppleUpdates(object):
|
||||
munkicommon.display_info(' *Logout required')
|
||||
munkicommon.report['LogoutRequired'] = True
|
||||
|
||||
def GetSoftwareUpdatePref(self, pref_name):
|
||||
"""Returns a preference from com.apple.SoftwareUpdate.
|
||||
|
||||
Uses CoreFoundation.
|
||||
|
||||
Args:
|
||||
pref_name: str preference name to get.
|
||||
"""
|
||||
# pylint: disable=no-self-use
|
||||
return CFPreferencesCopyAppValue(
|
||||
pref_name, APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN)
|
||||
|
||||
def _GetCatalogURL(self):
|
||||
"""Returns Software Update's CatalogURL"""
|
||||
# pylint: disable=no-self-use
|
||||
return CFPreferencesCopyValue(
|
||||
'CatalogURL',
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost)
|
||||
|
||||
def _SetCustomCatalogURL(self, catalog_url):
|
||||
"""Sets Software Update's CatalogURL to custom value, storing the
|
||||
original"""
|
||||
software_update_key_list = CFPreferencesCopyKeyList(
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost) or []
|
||||
if self.ORIGINAL_CATALOG_URL_KEY not in software_update_key_list:
|
||||
# store the original CatalogURL
|
||||
original_catalog_url = self._GetCatalogURL()
|
||||
if not original_catalog_url:
|
||||
# can't store None as a CFPreference
|
||||
original_catalog_url = ""
|
||||
CFPreferencesSetValue(
|
||||
self.ORIGINAL_CATALOG_URL_KEY,
|
||||
original_catalog_url,
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost)
|
||||
# now set our custom CatalogURL
|
||||
os_version_tuple = munkicommon.getOsVersion(as_tuple=True)
|
||||
if os_version_tuple < (10, 11):
|
||||
CFPreferencesSetValue(
|
||||
'CatalogURL', catalog_url,
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost)
|
||||
# finally, sync things up
|
||||
if not CFPreferencesSynchronize(
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost):
|
||||
munkicommon.display_error(
|
||||
'Error setting com.apple.SoftwareUpdate CatalogURL.')
|
||||
else:
|
||||
# use softwareupdate --set-catalog
|
||||
proc = subprocess.Popen(
|
||||
['/usr/sbin/softwareupdate', '--set-catalog', catalog_url],
|
||||
bufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(output, err) = proc.communicate()
|
||||
if output:
|
||||
munkicommon.display_detail(output)
|
||||
if err:
|
||||
munkicommon.display_error(err)
|
||||
|
||||
def _ResetOriginalCatalogURL(self):
|
||||
"""Resets SoftwareUpdate's CatalogURL to the original value"""
|
||||
software_update_key_list = CFPreferencesCopyKeyList(
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost) or []
|
||||
if self.ORIGINAL_CATALOG_URL_KEY not in software_update_key_list:
|
||||
# do nothing
|
||||
return
|
||||
original_catalog_url = CFPreferencesCopyValue(
|
||||
self.ORIGINAL_CATALOG_URL_KEY,
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost)
|
||||
if not original_catalog_url:
|
||||
original_catalog_url = None
|
||||
# reset CatalogURL to the one we stored
|
||||
os_version_tuple = munkicommon.getOsVersion(as_tuple=True)
|
||||
if os_version_tuple < (10, 11):
|
||||
CFPreferencesSetValue(
|
||||
'CatalogURL', original_catalog_url,
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost)
|
||||
else:
|
||||
if original_catalog_url:
|
||||
# use softwareupdate --set-catalog
|
||||
cmd = ['/usr/sbin/softwareupdate',
|
||||
'--set-catalog', original_catalog_url]
|
||||
else:
|
||||
# use softwareupdate --clear-catalog
|
||||
cmd = ['/usr/sbin/softwareupdate', '--clear-catalog']
|
||||
proc = subprocess.Popen(cmd, bufsize=-1, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
(output, err) = proc.communicate()
|
||||
if output:
|
||||
munkicommon.display_detail(output)
|
||||
if err:
|
||||
munkicommon.display_error(err)
|
||||
|
||||
# remove ORIGINAL_CATALOG_URL_KEY
|
||||
CFPreferencesSetValue(
|
||||
self.ORIGINAL_CATALOG_URL_KEY, None,
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost)
|
||||
# sync
|
||||
if not CFPreferencesSynchronize(
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost):
|
||||
munkicommon.display_error(
|
||||
'Error resetting com.apple.SoftwareUpdate CatalogURL.')
|
||||
|
||||
def CatalogURLisManaged(self):
|
||||
"""Returns True if Software Update's CatalogURL is managed
|
||||
via MCX or Profiles"""
|
||||
# pylint: disable=no-self-use
|
||||
return CFPreferencesAppValueIsForced(
|
||||
'CatalogURL', APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN)
|
||||
|
||||
def _RunSoftwareUpdate(
|
||||
self, options_list, catalog_url=None, stop_allowed=False,
|
||||
mode=None, results=None):
|
||||
@@ -1124,7 +539,7 @@ class AppleUpdates(object):
|
||||
if os_version_tuple < (10, 9):
|
||||
cmd.extend(['--CatalogURL', catalog_url])
|
||||
elif os_version_tuple in [(10, 9), (10, 10)]:
|
||||
self._SetCustomCatalogURL(catalog_url)
|
||||
su_prefs.SetCustomCatalogURL(catalog_url)
|
||||
|
||||
cmd.extend(options_list)
|
||||
|
||||
@@ -1231,12 +646,12 @@ class AppleUpdates(object):
|
||||
if catalog_url:
|
||||
# reset CatalogURL if needed
|
||||
if os_version_tuple in [(10, 9), (10, 10)]:
|
||||
self._ResetOriginalCatalogURL()
|
||||
su_prefs.ResetOriginalCatalogURL()
|
||||
|
||||
retcode = job.returncode()
|
||||
if retcode == 0:
|
||||
# get SoftwareUpdate's LastResultCode
|
||||
last_result_code = self.GetSoftwareUpdatePref(
|
||||
last_result_code = su_prefs.GetSoftwareUpdatePref(
|
||||
'LastResultCode') or 0
|
||||
if last_result_code > 2:
|
||||
retcode = last_result_code
|
||||
@@ -1246,9 +661,6 @@ class AppleUpdates(object):
|
||||
|
||||
return retcode
|
||||
|
||||
# TODO(jrand): The below functions are externally called. Should all
|
||||
# others be private?
|
||||
|
||||
def InstallAppleUpdates(self, only_unattended=False):
|
||||
"""Uses softwareupdate to install previously downloaded updates.
|
||||
|
||||
@@ -1309,13 +721,13 @@ class AppleUpdates(object):
|
||||
catalog_url = None
|
||||
else:
|
||||
# use our filtered local catalog
|
||||
if not os.path.exists(self.local_catalog_path):
|
||||
if not os.path.exists(self.applesync.local_catalog_path):
|
||||
munkicommon.display_error(
|
||||
'Missing local Software Update catalog at %s',
|
||||
self.local_catalog_path)
|
||||
self.applesync.local_catalog_path)
|
||||
return False # didn't do anything, so no restart needed
|
||||
catalog_url = 'file://localhost' + urllib2.quote(
|
||||
self.local_catalog_path)
|
||||
self.applesync.local_catalog_path)
|
||||
|
||||
retcode = self._RunSoftwareUpdate(
|
||||
su_options, mode='install', catalog_url=catalog_url,
|
||||
@@ -1369,12 +781,11 @@ class AppleUpdates(object):
|
||||
# since we may have performed some unattended installs
|
||||
if only_unattended:
|
||||
product_ids = self.GetAvailableUpdateProductIDs()
|
||||
self._WriteFilteredCatalog(product_ids, self.filtered_catalog_path)
|
||||
self.applesync.WriteFilteredCatalog(product_ids)
|
||||
|
||||
# clean up our now stale local cache
|
||||
if os.path.exists(self.cache_dir) and not only_unattended:
|
||||
# TODO(unassigned): change this to Pythonic delete.
|
||||
dummy_retcode = subprocess.call(['/bin/rm', '-rf', self.cache_dir])
|
||||
if not only_unattended:
|
||||
self.applesync.clean_up_cache()
|
||||
# remove the now invalid AppleUpdates.plist and AvailableUpdates.plist
|
||||
self.ClearAppleUpdateInfo()
|
||||
# Also clear our pref value for last check date. We may have
|
||||
@@ -1549,8 +960,6 @@ class AppleUpdates(object):
|
||||
# for now.
|
||||
|
||||
apple_updates_object = None
|
||||
|
||||
|
||||
def getAppleUpdatesInstance():
|
||||
"""Returns either an AppleUpdates instance, either cached or new."""
|
||||
global apple_updates_object
|
||||
@@ -1581,7 +990,7 @@ def appleSoftwareUpdatesAvailable(forcecheck=False, suppresscheck=False,
|
||||
munkicommon.display_warning(
|
||||
"Custom softwareupate catalog %s in Munki's preferences will "
|
||||
"be ignored." % munkisuscatalog)
|
||||
elif appleUpdatesObject.CatalogURLisManaged():
|
||||
elif su_prefs.CatalogURLisManaged():
|
||||
munkicommon.display_warning(
|
||||
"Cannot efficiently manage Apple Software updates because "
|
||||
"softwareupdate's CatalogURL is managed via MCX or profiles. "
|
||||
@@ -15,7 +15,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
appledistutils.py
|
||||
dist.py
|
||||
|
||||
Created by Greg Neagle on 2017-01-04.
|
||||
|
||||
@@ -34,9 +34,9 @@ from AppKit import NSAttributedString
|
||||
from LaunchServices import LSFindApplicationForInfo
|
||||
# pylint: enable=E0611
|
||||
|
||||
from . import display
|
||||
from . import pkgutils
|
||||
from . import FoundationPlist
|
||||
from .. import display
|
||||
from .. import pkgutils
|
||||
from .. import FoundationPlist
|
||||
|
||||
|
||||
def get_restart_action(restart_action_list):
|
||||
171
code/client/munkilib/appleupdates/su_prefs.py
Executable file
171
code/client/munkilib/appleupdates/su_prefs.py
Executable file
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/python
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Copyright 2009-2017 Greg Neagle.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
su_prefs.py
|
||||
|
||||
Created by Greg Neagle on 2017-01-06.
|
||||
|
||||
Utilities for working with Apple software update preferences
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
|
||||
# PyLint cannot properly find names inside Cocoa libraries, so issues bogus
|
||||
# No name 'Foo' in module 'Bar' warnings. Disable them.
|
||||
# pylint: disable=no-name-in-module
|
||||
from CoreFoundation import CFPreferencesAppValueIsForced
|
||||
from CoreFoundation import CFPreferencesCopyAppValue
|
||||
from CoreFoundation import CFPreferencesCopyKeyList
|
||||
from CoreFoundation import CFPreferencesCopyValue
|
||||
from CoreFoundation import CFPreferencesSetValue
|
||||
from CoreFoundation import CFPreferencesSynchronize
|
||||
from CoreFoundation import kCFPreferencesAnyUser
|
||||
#from CoreFoundation import kCFPreferencesCurrentUser
|
||||
from CoreFoundation import kCFPreferencesCurrentHost
|
||||
# pylint: enable=no-name-in-module
|
||||
|
||||
from .. import display
|
||||
from .. import osutils
|
||||
|
||||
# Preference domain for Apple Software Update.
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN = 'com.apple.SoftwareUpdate'
|
||||
|
||||
# prefs key to store original catalog URL
|
||||
ORIGINAL_CATALOG_URL_KEY = '_OriginalCatalogURL'
|
||||
|
||||
|
||||
def GetSoftwareUpdatePref(pref_name):
|
||||
"""Returns a preference from com.apple.SoftwareUpdate.
|
||||
|
||||
Uses CoreFoundation.
|
||||
|
||||
Args:
|
||||
pref_name: str preference name to get.
|
||||
"""
|
||||
return CFPreferencesCopyAppValue(
|
||||
pref_name, APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN)
|
||||
|
||||
|
||||
def CatalogURLisManaged():
|
||||
"""Returns True if Software Update's CatalogURL is managed
|
||||
via MCX or Profiles"""
|
||||
return CFPreferencesAppValueIsForced(
|
||||
'CatalogURL', APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN)
|
||||
|
||||
|
||||
def GetCatalogURL():
|
||||
"""Returns Software Update's CatalogURL"""
|
||||
return CFPreferencesCopyValue(
|
||||
'CatalogURL',
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost)
|
||||
|
||||
|
||||
def SetCustomCatalogURL(catalog_url):
|
||||
"""Sets Software Update's CatalogURL to custom value, storing the
|
||||
original"""
|
||||
software_update_key_list = CFPreferencesCopyKeyList(
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost) or []
|
||||
if ORIGINAL_CATALOG_URL_KEY not in software_update_key_list:
|
||||
# store the original CatalogURL
|
||||
original_catalog_url = GetCatalogURL()
|
||||
if not original_catalog_url:
|
||||
# can't store None as a CFPreference
|
||||
original_catalog_url = ""
|
||||
CFPreferencesSetValue(
|
||||
ORIGINAL_CATALOG_URL_KEY,
|
||||
original_catalog_url,
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost)
|
||||
# now set our custom CatalogURL
|
||||
os_version_tuple = osutils.getOsVersion(as_tuple=True)
|
||||
if os_version_tuple < (10, 11):
|
||||
CFPreferencesSetValue(
|
||||
'CatalogURL', catalog_url,
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost)
|
||||
# finally, sync things up
|
||||
if not CFPreferencesSynchronize(
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost):
|
||||
display.display_error(
|
||||
'Error setting com.apple.SoftwareUpdate CatalogURL.')
|
||||
else:
|
||||
# use softwareupdate --set-catalog
|
||||
proc = subprocess.Popen(
|
||||
['/usr/sbin/softwareupdate', '--set-catalog', catalog_url],
|
||||
bufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(output, err) = proc.communicate()
|
||||
if output:
|
||||
display.display_detail(output)
|
||||
if err:
|
||||
display.display_error(err)
|
||||
|
||||
|
||||
def ResetOriginalCatalogURL():
|
||||
"""Resets SoftwareUpdate's CatalogURL to the original value"""
|
||||
software_update_key_list = CFPreferencesCopyKeyList(
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost) or []
|
||||
if ORIGINAL_CATALOG_URL_KEY not in software_update_key_list:
|
||||
# do nothing
|
||||
return
|
||||
original_catalog_url = CFPreferencesCopyValue(
|
||||
ORIGINAL_CATALOG_URL_KEY,
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost)
|
||||
if not original_catalog_url:
|
||||
original_catalog_url = None
|
||||
# reset CatalogURL to the one we stored
|
||||
os_version_tuple = osutils.getOsVersion(as_tuple=True)
|
||||
if os_version_tuple < (10, 11):
|
||||
CFPreferencesSetValue(
|
||||
'CatalogURL', original_catalog_url,
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost)
|
||||
else:
|
||||
if original_catalog_url:
|
||||
# use softwareupdate --set-catalog
|
||||
cmd = ['/usr/sbin/softwareupdate',
|
||||
'--set-catalog', original_catalog_url]
|
||||
else:
|
||||
# use softwareupdate --clear-catalog
|
||||
cmd = ['/usr/sbin/softwareupdate', '--clear-catalog']
|
||||
proc = subprocess.Popen(cmd, bufsize=-1, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
(output, err) = proc.communicate()
|
||||
if output:
|
||||
display.display_detail(output)
|
||||
if err:
|
||||
display.display_error(err)
|
||||
|
||||
# remove ORIGINAL_CATALOG_URL_KEY
|
||||
CFPreferencesSetValue(
|
||||
ORIGINAL_CATALOG_URL_KEY, None,
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost)
|
||||
# sync
|
||||
if not CFPreferencesSynchronize(
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN,
|
||||
kCFPreferencesAnyUser, kCFPreferencesCurrentHost):
|
||||
display.display_error(
|
||||
'Error resetting com.apple.SoftwareUpdate CatalogURL.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print 'This is a library of support tools for the Munki Suite.'
|
||||
533
code/client/munkilib/appleupdates/sync.py
Executable file
533
code/client/munkilib/appleupdates/sync.py
Executable file
@@ -0,0 +1,533 @@
|
||||
#!/usr/bin/python
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Copyright 2009-2017 Greg Neagle.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
sync.py
|
||||
|
||||
Created by Greg Neagle on 2017-01-06.
|
||||
|
||||
Utilities for replicating and retreiving Apple software update metadata
|
||||
"""
|
||||
|
||||
import gzip
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import urllib2
|
||||
import urlparse
|
||||
|
||||
# PyLint cannot properly find names inside Cocoa libraries, so issues bogus
|
||||
# No name 'Foo' in module 'Bar' warnings. Disable them.
|
||||
# pylint: disable=E0611
|
||||
from Foundation import NSBundle
|
||||
# pylint: enable=E0611
|
||||
|
||||
|
||||
from . import su_prefs
|
||||
|
||||
from .. import display
|
||||
from .. import fetch
|
||||
from .. import info
|
||||
from .. import osutils
|
||||
from .. import prefs
|
||||
from .. import processes
|
||||
from .. import FoundationPlist
|
||||
|
||||
|
||||
# Apple Software Update Catalog URLs.
|
||||
DEFAULT_CATALOG_URLS = {
|
||||
'10.6': ('http://swscan.apple.com/content/catalogs/others/'
|
||||
'index-leopard-snowleopard.merged-1.sucatalog'),
|
||||
'10.7': ('http://swscan.apple.com/content/catalogs/others/'
|
||||
'index-lion-snowleopard-leopard.merged-1.sucatalog'),
|
||||
'10.8': ('http://swscan.apple.com/content/catalogs/others/'
|
||||
'index-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog'),
|
||||
'10.9': ('https://swscan.apple.com/content/catalogs/others/'
|
||||
'index-10.9-mountainlion-lion-snowleopard-leopard.merged-1'
|
||||
'.sucatalog'),
|
||||
'10.10': ('https://swscan.apple.com/content/catalogs/others/'
|
||||
'index-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1'
|
||||
'.sucatalog'),
|
||||
'10.11': ('https://swscan.apple.com/content/catalogs/others/'
|
||||
'index-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard'
|
||||
'.merged-1.sucatalog'),
|
||||
'10.12': ('https://swscan.apple.com/content/catalogs/others/'
|
||||
'index-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard'
|
||||
'-leopard.merged-1.sucatalog')
|
||||
}
|
||||
|
||||
# Preference domain for Apple Software Update.
|
||||
APPLE_SOFTWARE_UPDATE_PREFS_DOMAIN = 'com.apple.SoftwareUpdate'
|
||||
|
||||
# Path to the directory where local catalogs are stored, relative to
|
||||
# prefs.pref('ManagedInstallDir') + /swupd/mirror/.
|
||||
LOCAL_CATALOG_DIR_REL_PATH = 'content/catalogs/'
|
||||
|
||||
# The pristine, untouched, but potentially gzipped catalog.
|
||||
APPLE_DOWNLOAD_CATALOG_NAME = 'apple.sucatalog'
|
||||
|
||||
# The pristine, untouched, and extracted catalog.
|
||||
APPLE_EXTRACTED_CATALOG_NAME = 'apple_index.sucatalog'
|
||||
|
||||
# The catalog containing only applicable updates
|
||||
# This is used to replicate a subset of the software update
|
||||
# server data to our local cache.
|
||||
FILTERED_CATALOG_NAME = 'filtered_index.sucatalog'
|
||||
|
||||
# The catalog containing only updates to be downloaded and installed.
|
||||
# We use this one when downloading Apple updates.
|
||||
# In this case package URLs are still pointing to the
|
||||
# software update server so we can download them, but the rest of the
|
||||
# URLs point to our local cache.
|
||||
LOCAL_DOWNLOAD_CATALOG_NAME = 'local_download.sucatalog'
|
||||
|
||||
# Catalog with all URLs (including package URLs) pointed to local cache.
|
||||
# We use this one during install phase.
|
||||
# This causes softwareupdate -i -a to fail cleanly if we don't
|
||||
# have the required packages already downloaded.
|
||||
LOCAL_CATALOG_NAME = 'local_install.sucatalog'
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Class for domain specific exceptions."""
|
||||
|
||||
|
||||
class ReplicationError(Error):
|
||||
"""A custom error when replication fails."""
|
||||
|
||||
|
||||
class CatalogNotFoundError(Error):
|
||||
"""A catalog was not found."""
|
||||
|
||||
|
||||
class AppleUpdateSync(object):
|
||||
'''Object that handles local replication of Apple Software Update data'''
|
||||
|
||||
def __init__(self):
|
||||
'''Set 'em all up '''
|
||||
real_cache_dir = os.path.join(prefs.pref('ManagedInstallDir'), 'swupd')
|
||||
if os.path.exists(real_cache_dir):
|
||||
if not os.path.isdir(real_cache_dir):
|
||||
display.display_error(
|
||||
'%s exists but is not a dir.', real_cache_dir)
|
||||
else:
|
||||
os.mkdir(real_cache_dir)
|
||||
|
||||
# symlink to work around an issue with paths containing spaces
|
||||
# in 10.8.2's SoftwareUpdate
|
||||
self.cache_dir = os.path.join('/tmp', 'munki_swupd_cache')
|
||||
try:
|
||||
if os.path.islink(self.cache_dir):
|
||||
# remove any pre-existing symlink
|
||||
os.unlink(self.cache_dir)
|
||||
if os.path.exists(self.cache_dir):
|
||||
# there should not be a file or directory at that path!
|
||||
# move it
|
||||
new_name = os.path.join(
|
||||
'/tmp', ('munki_swupd_cache_moved_%s'
|
||||
% time.strftime('%Y.%m.%d.%H.%M.%S')))
|
||||
os.rename(self.cache_dir, new_name)
|
||||
os.symlink(real_cache_dir, self.cache_dir)
|
||||
except (OSError, IOError) as err:
|
||||
# error in setting up the cache directories
|
||||
raise Error('Could not configure cache directory: %s' % err)
|
||||
|
||||
self.temp_cache_dir = os.path.join(self.cache_dir, 'mirror')
|
||||
self.local_catalog_dir = os.path.join(
|
||||
self.cache_dir, LOCAL_CATALOG_DIR_REL_PATH)
|
||||
|
||||
self.apple_download_catalog_path = os.path.join(
|
||||
self.temp_cache_dir, APPLE_DOWNLOAD_CATALOG_NAME)
|
||||
|
||||
self.filtered_catalog_path = os.path.join(
|
||||
self.local_catalog_dir, FILTERED_CATALOG_NAME)
|
||||
self.local_catalog_path = os.path.join(
|
||||
self.local_catalog_dir, LOCAL_CATALOG_NAME)
|
||||
self.extracted_catalog_path = os.path.join(
|
||||
self.local_catalog_dir, APPLE_EXTRACTED_CATALOG_NAME)
|
||||
self.local_download_catalog_path = os.path.join(
|
||||
self.local_catalog_dir, LOCAL_DOWNLOAD_CATALOG_NAME)
|
||||
|
||||
def GetAppleCatalogURL(self):
|
||||
"""Returns the catalog URL of the Apple SU catalog for the current Mac.
|
||||
|
||||
Returns:
|
||||
String catalog URL for the current Mac.
|
||||
Raises:
|
||||
CatalogNotFoundError: an Apple catalog was not found for this Mac.
|
||||
"""
|
||||
os_version_tuple = osutils.getOsVersion(as_tuple=True)
|
||||
# Prefer Munki's preferences file in OS X <= 10.10
|
||||
munkisuscatalog = prefs.pref('SoftwareUpdateServerURL')
|
||||
if munkisuscatalog:
|
||||
if os_version_tuple < (10, 11):
|
||||
# only pay attention to Munki's SoftwareUpdateServerURL pref
|
||||
# in 10.10 and earlier
|
||||
return munkisuscatalog
|
||||
|
||||
# Otherwise prefer MCX or /Library/Preferences/com.apple.SoftwareUpdate
|
||||
prefs_catalog_url = su_prefs.GetSoftwareUpdatePref('CatalogURL')
|
||||
if prefs_catalog_url:
|
||||
return prefs_catalog_url
|
||||
|
||||
# Finally, fall back to using a hard-coded url in DEFAULT_CATALOG_URLS.
|
||||
os_version = osutils.getOsVersion()
|
||||
catalog_url = DEFAULT_CATALOG_URLS.get(os_version, None)
|
||||
if catalog_url:
|
||||
return catalog_url
|
||||
|
||||
raise CatalogNotFoundError(
|
||||
'No default Software Update CatalogURL for: %s' % os_version)
|
||||
|
||||
def ExtractAndCopyDownloadedCatalog(self, _open=open):
|
||||
"""Copy the downloaded catalog to a new file, extracting if gzipped.
|
||||
|
||||
Args:
|
||||
_open: func, default builtin open(), open method for unit testing.
|
||||
"""
|
||||
if not os.path.exists(self.local_catalog_dir):
|
||||
try:
|
||||
os.makedirs(self.local_catalog_dir)
|
||||
except OSError as oserr:
|
||||
raise ReplicationError(oserr)
|
||||
|
||||
local_apple_sus_catalog = os.path.join(
|
||||
self.local_catalog_dir, APPLE_EXTRACTED_CATALOG_NAME)
|
||||
|
||||
fileref = _open(self.apple_download_catalog_path, 'rb')
|
||||
magic = fileref.read(2)
|
||||
contents = ''
|
||||
if magic == '\x1f\x8b': # File is gzip compressed.
|
||||
fileref.close() # Close the open handle first.
|
||||
fileref = gzip.open(self.apple_download_catalog_path, 'rb')
|
||||
else: # Hopefully a nice plain plist.
|
||||
fileref.seek(0)
|
||||
contents = fileref.read()
|
||||
fileref.close()
|
||||
fileref = _open(local_apple_sus_catalog, 'wb')
|
||||
fileref.write(contents)
|
||||
fileref.close()
|
||||
|
||||
def CacheAppleCatalog(self):
|
||||
"""Caches a local copy of the current Apple SUS catalog.
|
||||
|
||||
Raises:
|
||||
CatalogNotFoundError: a catalog was not found to cache.
|
||||
ReplicationError: there was an error making the cache directory.
|
||||
fetch.MunkiDownloadError: error downloading the catalog.
|
||||
"""
|
||||
try:
|
||||
catalog_url = self.GetAppleCatalogURL()
|
||||
except CatalogNotFoundError as err:
|
||||
display.display_error(unicode(err))
|
||||
raise
|
||||
if not os.path.exists(self.temp_cache_dir):
|
||||
try:
|
||||
os.makedirs(self.temp_cache_dir)
|
||||
except OSError as oserr:
|
||||
raise ReplicationError(oserr)
|
||||
display.display_detail('Caching CatalogURL %s', catalog_url)
|
||||
try:
|
||||
dummy_file_changed = self.GetSoftwareUpdateResource(
|
||||
catalog_url, self.apple_download_catalog_path, resume=True)
|
||||
self.ExtractAndCopyDownloadedCatalog()
|
||||
except fetch.Error:
|
||||
raise
|
||||
|
||||
def _GetURLPath(self, full_url):
|
||||
"""Returns only the URL path.
|
||||
|
||||
Args:
|
||||
full_url: a str URL, complete with schema, domain, path, etc.
|
||||
Returns:
|
||||
The str path of the URL.
|
||||
"""
|
||||
# pylint: disable=no-self-use
|
||||
return urlparse.urlsplit(full_url)[2] # (schema, netloc, path, ...)
|
||||
|
||||
def RewriteURL(self, full_url):
|
||||
"""Rewrites a single URL to point to our local replica.
|
||||
|
||||
Args:
|
||||
full_url: a str URL, complete with schema, domain, path, etc.
|
||||
Returns:
|
||||
A str URL, rewritten if needed to point to the local cache.
|
||||
"""
|
||||
local_base_url = 'file://localhost' + urllib2.quote(
|
||||
self.cache_dir)
|
||||
if full_url.startswith(local_base_url):
|
||||
return full_url # url is already local, so just return it.
|
||||
return local_base_url + self._GetURLPath(full_url)
|
||||
|
||||
def RewriteProductURLs(self, product, rewrite_pkg_urls=False):
|
||||
"""Rewrites URLs in the product to point to our local cache.
|
||||
|
||||
Args:
|
||||
product: list, of dicts, product info. This dict is changed by
|
||||
this function.
|
||||
rewrite_pkg_urls: bool, default False, if True package URLs are
|
||||
rewritten, otherwise only MetadataURLs are rewritten.
|
||||
"""
|
||||
if 'ServerMetadataURL' in product:
|
||||
product['ServerMetadataURL'] = self.RewriteURL(
|
||||
product['ServerMetadataURL'])
|
||||
for package in product.get('Packages', []):
|
||||
if rewrite_pkg_urls and 'URL' in package:
|
||||
package['URL'] = self.RewriteURL(package['URL'])
|
||||
if 'MetadataURL' in package:
|
||||
package['MetadataURL'] = self.RewriteURL(
|
||||
package['MetadataURL'])
|
||||
distributions = product['Distributions']
|
||||
for dist_lang in distributions.keys():
|
||||
distributions[dist_lang] = self.RewriteURL(
|
||||
distributions[dist_lang])
|
||||
|
||||
def RewriteCatalogURLs(self, catalog, rewrite_pkg_urls=False):
|
||||
"""Rewrites URLs in a catalog to point to our local replica.
|
||||
|
||||
Args:
|
||||
rewrite_pkg_urls: Boolean, if True package URLs are rewritten,
|
||||
otherwise only MetadataURLs are rewritten.
|
||||
"""
|
||||
if not 'Products' in catalog:
|
||||
return
|
||||
|
||||
for product_key in catalog['Products'].keys():
|
||||
product = catalog['Products'][product_key]
|
||||
self.RewriteProductURLs(product, rewrite_pkg_urls=rewrite_pkg_urls)
|
||||
|
||||
def RetrieveURLToCacheDir(self, full_url, copy_only_if_missing=False):
|
||||
"""Downloads a URL and stores it in the same relative path on our
|
||||
filesystem. Returns a path to the replicated file.
|
||||
|
||||
Args:
|
||||
full_url: str, full URL to retrieve.
|
||||
copy_only_if_missing: boolean, True to copy only if the file is not
|
||||
already cached, False to copy regardless of existence in cache.
|
||||
Returns:
|
||||
String path to the locally cached file.
|
||||
"""
|
||||
relative_url = os.path.normpath(self._GetURLPath(full_url).lstrip('/'))
|
||||
local_file_path = os.path.join(
|
||||
self.cache_dir, relative_url)
|
||||
local_dir_path = os.path.dirname(local_file_path)
|
||||
if copy_only_if_missing and os.path.exists(local_file_path):
|
||||
return local_file_path
|
||||
if not os.path.exists(local_dir_path):
|
||||
try:
|
||||
os.makedirs(local_dir_path)
|
||||
except OSError as oserr:
|
||||
raise ReplicationError(oserr)
|
||||
try:
|
||||
self.GetSoftwareUpdateResource(
|
||||
full_url, local_file_path, resume=True)
|
||||
except fetch.Error as err:
|
||||
raise ReplicationError(err)
|
||||
return local_file_path
|
||||
|
||||
def GetSoftwareUpdateResource(self, url, destinationpath, resume=False):
|
||||
"""Gets item from Apple Software Update Server.
|
||||
|
||||
Args:
|
||||
url: str, URL of the resource to download.
|
||||
destinationpath: str, path of the destination to save the resource.
|
||||
resume: boolean, True to resume downloads, False to redownload.
|
||||
Returns:
|
||||
Boolean. True if a new download was required, False if the item was
|
||||
already in the local cache.
|
||||
"""
|
||||
# pylint: disable=no-self-use
|
||||
machine = info.getMachineFacts()
|
||||
darwin_version = os.uname()[2]
|
||||
# Set the User-Agent header to match that used by Apple's
|
||||
# softwareupdate client for better compatibility.
|
||||
user_agent_header = (
|
||||
"User-Agent: managedsoftwareupdate/%s Darwin/%s (%s) (%s)"
|
||||
% (machine['munki_version'], darwin_version,
|
||||
machine['arch'], machine['machine_model']))
|
||||
return fetch.getResourceIfChangedAtomically(
|
||||
url, destinationpath, custom_headers=[user_agent_header],
|
||||
resume=resume, follow_redirects=True)
|
||||
|
||||
def CacheUpdateMetadata(self, product_ids):
|
||||
"""Copies ServerMetadata (.smd), Metadata (.pkm), and
|
||||
Distribution (.dist) files for the available updates to the local
|
||||
machine and writes a new sucatalog that refers to the local copies
|
||||
of these files."""
|
||||
catalog = FoundationPlist.readPlist(
|
||||
self.extracted_catalog_path)
|
||||
if not 'Products' in catalog:
|
||||
display.display_warning(
|
||||
'"Products" not found in %s',
|
||||
self.extracted_catalog_path)
|
||||
return
|
||||
|
||||
for product_key in product_ids:
|
||||
if processes.stopRequested():
|
||||
break
|
||||
display.display_status_minor(
|
||||
'Caching metadata for product ID %s', product_key)
|
||||
product = catalog['Products'][product_key]
|
||||
if 'ServerMetadataURL' in product:
|
||||
self.RetrieveURLToCacheDir(
|
||||
product['ServerMetadataURL'], copy_only_if_missing=True)
|
||||
|
||||
for package in product.get('Packages', []):
|
||||
if processes.stopRequested():
|
||||
break
|
||||
if 'MetadataURL' in package:
|
||||
display.display_status_minor(
|
||||
'Caching package metadata for product ID %s',
|
||||
product_key)
|
||||
self.RetrieveURLToCacheDir(
|
||||
package['MetadataURL'], copy_only_if_missing=True)
|
||||
# if 'URL' in package:
|
||||
# display.display_status_minor(
|
||||
# 'Caching package for product ID %s',
|
||||
# product_key)
|
||||
# self.RetrieveURLToCacheDir(
|
||||
# package['URL'], copy_only_if_missing=True)
|
||||
|
||||
distributions = product['Distributions']
|
||||
for dist_lang in distributions.keys():
|
||||
if processes.stopRequested():
|
||||
break
|
||||
display.display_status_minor(
|
||||
'Caching %s distribution for product ID %s',
|
||||
dist_lang, product_key)
|
||||
dist_url = distributions[dist_lang]
|
||||
try:
|
||||
self.RetrieveURLToCacheDir(
|
||||
dist_url, copy_only_if_missing=True)
|
||||
except ReplicationError:
|
||||
display.display_warning(
|
||||
'Could not cache %s distribution for product ID %s',
|
||||
dist_lang, product_key)
|
||||
|
||||
if processes.stopRequested():
|
||||
return
|
||||
|
||||
if not os.path.exists(self.local_catalog_dir):
|
||||
try:
|
||||
os.makedirs(self.local_catalog_dir)
|
||||
except OSError as oserr:
|
||||
raise ReplicationError(oserr)
|
||||
|
||||
# rewrite metadata URLs to point to local caches.
|
||||
self.RewriteCatalogURLs(catalog, rewrite_pkg_urls=False)
|
||||
FoundationPlist.writePlist(
|
||||
catalog, self.local_download_catalog_path)
|
||||
|
||||
# rewrite all URLs, including pkgs, to point to local caches.
|
||||
self.RewriteCatalogURLs(catalog, rewrite_pkg_urls=True)
|
||||
FoundationPlist.writePlist(
|
||||
catalog, self.local_catalog_path)
|
||||
|
||||
def _GetPreferredLocalization(self, list_of_localizations):
|
||||
'''Picks the best localization from a list of available
|
||||
localizations. Returns a single language/localization name.'''
|
||||
# pylint: disable=no-self-use
|
||||
localization_preferences = (
|
||||
prefs.pref('AppleSoftwareUpdateLanguages') or ['English'])
|
||||
preferred_langs = (
|
||||
NSBundle.preferredLocalizationsFromArray_forPreferences_(
|
||||
list_of_localizations, localization_preferences))
|
||||
if preferred_langs:
|
||||
return preferred_langs[0]
|
||||
|
||||
# first fallback, return en or English
|
||||
if 'English' in list_of_localizations:
|
||||
return 'English'
|
||||
elif 'en' in list_of_localizations:
|
||||
return 'en'
|
||||
|
||||
# if we get this far, just return the first language
|
||||
# in the list of available languages
|
||||
return list_of_localizations[0]
|
||||
|
||||
def GetDistributionForProductKey(self, product_key, language=None):
|
||||
'''Returns the path to a distibution file from /Library/Updates
|
||||
or the local cache for the given product_key. If language is
|
||||
defined it will try to retrieve that specific language, otherwise
|
||||
it will use the available languages and the value of the
|
||||
AppleSoftwareUpdateLanguages preference to return the "best"
|
||||
language of those available.'''
|
||||
os_version_tuple = osutils.getOsVersion(as_tuple=True)
|
||||
if os_version_tuple < (10, 11):
|
||||
# use our filtered catalog
|
||||
sucatalog = self.local_catalog_path
|
||||
else:
|
||||
# use the cached Apple catalog
|
||||
sucatalog = self.extracted_catalog_path
|
||||
try:
|
||||
catalog = FoundationPlist.readPlist(sucatalog)
|
||||
except FoundationPlist.NSPropertyListSerializationException:
|
||||
return None
|
||||
product = catalog.get('Products', {}).get(product_key, {})
|
||||
if product:
|
||||
distributions = product.get('Distributions', {})
|
||||
if distributions:
|
||||
available_languages = distributions.keys()
|
||||
if language:
|
||||
preferred_language = language
|
||||
else:
|
||||
preferred_language = self._GetPreferredLocalization(
|
||||
available_languages)
|
||||
url = distributions[preferred_language]
|
||||
# do we already have it in /Library/Updates?
|
||||
filename = os.path.basename(self._GetURLPath(url))
|
||||
dist_path = os.path.join(
|
||||
'/Library/Updates', product_key, filename)
|
||||
if os.path.exists(dist_path):
|
||||
return dist_path
|
||||
# look for it in the cache
|
||||
if url.startswith('file://localhost'):
|
||||
fileurl = url[len('file://localhost'):]
|
||||
dist_path = urllib2.unquote(fileurl)
|
||||
if os.path.exists(dist_path):
|
||||
return dist_path
|
||||
# we haven't downloaded this yet
|
||||
try:
|
||||
return self.RetrieveURLToCacheDir(
|
||||
url, copy_only_if_missing=True)
|
||||
except ReplicationError, err:
|
||||
display.display_error(
|
||||
'Could not retrieve %s: %s', url, err)
|
||||
return None
|
||||
|
||||
def WriteFilteredCatalog(self, product_ids):
|
||||
"""Write out a sucatalog containing only the updates in product_ids.
|
||||
|
||||
Args:
|
||||
product_ids: list of str, ProductIDs.
|
||||
catalog_path: str, path of catalog to write.
|
||||
"""
|
||||
catalog = FoundationPlist.readPlist(self.extracted_catalog_path)
|
||||
product_ids = set(product_ids) # convert to set for O(1) lookups.
|
||||
for product_id in list(catalog.get('Products', [])):
|
||||
if product_id not in product_ids:
|
||||
del catalog['Products'][product_id]
|
||||
FoundationPlist.writePlist(catalog, self.filtered_catalog_path)
|
||||
|
||||
def clean_up_cache(self):
|
||||
"""Clean up our cache dir"""
|
||||
if os.path.exists(self.cache_dir):
|
||||
# TODO(unassigned): change this to Pythonic delete.
|
||||
dummy_retcode = subprocess.call(['/bin/rm', '-rf', self.cache_dir])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print 'This is a library of support tools for the Munki Suite.'
|
||||
Reference in New Issue
Block a user