Add support for importing, installing and removing configuration profiles.

This commit is contained in:
Greg Neagle
2014-12-16 21:57:41 -08:00
parent 2afd2606a8
commit 83fabfb5ce
6 changed files with 277 additions and 8 deletions

View File

@@ -113,6 +113,28 @@ def getCatalogInfoFromPath(pkgpath, options):
return cataloginfo
def getCatalogInfoForProfile(profile_path):
'''Populates some metadata for profile pkginfo'''
cataloginfo = {}
try:
profile = FoundationPlist.readPlist(profile_path)
except FoundationPlist.NSPropertyListSerializationException:
pass
if profile.get('PayloadType') == 'configuration':
cataloginfo['name'] = os.path.basename(profile_path)
cataloginfo['display_name'] = profile.get(
'PayloadDisplayName', cataloginfo['name'])
cataloginfo['description'] = profile.get('PayloadDescription')
cataloginfo['PayloadIdentifier'] = profile.get('PayloadIdentifier')
cataloginfo['version'] = '1.0'
cataloginfo['installer_type'] = 'profile'
cataloginfo['uninstall_method'] = 'remove_profile'
cataloginfo['unattended_install'] = True
cataloginfo['unattended_uninstall'] = True
cataloginfo['minimum_os_version'] = '10.7'
return cataloginfo
def getCatalogInfoFromDmg(dmgpath, options):
"""
* Mounts a disk image if it's not already mounted
@@ -834,6 +856,9 @@ def main():
# convert to kbytes
itemsize = int(itemsize/1024)
elif munkicommon.hasValidConfigProfileExt(item):
catinfo = getCatalogInfoForProfile(item)
else:
print >> sys.stderr, "%s is not a valid installer item!" % item
exit(-1)
@@ -1072,7 +1097,7 @@ def main():
catinfo['display_name'] = options.displayname
catinfo['installer_type'] = 'apple_update_metadata'
# add user/environment metadata
# add user/environment metadata
catinfo['_metadata'] = make_pkginfo_metadata()
# and now, what we've all been waiting for...

View File

@@ -80,7 +80,7 @@ def makeDMG(pkgpath):
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
while True:
output = proc.stdout.readline()
output = proc.stdout.readline()
if not output and (proc.poll() != None):
break
print output.rstrip('\n').encode('UTF-8')
@@ -431,6 +431,7 @@ def makeCatalogDB():
app_table = {}
installer_item_table = {}
hash_table = {}
profile_table = {}
itemindex = -1
for item in catalogitems:
@@ -451,6 +452,10 @@ def makeCatalogDB():
if 'installer_item_location' in item:
installer_item_name = os.path.basename(
item['installer_item_location'])
(name, ext) = os.path.splitext(installer_item_name)
if '-' in name:
(name, vers) = munkicommon.nameAndVersion(name)
installer_item_name = name + ext
if not installer_item_name in installer_item_table:
installer_item_table[installer_item_name] = {}
if not vers in installer_item_table[installer_item_name]:
@@ -486,11 +491,20 @@ def makeCatalogDB():
'Bad install data for %s-%s: %s'
% (name, vers, install))
# add to table of PayloadIdentifiers
if 'PayloadIdentifier' in item:
if not item['PayloadIdentifier'] in profile_table:
profile_table[item['PayloadIdentifier']] = {}
if not vers in profile_table[item['PayloadIdentifier']]:
profile_table[item['PayloadIdentifier']][vers] = []
profile_table[item['PayloadIdentifier']][vers].append(itemindex)
pkgdb = {}
pkgdb['hashes'] = hash_table
pkgdb['receipts'] = pkgid_table
pkgdb['applications'] = app_table
pkgdb['installer_items'] = installer_item_table
pkgdb['profiles'] = profile_table
pkgdb['items'] = catalogitems
return pkgdb
@@ -549,6 +563,15 @@ def findMatchingPkginfo(pkginfo):
indexes = catdb['applications'][app][versionlist[0]]
return catdb['items'][indexes[0]]
if 'PayloadIdentifier' in pkginfo:
identifier = pkginfo['PayloadIdentifier']
possiblematches = catdb['profiles'].get(identifier)
if possiblematches:
versionlist = possiblematches.keys()
versionlist.sort(compare_version_keys)
indexes = catdb['profiles'][identifier][versionlist[0]]
return catdb['items'][indexes[0]]
# no matches by receipts or installed applications,
# let's try to match based on installer_item_name
installer_item_name = os.path.basename(
@@ -878,7 +901,6 @@ def main():
# makepkginfo returned an error
print >> sys.stderr, 'Getting package info failed.'
cleanupAndExit(-1)
if not options.nointeractive:
# try to find existing pkginfo items that match this one
matchingpkginfo = findMatchingPkginfo(pkginfo)

View File

@@ -31,6 +31,7 @@ import adobeutils
import launchd
import munkicommon
import munkistatus
import profiles
import updatecheck
import FoundationPlist
from removepackages import removepackages
@@ -713,6 +714,8 @@ def installWithInfo(
munkicommon.display_warning(
"install_type 'appdmg' is deprecated. Use 'copy_from_dmg'.")
retcode = copyAppFromDMG(itempath)
elif installer_type == 'profile':
retcode = profiles.install_profile(itempath)
elif installer_type == "nopkg": # Packageless install
if (item.get("RestartAction") == "RequireRestart" or
item.get("RestartAction") == "RecommendRestart"):
@@ -1020,6 +1023,17 @@ def processRemovals(removallist, only_unattended=False):
"Application removal info missing from %s",
display_name)
elif uninstallmethod == 'remove_profile':
identifier = item.get('PayloadIdentifier')
if identifier:
retcode = 0
if not profiles.remove_profile(identifier):
retcode = -1
munkicommon.display_error(
"Profile removal error for %s", identifier)
else:
munkicommon.display_error(
"Profile removal info missing from %s", display_name)
elif uninstallmethod == 'uninstall_script':
retcode = munkicommon.runEmbeddedScript(
'uninstall_script', item)

View File

@@ -1840,6 +1840,12 @@ def nameAndVersion(aString):
return (aString, '')
def hasValidConfigProfileExt(path):
"""Verifies a path ends in '.mobileconfig'"""
ext = os.path.splitext(path)[1]
return ext.lower() == '.mobileconfig'
def hasValidPackageExt(path):
"""Verifies a path ends in '.pkg' or '.mpkg'"""
ext = os.path.splitext(path)[1]
@@ -1854,7 +1860,8 @@ def hasValidDiskImageExt(path):
def hasValidInstallerItemExt(path):
"""Verifies we have an installer item"""
return hasValidPackageExt(path) or hasValidDiskImageExt(path)
return (hasValidPackageExt(path) or hasValidDiskImageExt(path)
or hasValidConfigProfileExt(path))
def getChoiceChangesXML(pkgitem):

View File

@@ -0,0 +1,175 @@
#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2014 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
#
# http://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.
"""
profiles.py
Munki module for working with configuration profiles.
"""
import os
import subprocess
import tempfile
import FoundationPlist
import munkicommon
CONFIG_PROFILE_INFO = None
def config_profile_info():
'''Returns a dictionary representing the output of `profiles -C -o`'''
global CONFIG_PROFILE_INFO
if CONFIG_PROFILE_INFO is not None:
return CONFIG_PROFILE_INFO
output_plist = tempfile.mkdtemp(dir=munkicommon.tmpdir())
cmd = ['/usr/bin/profiles', '-C', '-o', output_plist]
proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
proc.communicate()
if proc.returncode != 0:
munkicommon.display_error(
'Could not obtain configuration profile info: %s' % proc.stderr)
CONFIG_PROFILE_INFO = {}
else:
try:
CONFIG_PROFILE_INFO = FoundationPlist.readPlist(
output_plist + '.plist')
except BaseException, err:
munkicommon.display_error(
'Could not read configuration profile info: %s' % err)
CONFIG_PROFILE_INFO = {}
finally:
try:
os.unlink(output_plist + '.plist')
except BaseException:
pass
return CONFIG_PROFILE_INFO
def identifier_in_config_profile_info(identifier):
'''Returns True if identifier is among the installed PayloadIdentifiers,
False otherwise'''
for profile in config_profile_info().get('_computerlevel', []):
if profile['ProfileIdentifier'] == identifier:
return True
return False
def profile_data_path():
'''Returns the path to our installed profile data store'''
ManagedInstallDir = munkicommon.pref('ManagedInstallDir')
return os.path.join(ManagedInstallDir, 'ConfigProfileData.plist')
def profile_install_data():
'''Reads profile install data'''
try:
profile_data = FoundationPlist.readPlist(profile_data_path())
return profile_data
except BaseException:
return {}
def store_profile_install_data(identifier, hash_value):
'''Stores file hash info for profile identifier.
If hash_value is None, item is removed from the datastore.'''
profile_data = profile_install_data()
if hash_value is not None:
profile_data[identifier] = hash_value
elif identifier in profile_data.keys():
del profile_data[identifier]
try:
FoundationPlist.writePlist(profile_data, profile_data_path())
except BaseException, err:
munkicommon.display_error(
'Cannot update hash for %s: %s' % (identifier, err))
def read_profile(profile_path):
'''Reads a profile. Currently supports only unsigned, unencrypted
profiles'''
try:
return FoundationPlist.readPlist(profile_path)
except BaseException, err:
munkicommon.display_error(
'Error reading profile %s: %s' % (profile_path, err))
return {}
def record_profile_hash(profile_path):
'''Stores a file hash for this profile in our profile tracking plist'''
profile_identifier = read_profile(profile_path).get('PayloadIdentifier')
profile_hash = munkicommon.getsha256hash(profile_path)
if profile_identifier:
store_profile_install_data(profile_identifier, profile_hash)
def remove_profile_hash(identifier):
'''Removes the stored hash for profile with identifier'''
store_profile_install_data(identifier, None)
def get_profile_hash(profile_identifier):
'''Returns the hash for profile_identifier'''
return profile_install_data().get(profile_identifier)
def install_profile(profile_path):
'''Installs a profile. Returns True on success, False otherwise'''
cmd = ['/usr/bin/profiles', '-IF', profile_path]
proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
proc.communicate()
if proc.returncode != 0:
munkicommon.display_error(
'Profile %s installation failed: %s'
% (os.path.basename(profile_path), proc.stderr))
return False
record_profile_hash(profile_path)
return True
def remove_profile(identifier):
'''Removes a profile with the given identifier. Returns True on success,
False otherwise'''
cmd = ['/usr/bin/profiles', '-Rp', identifier]
proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
proc.communicate()
if proc.returncode != 0:
munkicommon.display_error(
'Profile %s removal failed: %s' % (identifier, proc.stderr))
return False
remove_profile_hash(identifier)
return True
def profile_needs_to_be_installed(identifier, hash_value):
'''If either condition is True, we should install the profile:
1) identifier is not in the output of `profiles -C`
2) stored hash_value for identifier does not match ours'''
if not identifier_in_config_profile_info(identifier):
return True
if get_profile_hash(identifier) != hash_value:
return True
return False
def profile_is_installed(identifier):
'''If identifier is in the output of `profiles -C`
return True, else return False'''
if identifier_in_config_profile_info(identifier):
return True
return False

View File

@@ -32,11 +32,12 @@ from urllib import quote_plus
from OpenSSL.crypto import load_certificate, FILETYPE_PEM
# our libs
import appleupdates
import fetch
import keychain
import munkicommon
import munkistatus
import appleupdates
import profiles
import FoundationPlist
# Apple's libs
@@ -1226,6 +1227,14 @@ def installedState(item_pl):
# return 1 so we're marked as not needing to be installed
return 1
if item_pl.get('installer_type') == 'profile':
identifier = item_pl.get('PayloadIdentifier')
hash_value = item_pl.get('installer_item_hash')
if profiles.profile_needs_to_be_installed(identifier, hash_value):
return 0
else:
return 1
# does 'installs' exist and is it non-empty?
if item_pl.get('installs', None):
installitems = item_pl['installs']
@@ -1290,6 +1299,13 @@ def someVersionInstalled(item_pl):
# that an install is not needed. We hope it's the latter.
return True
if item_pl.get('installer_type') == 'profile':
identifier = item_pl.get('PayloadIdentifier')
if profiles.profile_is_installed(identifier):
return True
else:
return False
# does 'installs' exist and is it non-empty?
if item_pl.get('installs'):
installitems = item_pl['installs']
@@ -1359,6 +1375,13 @@ def evidenceThisIsInstalled(item_pl):
# that an install is not needed
return True
if item_pl.get('installer_type') == 'profile':
identifier = item_pl.get('PayloadIdentifier')
if profiles.profile_is_installed(identifier):
return True
else:
return False
foundallinstallitems = False
if ('installs' in item_pl and
item_pl.get('uninstall_method') != 'removepackages'):
@@ -1874,7 +1897,8 @@ def processInstall(manifestitem, cataloglist, installinfo):
'apple_item',
'category',
'developer',
'icon_name']
'icon_name',
'PayloadIdentifier']
for key in optional_keys:
if key in item_pl:
@@ -2211,7 +2235,8 @@ def processRemoval(manifestitem, cataloglist, installinfo):
uninstall_item = item
elif uninstallmethod in ['remove_copied_items',
'remove_app',
'uninstall_script']:
'uninstall_script',
'remove_profile']:
uninstall_item = item
else:
# uninstall_method is a local script.
@@ -2312,7 +2337,8 @@ def processRemoval(manifestitem, cataloglist, installinfo):
'apple_item',
'category',
'developer',
'icon_name']
'icon_name',
'PayloadIdentifier']
for key in optionalKeys:
if key in uninstall_item:
iteminfo[key] = uninstall_item[key]