def start(self): '''Starts a macOS install from an Install macOS.app stored at the root of a disk image, or from a locally installed Install macOS.app. Will always reboot after if the setup is successful. Therefore this must be done at the end of all other actions that Munki performs during a managedsoftwareupdate run.''' if boot_volume_is_cs_converting(): raise StartOSInstallError( 'Skipping macOS upgrade because the boot volume is in the ' 'middle of a CoreStorage conversion.') if self.installinfo and 'preinstall_script' in self.installinfo: # run the preinstall_script retcode = scriptutils.run_embedded_script( 'preinstall_script', self.installinfo) if retcode: # don't install macOS, return failure raise StartOSInstallError( 'Skipping macOS upgrade due to preinstall_script error.') # set up our signal handler signal.signal(signal.SIGUSR1, self.sigusr1_handler) # get our tool paths app_path = self.get_app_path(self.installer) startosinstall_path = os.path.join( app_path, 'Contents/Resources/startosinstall') os_vers_to_install = get_os_version(app_path) # run startosinstall via subprocess # we need to wrap our call to startosinstall with a utility # that makes startosinstall think it is connected to a tty-like # device so its output is unbuffered so we can get progress info # otherwise we get nothing until the process exits. # # Try to find our ptyexec tool # first look in the parent directory of this file's directory # (../) parent_dir = ( os.path.dirname( os.path.dirname( os.path.abspath(__file__)))) ptyexec_path = os.path.join(parent_dir, 'ptyexec') if not os.path.exists(ptyexec_path): # try absolute path in munki's normal install dir ptyexec_path = '/usr/local/munki/ptyexec' if os.path.exists(ptyexec_path): cmd = [ptyexec_path] else: # fall back to /usr/bin/script # this is not preferred because it uses way too much CPU # checking stdin for input that will never come... cmd = ['/usr/bin/script', '-q', '-t', '1', '/dev/null'] cmd.extend([startosinstall_path, '--agreetolicense', '--rebootdelay', '300', '--pidtosignal', str(os.getpid())]) if pkgutils.MunkiLooseVersion( os_vers_to_install) < pkgutils.MunkiLooseVersion('10.14'): # --applicationpath option is _required_ in Sierra and early # releases of High Sierra. It became optional (or is ignored?) in # later releases of High Sierra and causes warnings in Mojave # so don't add this option when installing Mojave cmd.extend(['--applicationpath', app_path]) if pkgutils.MunkiLooseVersion( os_vers_to_install) < pkgutils.MunkiLooseVersion('10.12.4'): # --volume option is _required_ prior to 10.12.4 installer # and must _not_ be included in 10.12.4+ installer's startosinstall cmd.extend(['--volume', '/']) if pkgutils.MunkiLooseVersion( os_vers_to_install) < pkgutils.MunkiLooseVersion('10.13.5'): # --nointeraction is an undocumented option that appears to be # not only no longer needed/useful but seems to trigger some issues # in more recent releases cmd.extend(['--nointeraction']) if (self.installinfo and 'additional_startosinstall_options' in self.installinfo): cmd.extend(self.installinfo['additional_startosinstall_options']) # more magic to get startosinstall to not buffer its output for # percent complete env = {'NSUnbufferedIO': 'YES'} try: job = launchd.Job(cmd, environment_vars=env, cleanup_at_exit=False) job.start() except launchd.LaunchdJobException as err: display.display_error( 'Error with launchd job (%s): %s', cmd, err) display.display_error('Aborting startosinstall run.') raise StartOSInstallError(err) startosinstall_output = [] timeout = 2 * 60 * 60 inactive = 0 while True: if processes.stop_requested(): job.stop() break info_output = job.stdout.readline() if not info_output: 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 the job display.display_error( "startosinstall 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 info_output = info_output.decode('UTF-8') # save all startosinstall output in case there is # an error so we can dump it to the log startosinstall_output.append(info_output) # parse output for useful progress info msg = info_output.strip() if msg.startswith('Preparing to '): display.display_status_minor(msg) elif msg.startswith(('Preparing ', 'Preparing: ')): # percent-complete messages percent_str = msg.split()[-1].rstrip('%.') try: percent = int(float(percent_str)) except ValueError: percent = -1 display.display_percent_done(percent, 100) elif msg.startswith(('By using the agreetolicense option', 'If you do not agree,')): # annoying legalese pass elif msg.startswith('Helper tool cr'): # no need to print that stupid message to screen! # 10.12: 'Helper tool creashed' # 10.13: 'Helper tool crashed' munkilog.log(msg) elif msg.startswith( ('Signaling PID:', 'Waiting to reboot', 'Process signaled okay')): # messages around the SIGUSR1 signalling display.display_debug1('startosinstall: %s', msg) elif msg.startswith('System going down for install'): display.display_status_minor( 'System will restart and begin upgrade of macOS.') else: # none of the above, just display display.display_status_minor(msg) # startosinstall exited munkistatus.percent(100) retcode = job.returncode() # previously we unmounted the disk image, but since we're going to # restart very very soon, don't bother #if self.dmg_mountpoint: # dmgutils.unmountdmg(self.dmg_mountpoint) if retcode and not (retcode == 255 and self.got_sigusr1): # append stderr to our startosinstall_output if job.stderr: startosinstall_output.extend(job.stderr.read().splitlines()) display.display_status_minor( "Starting macOS install failed with return code %s" % retcode) display.display_error("-"*78) for line in startosinstall_output: display.display_error(line.rstrip("\n")) display.display_error("-"*78) raise StartOSInstallError( 'startosinstall failed with return code %s' % retcode) elif self.got_sigusr1: # startosinstall got far enough along to signal us it was ready # to finish and reboot, so we can believe it was successful munkilog.log('macOS install successfully set up.') munkilog.log( 'Starting macOS install of %s: SUCCESSFUL' % os_vers_to_install, 'Install.log') # previously we checked if retcode == 255: # that may have been something specific to 10.12's startosinstall # if startosinstall exited after sending us sigusr1 we should # handle the restart. if retcode not in (0, 255): # some logging for possible investigation in the future munkilog.log('startosinstall exited %s' % retcode) munkilog.log('startosinstall quit instead of rebooted; we will ' 'do restart.') # clear our special secret InstallAssistant preference CFPreferencesSetValue( 'IAQuitInsteadOfReboot', None, '.GlobalPreferences', kCFPreferencesAnyUser, kCFPreferencesCurrentHost) # attempt to do an auth restart, or regular restart, or shutdown if not authrestartd.restart(): authrestart.do_authorized_or_normal_restart( shutdown=osutils.bridgeos_update_staged()) else: raise StartOSInstallError( 'startosinstall did not complete successfully. ' 'See /var/log/install.log for details.')
def start(self): '''Starts a macOS install from an Install macOS.app stored at the root of a disk image, or from a locally installed Install macOS.app. Will always reboot after if the setup is successful. Therefore this must be done at the end of all other actions that Munki performs during a managedsoftwareupdate run.''' if boot_volume_is_cs_converting(): raise StartOSInstallError( 'Skipping macOS upgrade because the boot volume is in the ' 'middle of a CoreStorage conversion.') if self.installinfo and 'preinstall_script' in self.installinfo: # run the postinstall_script retcode = scriptutils.run_embedded_script( 'preinstall_script', self.installinfo) if retcode: # don't install macOS, return failure raise StartOSInstallError( 'Skipping macOS upgrade due to preinstall_script error.') # set up our signal handler signal.signal(signal.SIGUSR1, self.sigusr1_handler) # get our tool paths app_path = self.get_app_path(self.installer) startosinstall_path = os.path.join( app_path, 'Contents/Resources/startosinstall') os_vers_to_install = get_os_version(app_path) # run startosinstall via subprocess # we need to wrap our call to startosinstall with a utility # that makes startosinstall think it is connected to a tty-like # device so its output is unbuffered so we can get progress info # otherwise we get nothing until the process exits. # # Try to find our ptyexec tool # first look in the parent directory of this file's directory # (../) parent_dir = ( os.path.dirname( os.path.dirname( os.path.abspath(__file__)))) ptyexec_path = os.path.join(parent_dir, 'ptyexec') if not os.path.exists(ptyexec_path): # try absolute path in munki's normal install dir ptyexec_path = '/usr/local/munki/ptyexec' if os.path.exists(ptyexec_path): cmd = [ptyexec_path] else: # fall back to /usr/bin/script # this is not preferred because it uses way too much CPU # checking stdin for input that will never come... cmd = ['/usr/bin/script', '-q', '-t', '1', '/dev/null'] cmd.extend([startosinstall_path, '--agreetolicense', '--rebootdelay', '300', '--pidtosignal', str(os.getpid())]) if pkgutils.MunkiLooseVersion( os_vers_to_install) < pkgutils.MunkiLooseVersion('10.14'): # --applicationpath option is _required_ in Sierra and early # releases of High Sierra. It became optional (or is ignored?) in # later releases of High Sierra and causes warnings in Mojave # so don't add this option when installing Mojave cmd.extend(['--applicationpath', app_path]) if pkgutils.MunkiLooseVersion( os_vers_to_install) < pkgutils.MunkiLooseVersion('10.12.4'): # --volume option is _required_ prior to 10.12.4 installer # and must _not_ be included in 10.12.4+ installer's startosinstall cmd.extend(['--volume', '/']) if pkgutils.MunkiLooseVersion( os_vers_to_install) < pkgutils.MunkiLooseVersion('10.13.5'): # --nointeraction is an undocumented option that appears to be # not only no longer needed/useful but seems to trigger some issues # in more recent releases cmd.extend(['--nointeraction']) if (self.installinfo and 'additional_startosinstall_options' in self.installinfo): cmd.extend(self.installinfo['additional_startosinstall_options']) # more magic to get startosinstall to not buffer its output for # percent complete env = {'NSUnbufferedIO': 'YES'} try: job = launchd.Job(cmd, environment_vars=env, cleanup_at_exit=False) job.start() except launchd.LaunchdJobException as err: display.display_error( 'Error with launchd job (%s): %s', cmd, err) display.display_error('Aborting startosinstall run.') raise StartOSInstallError(err) startosinstall_output = [] timeout = 2 * 60 * 60 inactive = 0 while True: if processes.stop_requested(): job.stop() break info_output = job.stdout.readline() if not info_output: 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 the job display.display_error( "startosinstall 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 info_output = info_output.decode('UTF-8') # save all startosinstall output in case there is # an error so we can dump it to the log startosinstall_output.append(info_output) # parse output for useful progress info msg = info_output.rstrip('\n') if msg.startswith('Preparing to '): display.display_status_minor(msg) elif msg.startswith('Preparing '): # percent-complete messages try: percent = int(float(msg[10:].rstrip().rstrip('.'))) except ValueError: percent = -1 display.display_percent_done(percent, 100) elif msg.startswith(('By using the agreetolicense option', 'If you do not agree,')): # annoying legalese pass elif msg.startswith('Helper tool cr'): # no need to print that stupid message to screen! # 10.12: 'Helper tool creashed' # 10.13: 'Helper tool crashed' munkilog.log(msg) elif msg.startswith( ('Signaling PID:', 'Waiting to reboot', 'Process signaled okay')): # messages around the SIGUSR1 signalling display.display_debug1('startosinstall: %s', msg) elif msg.startswith('System going down for install'): display.display_status_minor( 'System will restart and begin upgrade of macOS.') else: # none of the above, just display display.display_status_minor(msg) # startosinstall exited munkistatus.percent(100) retcode = job.returncode() # previously we unmounted the disk image, but since we're going to # restart very very soon, don't bother #if self.dmg_mountpoint: # dmgutils.unmountdmg(self.dmg_mountpoint) if retcode and not (retcode == 255 and self.got_sigusr1): # append stderr to our startosinstall_output if job.stderr: startosinstall_output.extend(job.stderr.read().splitlines()) display.display_status_minor( "Starting macOS install failed with return code %s" % retcode) display.display_error("-"*78) for line in startosinstall_output: display.display_error(line.rstrip("\n")) display.display_error("-"*78) raise StartOSInstallError( 'startosinstall failed with return code %s' % retcode) elif self.got_sigusr1: # startosinstall got far enough along to signal us it was ready # to finish and reboot, so we can believe it was successful munkilog.log('macOS install successfully set up.') munkilog.log( 'Starting macOS install of %s: SUCCESSFUL' % os_vers_to_install, 'Install.log') # previously we checked if retcode == 255: # that may have been something specific to 10.12's startosinstall # if startosinstall exited after sending us sigusr1 we should # handle the restart. if retcode not in (0, 255): # some logging for possible investigation in the future munkilog.log('startosinstall exited %s' % retcode) munkilog.log('startosinstall quit instead of rebooted; we will ' 'do restart.') # clear our special secret InstallAssistant preference CFPreferencesSetValue( 'IAQuitInsteadOfReboot', None, '.GlobalPreferences', kCFPreferencesAnyUser, kCFPreferencesCurrentHost) # attempt to do an auth restart, or regular restart if not authrestartd.restart(): authrestart.do_authorized_or_normal_restart() else: raise StartOSInstallError( 'startosinstall did not complete successfully. ' 'See /var/log/install.log for details.')