Skip to content

Commit

Permalink
Add support for importing, installing and removing configuration prof…
Browse files Browse the repository at this point in the history
…iles.
  • Loading branch information
gregneagle committed Dec 17, 2014
1 parent 2afd260 commit 83fabfb
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 8 deletions.
27 changes: 26 additions & 1 deletion code/client/makepkginfo
Expand Up @@ -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'

This comment has been minimized.

Copy link
@timsutton

timsutton Dec 17, 2014

Member

I also needed to add this:

        cataloginfo['uninstallable'] = True

This comment has been minimized.

Copy link
@gregneagle

gregneagle Dec 17, 2014

Author Contributor

Thanks -- I noticed this in testing, manually fixed the affected pkginfo, then forgot to fix the code in makepkginfo.

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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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...
Expand Down
26 changes: 24 additions & 2 deletions code/client/munkiimport
Expand Up @@ -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')
Expand Down Expand Up @@ -431,6 +431,7 @@ def makeCatalogDB():
app_table = {}
installer_item_table = {}
hash_table = {}
profile_table = {}

itemindex = -1
for item in catalogitems:
Expand All @@ -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]:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions code/client/munkilib/installer.py
Expand Up @@ -31,6 +31,7 @@
import launchd
import munkicommon
import munkistatus
import profiles
import updatecheck
import FoundationPlist
from removepackages import removepackages
Expand Down Expand Up @@ -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"):
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 8 additions & 1 deletion code/client/munkilib/munkicommon.py
Expand Up @@ -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]
Expand All @@ -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):
Expand Down
175 changes: 175 additions & 0 deletions code/client/munkilib/profiles.py
@@ -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

0 comments on commit 83fabfb

Please sign in to comment.