Move pkg install functions from installer.py to pkginstalls.py

This commit is contained in:
Greg Neagle
2017-01-03 16:51:08 -08:00
parent 5c4f883303
commit 39431b02e6
2 changed files with 287 additions and 249 deletions

View File

@@ -22,16 +22,14 @@ munki module to automatically install pkgs, mpkgs, and dmgs
import datetime
import os
import pwd
import subprocess
import time
import adobeutils
import catalogs
import copyfromdmg
import launchd
import munkicommon
import munkistatus
import pkginstalls
import pkgutils
import powermgr
import processes
@@ -55,248 +53,6 @@ munkicommon.report['InstallResults'] = []
munkicommon.report['RemovalResults'] = []
def removeBundleRelocationInfo(pkgpath):
'''Attempts to remove any info in the package
that would cause bundle relocation behavior.
This makes bundles install or update in their
default location.'''
munkicommon.display_debug1("Looking for bundle relocation info...")
if os.path.isdir(pkgpath):
# remove relocatable stuff
tokendefinitions = os.path.join(
pkgpath, "Contents/Resources/TokenDefinitions.plist")
if os.path.exists(tokendefinitions):
try:
os.remove(tokendefinitions)
munkicommon.display_debug1(
"Removed Contents/Resources/TokenDefinitions.plist")
except OSError:
pass
plist = {}
infoplist = os.path.join(pkgpath, "Contents/Info.plist")
if os.path.exists(infoplist):
try:
plist = FoundationPlist.readPlist(infoplist)
except FoundationPlist.NSPropertyListSerializationException:
pass
if 'IFPkgPathMappings' in plist:
del plist['IFPkgPathMappings']
try:
FoundationPlist.writePlist(plist, infoplist)
munkicommon.display_debug1("Removed IFPkgPathMappings")
except FoundationPlist.NSPropertyListWriteException:
pass
def install(pkgpath, display_name=None, choicesXMLpath=None,
suppressBundleRelocation=False, environment=None):
"""
Uses the apple installer to install the package or metapackage
at pkgpath. Prints status messages to STDOUT.
Returns a tuple:
the installer return code and restart needed as a boolean.
"""
restartneeded = False
installeroutput = []
if os.path.islink(pkgpath):
# resolve links before passing them to /usr/bin/installer
pkgpath = os.path.realpath(pkgpath)
if suppressBundleRelocation:
removeBundleRelocationInfo(pkgpath)
packagename = os.path.basename(pkgpath)
if not display_name:
display_name = packagename
munkicommon.log("Installing %s from %s" % (display_name, packagename))
cmd = ['/usr/sbin/installer', '-query', 'RestartAction', '-pkg', pkgpath]
if choicesXMLpath:
cmd.extend(['-applyChoiceChangesXML', choicesXMLpath])
proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, dummy_err) = proc.communicate()
restartaction = str(output).decode('UTF-8').rstrip("\n")
if restartaction == "RequireRestart" or \
restartaction == "RecommendRestart":
munkicommon.display_status_minor(
'%s requires a restart after installation.' % display_name)
restartneeded = True
# get the OS version; we need it later when processing installer's output,
# which varies depending on OS version.
os_version = munkicommon.getOsVersion()
cmd = ['/usr/sbin/installer', '-verboseR', '-pkg', pkgpath, '-target', '/']
if choicesXMLpath:
cmd.extend(['-applyChoiceChangesXML', choicesXMLpath])
# set up environment for installer
env_vars = os.environ.copy()
# get info for root
userinfo = pwd.getpwuid(0)
env_vars['USER'] = userinfo.pw_name
env_vars['HOME'] = userinfo.pw_dir
if environment:
# Munki admin has specified custom installer environment
for key in environment.keys():
if key == 'USER' and environment[key] == 'CURRENT_CONSOLE_USER':
# current console user (if there is one) 'owns' /dev/console
userinfo = pwd.getpwuid(os.stat('/dev/console').st_uid)
env_vars['USER'] = userinfo.pw_name
env_vars['HOME'] = userinfo.pw_dir
else:
env_vars[key] = environment[key]
munkicommon.display_debug1(
'Using custom installer environment variables: %s', env_vars)
# run installer as a launchd job
try:
job = launchd.Job(cmd, environment_vars=env_vars)
job.start()
except launchd.LaunchdJobException, err:
munkicommon.display_error(
'Error with launchd job (%s): %s', cmd, str(err))
munkicommon.display_error('Can\'t run installer.')
return (-3, False)
timeout = 2 * 60 * 60
inactive = 0
last_output = None
while True:
installinfo = job.stdout.readline()
if not installinfo:
if job.returncode() is not None:
break
else:
# no data, but we're still running
inactive += 1
if inactive >= timeout:
# no output for too long, kill this installer session
munkicommon.display_error(
"/usr/sbin/installer timeout after %d seconds"
% timeout)
job.stop()
break
# sleep a bit before checking for more output
time.sleep(1)
continue
# we got non-empty output, reset inactive timer
inactive = 0
# Don't bother parsing the stdout output if it hasn't changed since
# the last loop iteration.
if last_output == installinfo:
continue
last_output = installinfo
installinfo = installinfo.decode('UTF-8')
if installinfo.startswith("installer:"):
# save all installer output in case there is
# an error so we can dump it to the log
installeroutput.append(installinfo)
msg = installinfo[10:].rstrip("\n")
if msg.startswith("PHASE:"):
phase = msg[6:]
if phase:
munkicommon.display_status_minor(phase)
elif msg.startswith("STATUS:"):
status = msg[7:]
if status:
munkicommon.display_status_minor(status)
elif msg.startswith("%"):
percent = float(msg[1:])
if os_version == '10.5':
# Leopard uses a float from 0 to 1
percent = int(percent * 100)
munkistatus.percent(percent)
munkicommon.display_status_minor(
"%s percent complete" % percent)
elif msg.startswith(" Error"):
munkicommon.display_error(msg)
munkistatus.detail(msg)
elif msg.startswith(" Cannot install"):
munkicommon.display_error(msg)
munkistatus.detail(msg)
else:
munkicommon.log(msg)
# installer exited
retcode = job.returncode()
if retcode != 0:
# append stdout to our installer output
installeroutput.extend(job.stderr.read().splitlines())
munkicommon.display_status_minor(
"Install of %s failed with return code %s" % (packagename, retcode))
munkicommon.display_error("-"*78)
for line in installeroutput:
munkicommon.display_error(line.rstrip("\n"))
munkicommon.display_error("-"*78)
restartneeded = False
elif retcode == 0:
munkicommon.log("Install of %s was successful." % packagename)
munkistatus.percent(100)
return (retcode, restartneeded)
def installall(dirpath, display_name=None, choicesXMLpath=None,
suppressBundleRelocation=False, environment=None):
"""
Attempts to install all pkgs and mpkgs in a given directory.
Will mount dmg files and install pkgs and mpkgs found at the
root of any mountpoints.
"""
retcode = 0
restartflag = False
installitems = munkicommon.listdir(dirpath)
for item in installitems:
if munkicommon.stopRequested():
return (retcode, restartflag)
itempath = os.path.join(dirpath, item)
if munkicommon.hasValidDiskImageExt(item):
munkicommon.display_info("Mounting disk image %s" % item)
mountpoints = munkicommon.mountdmg(itempath, use_shadow=True)
if mountpoints == []:
munkicommon.display_error("No filesystems mounted from %s",
item)
return (retcode, restartflag)
if munkicommon.stopRequested():
munkicommon.unmountdmg(mountpoints[0])
return (retcode, restartflag)
for mountpoint in mountpoints:
# install all the pkgs and mpkgs at the root
# of the mountpoint -- call us recursively!
(retcode, needsrestart) = installall(mountpoint, display_name,
choicesXMLpath,
suppressBundleRelocation,
environment)
if needsrestart:
restartflag = True
if retcode:
# ran into error; should unmount and stop.
munkicommon.unmountdmg(mountpoints[0])
return (retcode, restartflag)
munkicommon.unmountdmg(mountpoints[0])
if munkicommon.hasValidInstallerItemExt(item):
(retcode, needsrestart) = install(
itempath, display_name,
choicesXMLpath, suppressBundleRelocation, environment)
if needsrestart:
restartflag = True
if retcode:
# ran into error; should stop.
return (retcode, restartflag)
return (retcode, restartflag)
def removeCopiedItems(itemlist):
'''Removes filesystem items based on info in itemlist.
These items were typically installed via DMG'''
@@ -535,14 +291,14 @@ def installWithInfo(
fullpkgpath = os.path.join(
mountpoints[0], item['package_path'])
if os.path.exists(fullpkgpath):
(retcode, needtorestart) = install(
(retcode, needtorestart) = pkginstalls.install(
fullpkgpath, display_name, choicesXMLfile,
suppressBundleRelocation, installer_environment)
else:
# no relative path to pkg on dmg, so just install all
# pkgs found at the root of the first mountpoint
# (hopefully there's only one)
(retcode, needtorestart) = installall(
(retcode, needtorestart) = pkginstalls.installall(
mountpoints[0], display_name, choicesXMLfile,
suppressBundleRelocation, installer_environment)
if (needtorestart or
@@ -552,7 +308,7 @@ def installWithInfo(
munkicommon.unmountdmg(mountpoints[0])
elif (munkicommon.hasValidPackageExt(itempath) or
itempath.endswith(".dist")):
(retcode, needtorestart) = install(
(retcode, needtorestart) = pkginstalls.install(
itempath, display_name, choicesXMLfile,
suppressBundleRelocation, installer_environment)
if (needtorestart or
@@ -965,7 +721,7 @@ def run(only_unattended=False):
# filter list to items that need to be installed
installlist = [item for item in
installinfo['managed_installs']
if item.get('installed') == False]
if item.get('installed') is False]
munkicommon.report['ItemsToInstall'] = installlist
if installlist:
if len(installlist) == 1:

View File

@@ -0,0 +1,282 @@
#!/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.
"""
pkginstalls.py
Created by Greg Neagle on 2017-01-03.
Routines for installing Apple pkgs
"""
import os
import pwd
import subprocess
import time
from . import display
from . import dmgutils
from . import launchd
from . import munkilog
from . import munkistatus
from . import osutils
from . import processes
from . import pkgutils
from . import FoundationPlist
def remove_bundle_relocation_info(pkgpath):
'''Attempts to remove any info in the package that would cause
bundle relocation behavior. This makes bundles install or update in their
default location.'''
display.display_debug1("Looking for bundle relocation info...")
if os.path.isdir(pkgpath):
# remove relocatable stuff
tokendefinitions = os.path.join(
pkgpath, "Contents/Resources/TokenDefinitions.plist")
if os.path.exists(tokendefinitions):
try:
os.remove(tokendefinitions)
display.display_debug1(
"Removed Contents/Resources/TokenDefinitions.plist")
except OSError:
pass
plist = {}
infoplist = os.path.join(pkgpath, "Contents/Info.plist")
if os.path.exists(infoplist):
try:
plist = FoundationPlist.readPlist(infoplist)
except FoundationPlist.NSPropertyListSerializationException:
pass
if 'IFPkgPathMappings' in plist:
del plist['IFPkgPathMappings']
try:
FoundationPlist.writePlist(plist, infoplist)
display.display_debug1("Removed IFPkgPathMappings")
except FoundationPlist.NSPropertyListWriteException:
pass
def install(pkgpath, display_name=None, choicesXMLpath=None,
suppressBundleRelocation=False, environment=None):
"""
Uses the apple installer to install the package or metapackage
at pkgpath. Prints status messages to STDOUT.
Returns a tuple:
the installer return code and restart needed as a boolean.
"""
restartneeded = False
installeroutput = []
if os.path.islink(pkgpath):
# resolve links before passing them to /usr/bin/installer
pkgpath = os.path.realpath(pkgpath)
if suppressBundleRelocation:
remove_bundle_relocation_info(pkgpath)
packagename = os.path.basename(pkgpath)
if not display_name:
display_name = packagename
munkilog.log("Installing %s from %s" % (display_name, packagename))
cmd = ['/usr/sbin/installer', '-query', 'RestartAction', '-pkg', pkgpath]
if choicesXMLpath:
cmd.extend(['-applyChoiceChangesXML', choicesXMLpath])
proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, dummy_err) = proc.communicate()
restartaction = str(output).decode('UTF-8').rstrip("\n")
if restartaction == "RequireRestart" or \
restartaction == "RecommendRestart":
display.display_status_minor(
'%s requires a restart after installation.' % display_name)
restartneeded = True
# get the OS version; we need it later when processing installer's output,
# which varies depending on OS version.
os_version = osutils.getOsVersion()
cmd = ['/usr/sbin/installer', '-verboseR', '-pkg', pkgpath, '-target', '/']
if choicesXMLpath:
cmd.extend(['-applyChoiceChangesXML', choicesXMLpath])
# set up environment for installer
env_vars = os.environ.copy()
# get info for root
userinfo = pwd.getpwuid(0)
env_vars['USER'] = userinfo.pw_name
env_vars['HOME'] = userinfo.pw_dir
if environment:
# Munki admin has specified custom installer environment
for key in environment.keys():
if key == 'USER' and environment[key] == 'CURRENT_CONSOLE_USER':
# current console user (if there is one) 'owns' /dev/console
userinfo = pwd.getpwuid(os.stat('/dev/console').st_uid)
env_vars['USER'] = userinfo.pw_name
env_vars['HOME'] = userinfo.pw_dir
else:
env_vars[key] = environment[key]
display.display_debug1(
'Using custom installer environment variables: %s', env_vars)
# run installer as a launchd job
try:
job = launchd.Job(cmd, environment_vars=env_vars)
job.start()
except launchd.LaunchdJobException, err:
display.display_error(
'Error with launchd job (%s): %s', cmd, str(err))
display.display_error('Can\'t run installer.')
return (-3, False)
timeout = 2 * 60 * 60
inactive = 0
last_output = None
while True:
installinfo = job.stdout.readline()
if not installinfo:
if job.returncode() is not None:
break
else:
# no data, but we're still running
inactive += 1
if inactive >= timeout:
# no output for too long, kill this installer session
display.display_error(
"/usr/sbin/installer timeout after %d seconds"
% timeout)
job.stop()
break
# sleep a bit before checking for more output
time.sleep(1)
continue
# we got non-empty output, reset inactive timer
inactive = 0
# Don't bother parsing the stdout output if it hasn't changed since
# the last loop iteration.
if last_output == installinfo:
continue
last_output = installinfo
installinfo = installinfo.decode('UTF-8')
if installinfo.startswith("installer:"):
# save all installer output in case there is
# an error so we can dump it to the log
installeroutput.append(installinfo)
msg = installinfo[10:].rstrip("\n")
if msg.startswith("PHASE:"):
phase = msg[6:]
if phase:
display.display_status_minor(phase)
elif msg.startswith("STATUS:"):
status = msg[7:]
if status:
display.display_status_minor(status)
elif msg.startswith("%"):
percent = float(msg[1:])
if os_version == '10.5':
# Leopard uses a float from 0 to 1
percent = int(percent * 100)
munkistatus.percent(percent)
display.display_status_minor(
"%s percent complete" % percent)
elif msg.startswith(" Error"):
display.display_error(msg)
munkistatus.detail(msg)
elif msg.startswith(" Cannot install"):
display.display_error(msg)
munkistatus.detail(msg)
else:
munkilog.log(msg)
# installer exited
retcode = job.returncode()
if retcode != 0:
# append stdout to our installer output
installeroutput.extend(job.stderr.read().splitlines())
display.display_status_minor(
"Install of %s failed with return code %s" % (packagename, retcode))
display.display_error("-"*78)
for line in installeroutput:
display.display_error(line.rstrip("\n"))
display.display_error("-"*78)
restartneeded = False
elif retcode == 0:
munkilog.log("Install of %s was successful." % packagename)
munkistatus.percent(100)
return (retcode, restartneeded)
def installall(dirpath, display_name=None, choicesXMLpath=None,
suppressBundleRelocation=False, environment=None):
"""
Attempts to install all pkgs and mpkgs in a given directory.
Will mount dmg files and install pkgs and mpkgs found at the
root of any mountpoints.
"""
retcode = 0
restartflag = False
installitems = osutils.listdir(dirpath)
for item in installitems:
if processes.stopRequested():
return (retcode, restartflag)
itempath = os.path.join(dirpath, item)
if pkgutils.hasValidDiskImageExt(item):
display.display_info("Mounting disk image %s" % item)
mountpoints = dmgutils.mountdmg(itempath, use_shadow=True)
if mountpoints == []:
display.display_error("No filesystems mounted from %s", item)
return (retcode, restartflag)
if processes.stopRequested():
dmgutils.unmountdmg(mountpoints[0])
return (retcode, restartflag)
for mountpoint in mountpoints:
# install all the pkgs and mpkgs at the root
# of the mountpoint -- call us recursively!
(retcode, needsrestart) = installall(mountpoint, display_name,
choicesXMLpath,
suppressBundleRelocation,
environment)
if needsrestart:
restartflag = True
if retcode:
# ran into error; should unmount and stop.
dmgutils.unmountdmg(mountpoints[0])
return (retcode, restartflag)
dmgutils.unmountdmg(mountpoints[0])
if pkgutils.hasValidInstallerItemExt(item):
(retcode, needsrestart) = install(
itempath, display_name,
choicesXMLpath, suppressBundleRelocation, environment)
if needsrestart:
restartflag = True
if retcode:
# ran into error; should stop.
return (retcode, restartflag)
return (retcode, restartflag)
if __name__ == '__main__':
print 'This is a library of support tools for the Munki Suite.'