Merge branch 'master' into development

* master:
  Fix makepkginfo --force_install_after_date to create a date object for the pkginfo plist instead of a string.
  If a force_install_after_date install is near/past it's due date, logouthelper should be started (the machine should be forcefully rebooted) even if someone is logged in but the session is at the loginwindow (fast user switching).  Force should not be "force unless x, y, z".
  Add --repo_path and --repo_url options to munkiimport to allow the user to override the default repo_path and repo_url options set via munkiimport --configure at runtime
  If we are about to do an install at the loginwindow, check to see if FileSyncAgent.app is running. This might be HomeSync running during a login process. If so, don't install.
  Change apple_item logic in updatecheck.processRemoval to match that in updatecheck.processInstall. Specifically, allow admin to override detection of apple_item by explictly setting it in the pkginfo.
  Update version.plist to 0.9.0 for next development round.
This commit is contained in:
Hannes Juutilainen
2013-05-03 08:22:33 +03:00
5 changed files with 107 additions and 62 deletions

View File

@@ -36,16 +36,39 @@ import sys
import os
import re
import optparse
import time
from optparse import OptionValueError
from munkilib import munkicommon
from munkilib import FoundationPlist
from munkilib import adobeutils
from Foundation import NSDate
# circumvent cfprefsd plist scanning
os.environ['__CFPREFERENCES_AVOID_DAEMON'] = "1"
def convertDateStringToNSDate(datetime_string):
'''Converts a string in the "2013-04-25T20:00:00Z" format or
"2013-04-25 20:00:00 +0000" format to an NSDate'''
NSDateFormat = '%Y-%m-%dT%H:%M:%SZ'
ISOFormat = '%Y-%m-%d %H:%M:%S +0000'
FallBackFormat = '%Y-%m-%d %H:%M:%S'
try:
dt = time.strptime(datetime_string, NSDateFormat)
except ValueError:
try:
dt = time.strptime(datetime_string, ISOFormat)
except ValueError:
try:
dt = time.strptime(datetime_string, FallBackFormat)
except ValueError:
return None
iso_date_string = time.strftime(ISOFormat, dt)
return NSDate.dateWithString_(iso_date_string)
def getCatalogInfoFromDmg(dmgpath, options):
"""
* Mounts a disk image if it's not already mounted
@@ -932,10 +955,13 @@ def main():
if options.maximum_os_version:
catinfo['maximum_os_version'] = options.maximum_os_version
if options.force_install_after_date:
force_install_after_date = (
munkicommon.validateDateFormat(options.force_install_after_date))
if force_install_after_date:
catinfo['force_install_after_date'] = force_install_after_date
date_obj = convertDateStringToNSDate(options.force_install_after_date)
if date_obj:
catinfo['force_install_after_date'] = date_obj
else:
print >> sys.stderr, (
"Invalid date format %s for force_install_after_date"
% options.force_install_after_date)
if options.RestartAction:
validActions = ['RequireRestart', 'RequireLogout', 'RecommendRestart']
if options.RestartAction in validActions:

View File

@@ -268,11 +268,7 @@ def doRestart():
else:
munkicommon.display_info(restartMessage)
# TODO: temporary fix for forced logout problem where we've killed
# loginwindow sessions, but munkicommon.currentGUIusers() still returns
# users. Need to find a better solution, though.
#if not munkicommon.currentGUIusers():
# # no-one is logged in and we're at the loginwindow
# check current console user
consoleuser = munkicommon.getconsoleuser()
if not consoleuser or consoleuser == u'loginwindow':
# no-one is logged in or we're at the loginwindow
@@ -502,7 +498,7 @@ def main():
# network interfaces to come up
# before continuing
munkicommon.display_status_minor('Waiting for network...')
for i in range(5):
for unused_i in range(5):
if networkUp():
break
time.sleep(2)
@@ -786,6 +782,11 @@ def main():
munkicommon.log('Skipping auto install at loginwindow '
'because system is not idle '
'(keyboard or mouse activity).')
elif munkicommon.isAppRunning(
'/System/Library/CoreServices/FileSyncAgent.app'):
munkicommon.log('Skipping auto install at loginwindow '
'because FileSyncAgent.app is running '
'(HomeSyncing a mobile account on login?).')
else:
# no GUI users, system is idle, so we can install
# but first, enable status output over login window
@@ -877,16 +878,15 @@ def main():
# original updatecheck so tickle the updatecheck time
# so MSU.app knows to display results immediately
recordUpdateCheckResult(1)
consoleuser = munkicommon.getconsoleuser()
if consoleuser == u'loginwindow':
if force_action:
notifyUserOfUpdates(force=True)
time.sleep(2)
startLogoutHelper()
elif munkicommon.getconsoleuser() == u'loginwindow':
# someone is logged in, but we're sitting at
# the loginwindow due to fast user switching
# so do nothing
pass
elif force_action:
notifyUserOfUpdates(force=True)
time.sleep(2)
startLogoutHelper()
elif not munkicommon.pref('SuppressUserNotification'):
notifyUserOfUpdates()
else:

View File

@@ -97,17 +97,16 @@ def repoAvailable():
"""Checks the repo path for proper directory structure.
If the directories look wrong we probably don't have a
valid repo path. Returns True if things look OK."""
repo_path = pref('repo_path')
if not repo_path:
if not REPO_PATH:
print >> sys.stderr, 'No repo path specified.'
return False
if not os.path.exists(repo_path):
if not os.path.exists(REPO_PATH):
mountRepoCLI()
if not os.path.exists(repo_path):
if not os.path.exists(REPO_PATH):
return False
for subdir in ['catalogs', 'manifests', 'pkgs', 'pkgsinfo']:
if not os.path.exists(os.path.join(repo_path, subdir)):
print >> sys.stderr, "%s is missing %s" % (repo_path, subdir)
if not os.path.exists(os.path.join(REPO_PATH, subdir)):
print >> sys.stderr, "%s is missing %s" % (REPO_PATH, subdir)
return False
# if we get this far, the repo path looks OK
return True
@@ -116,16 +115,14 @@ def repoAvailable():
def mountRepoGUI():
"""Attempts to connect to the repo fileshare
Returns nothing whether we succeed or fail"""
repo_path = pref('repo_path')
repo_url = pref('repo_url')
if not repo_path or not repo_url:
if not REPO_PATH or not REPO_URL:
return
print 'Attempting to connect to munki repo...'
cmd = ['/usr/bin/open', repo_url]
cmd = ['/usr/bin/open', REPO_URL]
unused_retcode = subprocess.call(cmd)
for unused_i in range(60):
# wait up to 60 seconds to connect to repo
if os.path.exists(repo_path):
if os.path.exists(REPO_PATH):
break
time.sleep(1)
@@ -133,34 +130,31 @@ def mountRepoGUI():
def mountRepoCLI():
"""Attempts to connect to the repo fileshare"""
global WE_MOUNTED_THE_REPO
repo_path = pref('repo_path')
repo_url = pref('repo_url')
if os.path.exists(repo_path):
if os.path.exists(REPO_PATH):
return
os.mkdir(repo_path)
print 'Attempting to mount fileshare %s:' % repo_url
if repo_url.startswith('afp:'):
cmd = ['/sbin/mount_afp', '-i', repo_url, repo_path]
elif repo_url.startswith('smb:'):
cmd = ['/sbin/mount_smbfs', repo_url[4:], repo_path]
elif repo_url.startswith('nfs://'):
cmd = ['/sbin/mount_nfs', repo_url[6:], repo_path]
os.mkdir(REPO_PATH)
print 'Attempting to mount fileshare %s:' % REPO_URL
if REPO_URL.startswith('afp:'):
cmd = ['/sbin/mount_afp', '-i', REPO_URL, REPO_PATH]
elif REPO_URL.startswith('smb:'):
cmd = ['/sbin/mount_smbfs', REPO_URL[4:], REPO_PATH]
elif REPO_URL.startswith('nfs://'):
cmd = ['/sbin/mount_nfs', REPO_URL[6:], REPO_PATH]
else:
print >> sys.stderr, 'Unsupported filesystem URL!'
return
retcode = subprocess.call(cmd)
if retcode:
os.rmdir(repo_path)
os.rmdir(REPO_PATH)
else:
WE_MOUNTED_THE_REPO = True
def unmountRepoCLI():
"""Attempts to unmount the repo fileshare"""
repo_path = pref('repo_path')
if not os.path.exists(repo_path):
if not os.path.exists(REPO_PATH):
return
cmd = ['/sbin/umount', repo_path]
cmd = ['/sbin/umount', REPO_PATH]
return subprocess.call(cmd)
@@ -175,11 +169,10 @@ def copyItemToRepo(itempath, vers, subdirectory=''):
Renames the item if an item already exists with that name.
Returns the relative path to the item."""
repo_path = pref('repo_path')
if not os.path.exists(repo_path):
if not os.path.exists(REPO_PATH):
raise RepoCopyError('Could not connect to munki repo.')
destination_path = os.path.join(repo_path, 'pkgs', subdirectory)
destination_path = os.path.join(REPO_PATH, 'pkgs', subdirectory)
if not os.path.exists(destination_path):
try:
os.makedirs(destination_path)
@@ -227,8 +220,7 @@ def copyPkginfoToRepo(pkginfo, subdirectory=''):
"""Saves pkginfo to munki_repo_path/pkgsinfo/subdirectory"""
# less error checking because we copy the installer_item
# first and bail if it fails...
repo_path = pref('repo_path')
destination_path = os.path.join(repo_path, 'pkgsinfo', subdirectory)
destination_path = os.path.join(REPO_PATH, 'pkgsinfo', subdirectory)
if not os.path.exists(destination_path):
try:
os.makedirs(destination_path)
@@ -278,13 +270,12 @@ def promptForSubdirectory(subdirectory):
'Upload item to subdirectory path [%s]: '
% subdirectory)
if newdir:
repo_path = pref('repo_path')
if not repoAvailable():
raise RepoCopyError('Could not connect to munki repo.')
if APPLEMETADATA:
destination_path = os.path.join(repo_path, 'pkgsinfo', newdir)
destination_path = os.path.join(REPO_PATH, 'pkgsinfo', newdir)
else:
destination_path = os.path.join(repo_path, 'pkgs', newdir)
destination_path = os.path.join(REPO_PATH, 'pkgs', newdir)
if not os.path.exists(destination_path):
answer = raw_input('Path %s doesn\'t exist. Create it? [y/n] '
% destination_path)
@@ -305,7 +296,7 @@ class CatalogDBException(Exception):
def makeCatalogDB():
"""Returns a dict we can use like a database"""
all_items_path = os.path.join(pref('repo_path'), 'catalogs', 'all')
all_items_path = os.path.join(REPO_PATH, 'catalogs', 'all')
if not os.path.exists(all_items_path):
raise CatalogDBException
try:
@@ -498,12 +489,11 @@ def makeCatalogs():
if not os.path.exists(makecatalogs_path):
# didn't find it; assume the default install path
makecatalogs_path = '/usr/local/munki/makecatalogs'
repo_path = pref('repo_path')
if not repoAvailable():
raise RepoCopyError('Could not connect to munki repo.')
if not VERBOSE:
print 'Rebuilding catalogs at %s...' % repo_path
proc = subprocess.Popen([makecatalogs_path, repo_path],
print 'Rebuilding catalogs at %s...' % REPO_PATH
proc = subprocess.Popen([makecatalogs_path, REPO_PATH],
bufsize=-1, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
while True:
@@ -570,12 +560,16 @@ APPLEMETADATA = False
NOINTERACTIVE = False
WE_MOUNTED_THE_REPO = False
VERBOSE = False
REPO_PATH = ""
REPO_URL = ""
def main():
"""Main routine"""
global APPLEMETADATA
global NOINTERACTIVE
global VERBOSE
global REPO_PATH
global REPO_URL
usage = """usage: %prog [options] /path/to/installer_item
Imports an installer item into a munki repo.
@@ -608,6 +602,12 @@ def main():
parser.add_option('--nointeractive', '-n', action='store_true',
help="""No interactive prompts. May cause a failure
if repo path is unavailable.""")
parser.add_option('--repo_path', '--repo-path', default='',
help="""Optional path to munki repo that takes precedence
over the default repo_path specified via --configure.""")
parser.add_option('--repo_url', '--repo-url', default='',
help="""Optional repo fileshare URL that takes precedence
over the default repo_url specified via --configure.""")
parser.add_option('--version', '-V', action='store_true',
help='Print the version of the munki tools and exit.')
parser.add_option('--verbose', '-v', action='store_true',
@@ -625,7 +625,22 @@ def main():
NOINTERACTIVE = options.nointeractive
VERBOSE = options.verbose
REPO_PATH = pref('repo_path')
REPO_URL = pref('repo_url')
if options.repo_path:
if not os.path.exists(options.repo_path) and not options.repo_url:
print >> sys.stderr, ('Munki repo path override provided but '
'folder does not exist. Please either '
'provide --repo_url if you wish to map a '
'share, or correct the path and try again.')
exit(-1)
REPO_PATH = options.repo_path
if options.repo_url:
REPO_URL = options.repo_url
if len(arguments) == 0:
parser.print_usage()
exit(0)
@@ -661,10 +676,10 @@ def main():
print >> sys.stderr, '%s does not exist!' % installer_item
exit(-1)
if not pref('repo_path'):
if not REPO_PATH:
print >> sys.stderr, ('Path to munki repo has not been defined. '
'Run with --configure option to configure this '
'tool.')
'tool, or provide with --repo-path')
exit(-1)
if not repoAvailable():
@@ -791,7 +806,7 @@ def main():
cleanupAndExit(0)
if options.subdirectory == '':
pkgs_path = os.path.join(pref('repo_path'), 'pkgs')
pkgs_path = os.path.join(REPO_PATH, 'pkgs')
if installer_item.startswith(pkgs_path) and not APPLEMETADATA:
# the installer item is already in the repo.
# use its relative path as the subdirectory

View File

@@ -2212,13 +2212,17 @@ def processRemoval(manifestitem, cataloglist, installinfo):
'requires',
'update_for',
'preuninstall_script',
'postuninstall_script']
'postuninstall_script',
'apple_item']
for key in optionalKeys:
if key in uninstall_item:
iteminfo[key] = uninstall_item[key]
if isAppleItem(item_pl):
iteminfo['apple_item'] = True
if not 'apple_item' in iteminfo:
# admin did not explictly mark this item; let's determine if
# it's from Apple
if isAppleItem(item_pl):
iteminfo['apple_item'] = True
if packagesToRemove:
# remove references for each package

View File

@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>CFBundleShortVersionString</key>
<string>0.8.4</string>
<string>0.9.0</string>
</dict>
</plist>