示例#1
0
    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.')
示例#2
0
    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.')