mirror of
https://github.com/munki/munki.git
synced 2026-01-05 22:20:00 -06:00
Add support for importing, installing and removing configuration profiles.
This commit is contained in:
@@ -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...
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
175
code/client/munkilib/profiles.py
Normal file
175
code/client/munkilib/profiles.py
Normal 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
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user