Ejemplo n.º 1
0
 def test_dangling_symlink(self, config):
     # LP: #1495688 reports a problem where /userdata/.last_update doesn't
     # exist, and the files in the config.d directory are dangling
     # symlinks.  In this case, there's really little that can be done to
     # find a reliable last update date, but at least we don't crash.
     #
     # Start by deleting any existing .ini files in config.d.
     for path in Path(config.config_d).iterdir():
         if path.suffix == '.ini':
             path.unlink()
     with ExitStack() as stack:
         tmpdir = stack.enter_context(temporary_directory())
         userdata_path = Path(tmpdir) / '.last_update'
         stack.enter_context(
             patch('systemimage.helpers.LAST_UPDATE_FILE',
                   str(userdata_path)))
         # Do not create the .last_update file.
         missing_ini = Path(tmpdir) / 'missing.ini'
         config.ini_files = [missing_ini]
         # Do not create the missing.ini file, but do create a symlink from
         # a config.d file to this missing file.
         default_ini = Path(config.config_d) / '00_default.ini'
         default_ini.symlink_to(missing_ini)
         last_update_date()
         self.assertEqual(last_update_date(), 'Unknown')
Ejemplo n.º 2
0
 def _check_for_update(self):
     # Asynchronous method call.
     log.info('Enter _check_for_update()')
     self._update = self._api.check_for_update()
     log.info('_check_for_update(): checking lock releasing')
     try:
         self._checking.release()
     except RuntimeError:  # pragma: no udm
         log.info('_check_for_update(): checking lock already released')
     else:
         log.info('_check_for_update(): checking lock released')
     # Do we have an update and can we auto-download it?
     delayed_download = False
     if self._update.is_available:
         settings = Settings()
         auto = settings.get('auto_download')
         log.info('Update available; auto-download: {}', auto)
         if auto in ('1', '2'):
             # XXX When we have access to the download service, we can
             # check if we're on the wifi (auto == '1').
             delayed_download = True
             GLib.timeout_add(50, self._download)
     # We have a timing issue.  We can't lock the downloading lock here,
     # otherwise when _download() starts running in ~50ms it will think a
     # download is already in progress.  But we want to send the UAS signal
     # here and now, *and* indicate whether the download is about to happen.
     # So just lie for now since in ~50ms the download will begin.
     self.UpdateAvailableStatus(self._update.is_available, delayed_download,
                                self._update.version, self._update.size,
                                last_update_date(), self._update.error)
     # Stop GLib from calling this method again.
     return False
Ejemplo n.º 3
0
 def test_date_from_config_d_reversed(self, config):
     # As above, but the higher numbered ini files have earlier mtimes.
     for year in range(22, 18, -1):
         ini_file = Path(config.config_d) / '{:02d}_config.ini'.format(year)
         ini_file.touch()
         timestamp = int(datetime(2040 - year, 1, 2, 3, 4, 5).timestamp())
         os.utime(str(ini_file), (timestamp, timestamp))
     config.reload()
     self.assertEqual(last_update_date(), '2021-01-02 03:04:05')
Ejemplo n.º 4
0
 def test_post_startup_delete(self, config):
     # Like test_dangling_symlink() except that an existing ini file gets
     # deleted after system startup, so some of the files that
     # last_update_date() looks at will throw an exception.
     #
     # Start by deleting any existing .ini files in config.d.  This time
     # however we don't update config.ini_files.
     for path in Path(config.config_d).iterdir():
         if path.suffix == '.ini':
             path.unlink()
     with ExitStack() as stack:
         tmpdir = stack.enter_context(temporary_directory())
         userdata_path = Path(tmpdir) / '.last_update'
         stack.enter_context(
             patch('systemimage.helpers.LAST_UPDATE_FILE',
                   str(userdata_path)))
         # Do not create the .last_update file.
         last_update_date()
         self.assertEqual(last_update_date(), 'Unknown')
Ejemplo n.º 5
0
 def test_date_no_microseconds(self, config):
     # Resolution is seconds.
     ini_file = Path(config.config_d) / '01_config.ini'
     ini_file.touch()
     timestamp = datetime(2022, 12, 11, 10, 9, 8, 7).timestamp()
     # We need nanoseconds.
     timestamp *= 1000000000
     os.utime(str(ini_file), ns=(timestamp, timestamp))
     config.reload()
     self.assertEqual(last_update_date(), '2022-12-11 10:09:08')
Ejemplo n.º 6
0
 def test_date_from_config_d(self, config):
     # The latest mtime from all the config.d files is taken as the last
     # update date.  Add a bunch of ini files where the higher numbered
     # ones have higher numbered year mtimes.
     for year in range(18, 22):
         ini_file = Path(config.config_d) / '{:02d}_config.ini'.format(year)
         ini_file.touch()
         timestamp = int(datetime(2000 + year, 1, 2, 3, 4, 5).timestamp())
         os.utime(str(ini_file), (timestamp, timestamp))
     config.reload()
     self.assertEqual(last_update_date(), '2021-01-02 03:04:05')
Ejemplo n.º 7
0
 def test_date_unknown(self):
     # If there is no /userdata/.last_update file and no ini files, then
     # the last update date is unknown.
     with ExitStack() as stack:
         config_d = stack.enter_context(temporary_directory())
         tempdir = stack.enter_context(temporary_directory())
         userdata_path = os.path.join(tempdir, '.last_update')
         stack.enter_context(
             patch('systemimage.helpers.LAST_UPDATE_FILE', userdata_path))
         config = Configuration(config_d)
         stack.enter_context(patch('systemimage.config._config', config))
         self.assertEqual(last_update_date(), 'Unknown')
Ejemplo n.º 8
0
 def test_date_from_userdata(self):
     # The last upgrade data can come from /userdata/.last_update.
     with ExitStack() as stack:
         tmpdir = stack.enter_context(temporary_directory())
         userdata_path = Path(tmpdir) / '.last_update'
         stack.enter_context(
             patch('systemimage.helpers.LAST_UPDATE_FILE',
                   str(userdata_path)))
         timestamp = int(datetime(2012, 11, 10, 9, 8, 7).timestamp())
         userdata_path.touch()
         os.utime(str(userdata_path), (timestamp, timestamp))
         self.assertEqual(last_update_date(), '2012-11-10 09:08:07')
Ejemplo n.º 9
0
 def test_date_from_userdata_takes_precedence(self, config_d):
     # The last upgrade data will come from /userdata/.last_update, even if
     # there are .ini files with later mtimes in them.
     for year in range(18, 22):
         ini_file = Path(config_d) / '{:02d}_config.ini'.format(year)
         ini_file.touch()
         timestamp = int(datetime(2000 + year, 1, 2, 3, 4, 5).timestamp())
         os.utime(str(ini_file), (timestamp, timestamp))
     with ExitStack() as stack:
         tmpdir = stack.enter_context(temporary_directory())
         userdata_path = Path(tmpdir) / '.last_update'
         stack.enter_context(
             patch('systemimage.helpers.LAST_UPDATE_FILE',
                   str(userdata_path)))
         timestamp = int(datetime(2012, 11, 10, 9, 8, 7).timestamp())
         userdata_path.touch()
         os.utime(str(userdata_path), (timestamp, timestamp))
         self.assertEqual(last_update_date(), '2012-11-10 09:08:07')
Ejemplo n.º 10
0
    def test_date_from_userdata_with_offset(self):
        def dummy_offset():
            """Returns a set time offset of one day (86400 seconds)"""
            return 86400

        # The last upgrade data can come from /userdata/.last_update.
        with ExitStack() as stack:
            tmpdir = stack.enter_context(temporary_directory())
            userdata_path = Path(tmpdir) / '.last_update'
            stack.enter_context(
                patch('systemimage.helpers.LAST_UPDATE_FILE',
                      str(userdata_path)))
            stack.enter_context(
                patch('systemimage.helpers.get_android_offset', dummy_offset))
            timestamp = int(datetime(2012, 11, 10, 9, 8, 7).timestamp())
            userdata_path.touch()
            os.utime(str(userdata_path), (timestamp, timestamp))
            self.assertEqual(last_update_date(), '2012-11-11 09:08:07')
Ejemplo n.º 11
0
 def test_last_date_no_permission(self, config):
     # LP: #1365761 reports a problem where stat'ing /userdata/.last_update
     # results in a PermissionError.  In that case it should fall back to
     # using the mtimes of the config.d ini files.
     timestamp_1 = int(datetime(2022, 1, 2, 3, 4, 5).timestamp())
     touch_build(2, timestamp_1)
     # Now create an unstat'able /userdata/.last_update file.
     with ExitStack() as stack:
         tmpdir = stack.enter_context(temporary_directory())
         userdata_path = Path(tmpdir) / '.last_update'
         stack.enter_context(
             patch('systemimage.helpers.LAST_UPDATE_FILE',
                   str(userdata_path)))
         timestamp = int(datetime(2012, 11, 10, 9, 8, 7).timestamp())
         # Make the file unreadable.
         userdata_path.touch()
         os.utime(str(userdata_path), (timestamp, timestamp))
         stack.callback(os.chmod, tmpdir, 0o777)
         os.chmod(tmpdir, 0o000)
         config.reload()
         # The last update date will be the date of the 99_build.ini file.
         self.assertEqual(last_update_date(), '2022-01-02 03:04:05')
Ejemplo n.º 12
0
    def CheckForUpdate(self):
        """Find out whether an update is available.

        This method is used to explicitly check whether an update is
        available, by communicating with the server and calculating an
        upgrade path from the current build number to a later build
        available on the server.

        This method runs asynchronously and thus does not return a result.
        Instead, an `UpdateAvailableStatus` signal is triggered when the check
        completes.  The argument to that signal is a boolean indicating
        whether the update is available or not.
        """
        self.loop.keepalive()
        # Check-and-acquire the lock.
        log.info('CheckForUpdate(): checking lock test and acquire')
        if not self._checking.acquire(blocking=False):
            log.info('CheckForUpdate(): checking lock not acquired')
            # Check is already in progress, so there's nothing more to do.  If
            # there's status available (i.e. we are in the auto-downloading
            # phase of the last CFU), then send the status.
            if self._update is not None:
                self.UpdateAvailableStatus(self._update.is_available,
                                           self._downloading.locked(),
                                           self._update.version,
                                           self._update.size,
                                           last_update_date(), "")
            return
        log.info('CheckForUpdate(): checking lock acquired')
        # We've now acquired the lock.  Reset any failure or in-progress
        # state.  Get a new mediator to reset any of its state.
        self._api = Mediator(self._progress_callback)
        log.info('Mediator recreated {}', self._api)
        self._failure_count = 0
        self._last_error = ''
        # Arrange for the actual check to happen in a little while, so that
        # this method can return immediately.
        GLib.timeout_add(50, self._check_for_update)
Ejemplo n.º 13
0
 def Information(self):
     self.loop.keepalive()
     settings = Settings()
     current_build_number = str(config.build_number)
     version_detail = getattr(config.service, 'version_detail', '')
     response = dict(
         current_build_number=current_build_number,
         device_name=config.device,
         channel_name=config.channel,
         last_update_date=last_update_date(),
         version_detail=version_detail,
         last_check_date=settings.get('last_check_date'),
     )
     if self._update is None:
         response['target_build_number'] = '-1'
         response['target_version_detail'] = ''
     elif not self._update.is_available:
         response['target_build_number'] = current_build_number
         response['target_version_detail'] = version_detail
     else:
         response['target_build_number'] = str(self._update.version)
         response['target_version_detail'] = self._update.version_detail
     return response
Ejemplo n.º 14
0
def main():
    parser = argparse.ArgumentParser(
        prog='system-image-cli', description='Ubuntu System Image Upgrader')
    parser.add_argument('--version',
                        action='version',
                        version='system-image-cli {}'.format(__version__))
    parser.add_argument('-C',
                        '--config',
                        default=DEFAULT_CONFIG_D,
                        action='store',
                        metavar='DIRECTORY',
                        help="""Use the given configuration directory instead
                                of the default""")
    parser.add_argument('-b',
                        '--build',
                        default=None,
                        action='store',
                        help="""Override the current build number just
                                this once""")
    parser.add_argument('-c',
                        '--channel',
                        default=None,
                        action='store',
                        help="""Override the channel just this once.  Use in
                                combination with `--build 0` to switch
                                channels.""")
    parser.add_argument('-d',
                        '--device',
                        default=None,
                        action='store',
                        help='Override the device name just this once')
    parser.add_argument('-f',
                        '--filter',
                        default=None,
                        action='store',
                        help="""Filter the candidate paths to contain only
                                full updates or only delta updates.  The
                                argument to this option must be either `full`
                                or `delta`""")
    parser.add_argument('-m',
                        '--maximage',
                        default=None,
                        type=int,
                        help="""After the winning upgrade path is selected,
                                remove all images with version numbers greater
                                than the given one.  If no images remain in
                                the winning path, the device is considered
                                up-to-date.""")
    parser.add_argument('-g',
                        '--no-apply',
                        default=False,
                        action='store_true',
                        help="""Download (i.e. "get") all the data files and
                                prepare for updating, but don't actually
                                reboot the device into recovery to apply the
                                update""")
    parser.add_argument('-i',
                        '--info',
                        default=False,
                        action='store_true',
                        help="""Show some information about the current
                                device, including the current build number,
                                device name and channel, then exit""")
    parser.add_argument('-n',
                        '--dry-run',
                        default=False,
                        action='store_true',
                        help="""Calculate and print the upgrade path, but do
                                not download or apply it""")
    parser.add_argument('-v',
                        '--verbose',
                        default=0,
                        action='count',
                        help='Increase verbosity')
    parser.add_argument('--progress',
                        default=[],
                        action='append',
                        help="""Add a progress meter.  Available meters are:
                                dots, logfile, and json.  Multiple --progress
                                options are allowed.""")
    parser.add_argument('-p',
                        '--percentage',
                        default=None,
                        action='store',
                        help="""Override the device's phased percentage value
                                during upgrade candidate calculation.""")
    parser.add_argument('--list-channels',
                        default=False,
                        action='store_true',
                        help="""List all available channels, then exit""")
    parser.add_argument('--factory-reset',
                        default=False,
                        action='store_true',
                        help="""Perform a destructive factory reset and
                                reboot.  WARNING: this will wipe all user data
                                on the device!""")
    parser.add_argument('--production-reset',
                        default=False,
                        action='store_true',
                        help="""Perform a destructive production reset
                                (similar to factory reset) and reboot.
                                WARNING: this will wipe all user data
                                on the device!""")
    parser.add_argument('--switch',
                        default=None,
                        action='store',
                        metavar='CHANNEL',
                        help="""Switch to the given channel.  This is
                                equivalent to `-c CHANNEL -b 0`.""")
    # Settings options.
    parser.add_argument('--show-settings',
                        default=False,
                        action='store_true',
                        help="""Show all settings as key=value pairs,
                                then exit""")
    parser.add_argument('--set',
                        default=[],
                        action='append',
                        metavar='KEY=VAL',
                        help="""Set a key and value in the settings, adding
                                the key if it doesn't yet exist, or overriding
                                its value if the key already exists.  Multiple
                                --set arguments can be given.""")
    parser.add_argument('--get',
                        default=[],
                        action='append',
                        metavar='KEY',
                        help="""Get the value for a key.  If the key does not
                                exist, a default value is returned.  Multiple
                                --get arguments can be given.""")
    parser.add_argument('--del',
                        default=[],
                        action='append',
                        metavar='KEY',
                        dest='delete',
                        help="""Delete the key and its value.  It is a no-op
                                if the key does not exist.  Multiple
                                --del arguments can be given.""")
    parser.add_argument('--override-gsm',
                        default=False,
                        action='store_true',
                        help="""When the device is set to only download over
                                WiFi, but is currently on GSM, use this switch
                                to temporarily override the update restriction.
                                This switch has no effect when using the cURL
                                based downloader.""")
    # Hidden system-image-cli only feature for testing purposes.  LP: #1333414
    parser.add_argument('--skip-gpg-verification',
                        default=False,
                        action='store_true',
                        help=argparse.SUPPRESS)

    args = parser.parse_args(sys.argv[1:])
    try:
        config.load(args.config)
    except (TypeError, FileNotFoundError):
        parser.error('\nConfiguration directory not found: {}'.format(
            args.config))
        assert 'parser.error() does not return'  # pragma: no cover

    if args.skip_gpg_verification:
        print("""\
WARNING: All GPG signature verifications have been disabled.
Your upgrades are INSECURE.""",
              file=sys.stderr)
        config.skip_gpg_verification = True

    config.override_gsm = args.override_gsm

    # Perform factory and production resets.
    if args.factory_reset:
        factory_reset()
        # We should never get here, except possibly during the testing
        # process, so just return as normal.
        return 0
    if args.production_reset:
        production_reset()
        # We should never get here, except possibly during the testing
        # process, so just return as normal.
        return 0

    # Handle all settings arguments.  They are mutually exclusive.
    if sum(
            bool(arg) for arg in (args.set, args.get, args.delete,
                                  args.show_settings)) > 1:
        parser.error('Cannot mix and match settings arguments')
        assert 'parser.error() does not return'  # pragma: no cover

    if args.show_settings:
        rows = sorted(Settings())
        for row in rows:
            print('{}={}'.format(*row))
        return 0
    if args.get:
        settings = Settings()
        for key in args.get:
            print(settings.get(key))
        return 0
    if args.set:
        settings = Settings()
        for keyval in args.set:
            key, val = keyval.split('=', 1)
            settings.set(key, val)
        return 0
    if args.delete:
        settings = Settings()
        for key in args.delete:
            settings.delete(key)
        return 0

    # Sanity check -f/--filter.
    if args.filter is None:
        candidate_filter = None
    elif args.filter == 'full':
        candidate_filter = full_filter
    elif args.filter == 'delta':
        candidate_filter = delta_filter
    else:
        parser.error('Bad filter type: {}'.format(args.filter))
        assert 'parser.error() does not return'  # pragma: no cover

    # Create the temporary directory if it doesn't exist.
    makedirs(config.system.tempdir)
    # Initialize the loggers.
    initialize(verbosity=args.verbose)
    log = logging.getLogger('systemimage')
    # We assume the cache_partition already exists, as does the /etc directory
    # (i.e. where the archive master key lives).

    # Command line overrides.  Process --switch first since if both it and
    # -c/-b are given, the latter take precedence.
    if args.switch is not None:
        config.build_number = 0
        config.channel = args.switch
    if args.build is not None:
        try:
            config.build_number = int(args.build)
        except ValueError:
            parser.error('-b/--build requires an integer: {}'.format(
                args.build))
            assert 'parser.error() does not return'  # pragma: no cover
    if args.channel is not None:
        config.channel = args.channel
    if args.device is not None:
        config.device = args.device
    if args.percentage is not None:
        config.phase_override = args.percentage

    if args.info:
        alias = getattr(config.service, 'channel_target', None)
        kws = dict(
            build_number=config.build_number,
            device=config.device,
            channel=config.channel,
            last_update=last_update_date(),
        )
        if alias is None:
            template = """\
                current build number: {build_number}
                device name: {device}
                channel: {channel}
                last update: {last_update}"""
        else:
            template = """\
                current build number: {build_number}
                device name: {device}
                channel: {channel}
                alias: {alias}
                last update: {last_update}"""
            kws['alias'] = alias
        print(dedent(template).format(**kws))
        # If there's additional version details, print this out now too.  We
        # sort the keys in reverse order because we want 'ubuntu' to generally
        # come first.
        details = version_detail()
        for key in sorted(details, reverse=True):
            print('version {}: {}'.format(key, details[key]))
        return 0

    DBusGMainLoop(set_as_default=True)

    if args.list_channels:
        state = State()
        try:
            state.run_thru('get_channel')
        except Exception:
            print(
                'Exception occurred during channel search; '
                'see log file for details',
                file=sys.stderr)
            log.exception('system-image-cli exception')
            return 1
        print('Available channels:')
        for key in sorted(state.channels):
            alias = state.channels[key].get('alias')
            if alias is None:
                print('    {}'.format(key))
            else:
                print('    {} (alias for: {})'.format(key, alias))
        return 0

    state = State()
    state.candidate_filter = candidate_filter
    if args.maximage is not None:
        state.winner_filter = version_filter(args.maximage)

    for meter in args.progress:
        if meter == 'dots':
            state.downloader.callbacks.append(_DotsProgress().callback)
        elif meter == 'json':
            state.downloader.callbacks.append(_json_progress)
        elif meter == 'logfile':
            state.downloader.callbacks.append(_LogfileProgress(log).callback)
        else:
            parser.error('Unknown progress meter: {}'.format(meter))
            assert 'parser.error() does not return'  # pragma: no cover

    if args.dry_run:
        try:
            state.run_until('download_files')
        except Exception:
            print(
                'Exception occurred during dry-run; '
                'see log file for details',
                file=sys.stderr)
            log.exception('system-image-cli exception')
            return 1
        # Say -c <no-such-channel> was given.  This will fail.
        if state.winner is None or len(state.winner) == 0:
            print('Already up-to-date')
        else:
            winning_path = [str(image.version) for image in state.winner]
            kws = dict(path=COLON.join(winning_path))
            target_build = state.winner[-1].version
            if state.channel_switch is None:
                # We're not switching channels due to an alias change.
                template = 'Upgrade path is {path}'
                percentage = phased_percentage(config.channel, target_build)
            else:
                # This upgrade changes the channel that our alias is mapped
                # to, so include that information in the output.
                template = 'Upgrade path is {path} ({from} -> {to})'
                kws['from'], kws['to'] = state.channel_switch
                percentage = phased_percentage(kws['to'], target_build)
            print(template.format(**kws))
            print('Target phase: {}%'.format(percentage))
        return 0
    else:
        # Run the state machine to conclusion.  Suppress all exceptions, but
        # note that the state machine will log them.  If an exception occurs,
        # exit with a non-zero status.
        log.info('running state machine [{}/{}]', config.channel,
                 config.device)
        try:
            if args.no_apply:
                state.run_until('apply')
            else:
                list(state)
        except KeyboardInterrupt:  # pragma: no cover
            return 0
        except Exception as error:
            print('Exception occurred during update; see log file for details',
                  file=sys.stderr)
            log.exception('system-image-cli exception')
            # This is a little bit of a hack because it's not generalized to
            # all values of --progress.  But OTOH, we always want to log the
            # error, so --progress=logfile is redundant, and --progress=dots
            # doesn't make much sense either.  Just just include some JSON
            # output if --progress=json was specified.
            if 'json' in args.progress:
                print(json.dumps(dict(type='error', msg=str(error))))
            return 1
        else:
            return 0
        finally:
            log.info('state machine finished')