Ejemplo n.º 1
0
    def test_download_winners_overwrite(self):
        # Check that all the winning path's files are downloaded, even if
        # those files already exist in their destination paths.
        setup_keyrings()
        state = State()
        touch_build(100)
        # Run the state machine until we download the files.
        for basename in '56789abcd':
            base = os.path.join(config.updater.cache_partition, basename)
            path = base + '.txt'
            with open(path, 'w', encoding='utf-8') as fp:
                print('stale', file=fp)
        state.run_thru('download_files')

        # The B path files contain their checksums.
        def assert_file_contains(filename, contents):
            path = os.path.join(config.updater.cache_partition, filename)
            with open(path, encoding='utf-8') as fp:
                self.assertEqual(fp.read(), contents)

        assert_file_contains('5.txt', '345')
        assert_file_contains('6.txt', '456')
        assert_file_contains('7.txt', '567')
        # Delta B.1 files.
        assert_file_contains('8.txt', '678')
        assert_file_contains('9.txt', '789')
        assert_file_contains('a.txt', '89a')
        # Delta B.2 files.
        assert_file_contains('b.txt', '9ab')
        assert_file_contains('d.txt', 'fed')
        assert_file_contains('c.txt', 'edc')
Ejemplo n.º 2
0
 def test_download_winners_signed_by_wrong_key(self):
     # There is a device key, but the image files are signed by the image
     # signing key, which according to the spec means the files are not
     # signed correctly.
     setup_keyrings()
     # To set up the device signing key, we need to load this channels.json
     # file and copy the device keyring to the server.
     copy('winner.channels_02.json', self._serverdir, 'channels.json')
     sign(os.path.join(self._serverdir, 'channels.json'),
          'image-signing.gpg')
     setup_keyring_txz(
         'device-signing.gpg', 'image-signing.gpg',
         dict(type='device-signing'),
         os.path.join(self._serverdir, 'stable', 'nexus7', 'device.tar.xz'))
     sign(os.path.join(self._serverdir, self._indexpath),
          'device-signing.gpg')
     # All the downloadable files are now signed with a bogus key.
     setup_index('winner.index_02.json', self._serverdir, 'spare.gpg')
     touch_build(100)
     # Run the state machine until just before we download the files.
     state = State()
     state.run_until('download_files')
     # The next state transition will fail because of the missing signature.
     self.assertRaises(SignatureError, next, state)
     # There are no downloaded files.
     txtfiles = set(filename for filename in os.listdir(config.tempdir)
                    if os.path.splitext(filename)[1] == '.txt')
     self.assertEqual(len(txtfiles), 0)
Ejemplo n.º 3
0
    def test_download_winners(self):
        # Check that all the winning path's files are downloaded.
        setup_keyrings()
        state = State()
        touch_build(100)
        # Run the state machine until we download the files.
        state.run_thru('download_files')

        # The B path files contain their checksums.
        def assert_file_contains(filename, contents):
            path = os.path.join(config.updater.cache_partition, filename)
            with open(path, encoding='utf-8') as fp:
                self.assertEqual(fp.read(), contents)

        assert_file_contains('5.txt', '345')
        assert_file_contains('6.txt', '456')
        assert_file_contains('7.txt', '567')
        # Delta B.1 files.
        assert_file_contains('8.txt', '678')
        assert_file_contains('9.txt', '789')
        assert_file_contains('a.txt', '89a')
        # Delta B.2 files.
        assert_file_contains('b.txt', '9ab')
        assert_file_contains('d.txt', 'fed')
        assert_file_contains('c.txt', 'edc')
Ejemplo n.º 4
0
 def test_load_channel_over_https_port_with_http_fails(self):
     # We maliciously put an HTTP server on the HTTPS port.
     setup_keyrings()
     state = State()
     # Try to get the blacklist.  This will fail silently since it's okay
     # not to find a blacklist.
     state.run_thru('get_blacklist_1')
     # This will fail to get the channels.json file.
     with make_http_server(self._serverdir, 8943):
         self.assertRaises(FileNotFoundError, next, state)
Ejemplo n.º 5
0
 def setUp(self):
     self._stack = ExitStack()
     self._state = State()
     try:
         self._serverdir = self._stack.enter_context(temporary_directory())
         self._stack.push(
             make_http_server(self._serverdir, 8943, 'cert.pem', 'key.pem'))
         copy('channel.channels_01.json', self._serverdir, 'channels.json')
         self._channels_path = os.path.join(self._serverdir,
                                            'channels.json')
     except:
         self._stack.close()
         raise
Ejemplo n.º 6
0
 def test_calculate_winner(self):
     # Calculate the winning upgrade path.
     setup_keyrings()
     state = State()
     touch_build(100)
     # Run the state machine long enough to get the candidates and winner.
     state.run_thru('calculate_winner')
     # There are three candidate upgrade paths.
     descriptions = []
     for image in state.winner:
         # There's only one description per image so order doesn't matter.
         descriptions.extend(image.descriptions.values())
     self.assertEqual(descriptions, ['Full B', 'Delta B.1', 'Delta B.2'])
Ejemplo n.º 7
0
 def test_load_index_with_bad_keyring(self):
     # Here, the index.json file is signed with a defective device keyring.
     self._copysign('index.channels_02.json', 'channels.json',
                    'image-signing.gpg')
     # This will be signed by a keyring that is not the device keyring.
     self._copysign('index.index_04.json', 'stable/nexus7/index.json',
                    'spare.gpg')
     setup_keyrings()
     setup_keyring_txz(
         'device-signing.gpg', 'image-signing.gpg',
         dict(type='device-signing'),
         os.path.join(self._serverdir, 'stable', 'nexus7', 'device.tar.xz'))
     state = State()
     state.run_until('get_index')
     self.assertRaises(SignatureError, next, state)
Ejemplo n.º 8
0
 def test_load_index_good_path(self):
     # Load the index.json pointed to by the channels.json.  All signatures
     # validate correctly and there is no device keyring or blacklist.
     self._copysign('index.channels_05.json', 'channels.json',
                    'image-signing.gpg')
     # index.index_04.json path B will win, with no bootme flags.
     self._copysign('index.index_04.json', 'stable/nexus7/index.json',
                    'image-signing.gpg')
     setup_keyrings()
     state = State()
     state.run_thru('get_index')
     self.assertEqual(
         state.index.global_.generated_at,
         datetime(2013, 4, 29, 18, 45, 27, tzinfo=timezone.utc))
     self.assertEqual(state.index.images[0].files[1].checksum, 'bcd')
Ejemplo n.º 9
0
 def setUp(self):
     # Avoid circular imports.
     from systemimage.state import State
     self._resources = ExitStack()
     self._state = State()
     try:
         self._serverdir = self._resources.enter_context(
             temporary_directory())
         # Start up both an HTTPS and HTTP server.  The data files are
         # vended over the latter, everything else, over the former.
         self._resources.push(
             make_http_server(self._serverdir, 8943, 'cert.pem', 'key.pem'))
         self._resources.push(make_http_server(self._serverdir, 8980))
         # Set up the server files.
         assert self.CHANNEL_FILE is not None, (
             'Subclasses must set CHANNEL_FILE')
         copy(self.CHANNEL_FILE, self._serverdir, 'channels.json')
         sign(os.path.join(self._serverdir, 'channels.json'),
              'image-signing.gpg')
         assert self.CHANNEL is not None, 'Subclasses must set CHANNEL'
         assert self.DEVICE is not None, 'Subclasses must set DEVICE'
         index_path = os.path.join(self._serverdir, self.CHANNEL,
                                   self.DEVICE, 'index.json')
         head, tail = os.path.split(index_path)
         assert self.INDEX_FILE is not None, (
             'Subclasses must set INDEX_FILE')
         copy(self.INDEX_FILE, head, tail)
         sign(index_path, self.SIGNING_KEY)
         setup_index(self.INDEX_FILE, self._serverdir, self.SIGNING_KEY)
     except:
         self._resources.close()
         raise
     self.addCleanup(self._resources.close)
Ejemplo n.º 10
0
 def test_no_download_winners_with_bad_signature(self):
     # If one of the download files has a bad a signature, none of the
     # downloaded files are available.
     setup_keyrings()
     state = State()
     touch_build(100)
     # Break a signature
     sign(os.path.join(self._serverdir, '6', '7', '8.txt'), 'spare.gpg')
     # Run the state machine to calculate the winning path.
     state.run_until('download_files')
     # The next state transition will fail because of the missing signature.
     self.assertRaises(SignatureError, next, state)
     # There are no downloaded files.
     txtfiles = set(filename for filename in os.listdir(config.tempdir)
                    if os.path.splitext(filename)[1] == '.txt')
     self.assertEqual(len(txtfiles), 0)
Ejemplo n.º 11
0
 def test_no_download_winners_with_missing_signature(self):
     # If one of the download files is missing a signature, none of the
     # files get downloaded and get_files() fails.
     setup_keyrings()
     state = State()
     touch_build(100)
     # Remove a signature.
     os.remove(os.path.join(self._serverdir, '6/7/8.txt.asc'))
     # Run the state machine to calculate the winning path.
     state.run_until('download_files')
     # The next state transition will fail because of the missing signature.
     self.assertRaises(FileNotFoundError, next, state)
     # There are no downloaded files.
     txtfiles = set(filename for filename in os.listdir(config.tempdir)
                    if os.path.splitext(filename)[1] == '.txt')
     self.assertEqual(len(txtfiles), 0, txtfiles)
Ejemplo n.º 12
0
 def test_download_winners_bad_checksums(self):
     # Similar to the various good paths, except because the checksums are
     # wrong in this index.json file, we'll get a error when downloading.
     copy('winner.index_01.json', self._serverdir, self._indexpath)
     sign(os.path.join(self._serverdir, self._indexpath),
          'image-signing.gpg')
     setup_index('winner.index_01.json', self._serverdir,
                 'image-signing.gpg')
     setup_keyrings()
     state = State()
     touch_build(100)
     # Run the state machine until we're prepped to download
     state.run_until('download_files')
     # Now try to download the files and get the error.
     with self.assertRaises(FileNotFoundError) as cm:
         next(state)
     self.assertIn('HASH ERROR', str(cm.exception))
Ejemplo n.º 13
0
 def test_load_index_with_device_keyring(self):
     # Here, the index.json file is signed with a device keyring.
     self._copysign('index.channels_02.json', 'channels.json',
                    'image-signing.gpg')
     # index.index_04.json.json path B will win, with no bootme flags.
     self._copysign('index.index_04.json', 'stable/nexus7/index.json',
                    'device-signing.gpg')
     setup_keyrings()
     setup_keyring_txz(
         'device-signing.gpg', 'image-signing.gpg',
         dict(type='device-signing'),
         os.path.join(self._serverdir, 'stable', 'nexus7', 'device.tar.xz'))
     state = State()
     state.run_thru('get_index')
     self.assertEqual(
         state.index.global_.generated_at,
         datetime(2013, 4, 29, 18, 45, 27, tzinfo=timezone.utc))
     self.assertEqual(state.index.images[0].files[1].checksum, 'bcd')
Ejemplo n.º 14
0
    def test_download_winners_signed_by_device_key(self):
        # Check that all the winning path's files are downloaded, even when
        # they are signed by the device key instead of the image signing
        # master.
        setup_keyrings()
        # To set up the device signing key, we need to load channels_03.json
        # and copy the device keyring to the server.
        copy('winner.channels_02.json', self._serverdir, 'channels.json')
        sign(os.path.join(self._serverdir, 'channels.json'),
             'image-signing.gpg')
        setup_keyring_txz(
            'device-signing.gpg', 'image-signing.gpg',
            dict(type='device-signing'),
            os.path.join(self._serverdir, 'stable', 'nexus7', 'device.tar.xz'))
        # The index.json file and all the downloadable files must now be
        # signed with the device key.
        sign(os.path.join(self._serverdir, self._indexpath),
             'device-signing.gpg')
        setup_index('winner.index_02.json', self._serverdir,
                    'device-signing.gpg')
        touch_build(100)
        # Run the state machine until we download the files.
        state = State()
        state.run_thru('download_files')

        # The B path files contain their checksums.
        def assert_file_contains(filename, contents):
            path = os.path.join(config.updater.cache_partition, filename)
            with open(path, encoding='utf-8') as fp:
                self.assertEqual(fp.read(), contents)

        assert_file_contains('5.txt', '345')
        assert_file_contains('6.txt', '456')
        assert_file_contains('7.txt', '567')
        # Delta B.1 files.
        assert_file_contains('8.txt', '678')
        assert_file_contains('9.txt', '789')
        assert_file_contains('a.txt', '89a')
        # Delta B.2 files.
        assert_file_contains('b.txt', '9ab')
        assert_file_contains('d.txt', 'fed')
        assert_file_contains('c.txt', 'edc')
Ejemplo n.º 15
0
 def test_missing_device(self):
     # The system's device does not exist.
     self._copysign('index.channels_04.json', 'channels.json',
                    'image-signing.gpg')
     # index.index_04.json path B will win, with no bootme flags.
     self._copysign('index.index_04.json', 'stable/nexus7/index.json',
                    'image-signing.gpg')
     setup_keyrings()
     # Our device (nexus7) isn't in the channels.json file, so there's
     # nothing to do.  Running the state machine to its conclusion leaves
     # us with no index file.
     state = State()
     list(state)
     # There really is nothing left to do.
     self.assertIsNone(state.index)
Ejemplo n.º 16
0
 def test_calculate_candidates(self):
     # Calculate the candidate paths.
     setup_keyrings()
     state = State()
     # Run the state machine until we get an index file.
     state.run_until('calculate_winner')
     candidates = get_candidates(state.index, 100)
     # There are three candidate upgrade paths.
     self.assertEqual(len(candidates), 3)
     descriptions = []
     for image in candidates[0]:
         # There's only one description per image so order doesn't matter.
         descriptions.extend(image.descriptions.values())
     self.assertEqual(descriptions, ['Full A', 'Delta A.1', 'Delta A.2'])
     descriptions = []
     for image in candidates[1]:
         # There's only one description per image so order doesn't matter.
         descriptions.extend(image.descriptions.values())
     self.assertEqual(descriptions, ['Full B', 'Delta B.1', 'Delta B.2'])
     descriptions = []
     for image in candidates[2]:
         # There's only one description per image so order doesn't matter.
         descriptions.extend(image.descriptions.values())
     self.assertEqual(descriptions, ['Full C', 'Delta C.1'])
Ejemplo n.º 17
0
class Mediator:
    """This is the DBus API mediator.

    It essentially implements the entire DBus API, but at a level below the
    mechanics of DBus.  Methods of this class are hooked directly into the
    DBus layer to satisfy that interface.
    """
    def __init__(self, callback=None):
        self._state = State()
        self._config = config
        self._update = None
        self._channels = None
        self._callback = callback

    def __repr__(self):  # pragma: no cover
        fmt = '<Mediator at 0x{:x} | State at 0x{:x} | Downloader at {}>'
        args = [
            id(self),
            id(self._state),
            'None' if self._state.downloader is None else '0x{:x}'.format(
                id(self._state.downloader))
        ]
        return fmt.format(*args)

    def cancel(self):
        self._state.downloader.cancel()

    def pause(self):
        self._state.downloader.pause()

    def resume(self):
        self._state.downloader.resume()

    def check_for_update(self):
        """Is there an update available for this machine?

        :return: Flag indicating whether an update is available or not.
        :rtype: bool
        """
        if self._update is None:
            try:
                self._state.run_until('download_files')
            except Exception as error:
                # Rather than letting this percolate up, eventually reaching
                # the GLib main loop and thus triggering apport, Let's log the
                # error and set the relevant information in the class.
                log.exception('check_for_update failed')
                self._update = Update(error=str(error))
            else:
                self._update = Update(self._state.winner)
                self._channels = list()
                for key in sorted(self._state.channels):
                    self._channels.append(
                        dict(
                            hidden=self._state.channels[key].get('hidden'),
                            alias=self._state.channels[key].get('alias'),
                            redirect=self._state.channels[key].get('redirect'),
                            name=key))
        return self._update

    def download(self):
        """Download the available update."""
        # We only want callback progress during the actual download.
        old_callbacks = self._state.downloader.callbacks[:]
        try:
            self._state.downloader.callbacks = [self._callback]
            self._state.run_until('apply')
        finally:
            self._state.downloader.callbacks = old_callbacks

    def apply(self):
        """Apply the update."""
        # Transition through all remaining states.
        list(self._state)

    def factory_reset(self):
        factory_reset()

    def production_reset(self):
        production_reset()

    def allow_gsm(self):
        self._state.downloader.allow_gsm()  # pragma: no curl

    def get_channels(self):
        """List channels. This returns output created by check_for_update."""
        return self._channels

    def set_channel(self, channel):
        found = False
        if self._channels:
            for key in self._channels:
                if key["name"] == channel:
                    found = True
                    self._config.channel = channel
                    break
        return found

    def set_build(self, build):
        self._config.build_number = build

    def get_channel(self):
        return self._config.channel

    def get_build(self):
        return self._config.build_number
Ejemplo n.º 18
0
class TestLoadChannel(unittest.TestCase):
    """Test downloading and caching the channels.json file."""
    @classmethod
    def setUpClass(cls):
        SystemImagePlugin.controller.set_mode(cert_pem='cert.pem')

    def setUp(self):
        self._stack = ExitStack()
        self._state = State()
        try:
            self._serverdir = self._stack.enter_context(temporary_directory())
            self._stack.push(
                make_http_server(self._serverdir, 8943, 'cert.pem', 'key.pem'))
            copy('channel.channels_01.json', self._serverdir, 'channels.json')
            self._channels_path = os.path.join(self._serverdir,
                                               'channels.json')
        except:
            self._stack.close()
            raise

    def tearDown(self):
        self._stack.close()

    @configuration
    def test_load_channel_good_path(self):
        # A channels.json file signed by the image signing key, no blacklist.
        sign(self._channels_path, 'image-signing.gpg')
        setup_keyrings()
        self._state.run_thru('get_channel')
        channels = self._state.channels
        self.assertEqual(channels.daily.devices.nexus7.keyring.signature,
                         '/daily/nexus7/device-keyring.tar.xz.asc')

    @configuration
    def test_load_channel_bad_signature(self):
        # We get an error if the signature on the channels.json file is bad.
        sign(self._channels_path, 'spare.gpg')
        setup_keyrings()
        self._state.run_thru('get_channel')
        # At this point, the state machine has determined that the
        # channels.json file is not signed with the cached image signing key,
        # so it will try to download a new imaging signing key.  Let's put one
        # on the server, but it will not match the key that channels.json is
        # signed with.
        key_path = os.path.join(self._serverdir, 'gpg', 'image-signing.tar.xz')
        setup_keyring_txz('image-signing.gpg', 'image-master.gpg',
                          dict(type='image-signing'), key_path)
        # This will succeed by grabbing a new image-signing key.
        from systemimage.testing.controller import stop_downloader
        stop_downloader(SystemImagePlugin.controller)
        next(self._state)
        # With the next state transition, we'll go back to trying to get the
        # channel.json file.  Since the signature will still be bad, we'll get
        # a SignatureError this time.
        self.assertRaises(SignatureError, next, self._state)

    @configuration
    def test_load_channel_bad_signature_gets_fixed(self, config_d):
        # Like above, but the second download of the image signing key results
        # in a properly signed channels.json file.
        sign(self._channels_path, 'spare.gpg')
        setup_keyrings()
        self._state.run_thru('get_channel')
        # At this point, the state machine has determined that the
        # channels.json file is not signed with the cached image signing key,
        # so it will try to download a new imaging signing key.  Let's put one
        # on the server, but it will not match the key that channels.json is
        # signed with.
        self.assertIsNone(self._state.channels)
        setup_keyring_txz(
            'spare.gpg', 'image-master.gpg', dict(type='image-signing'),
            os.path.join(self._serverdir, 'gpg', 'image-signing.tar.xz'))
        # This will succeed by grabbing a new image-signing key.
        config = Configuration(config_d)
        with open(config.gpg.image_signing, 'rb') as fp:
            checksum = hashlib.md5(fp.read()).digest()
        next(self._state)
        with open(config.gpg.image_signing, 'rb') as fp:
            self.assertNotEqual(checksum, hashlib.md5(fp.read()).digest())
        # The next state transition will find that the channels.json file is
        # properly signed.
        next(self._state)
        self.assertIsNotNone(self._state.channels)
        self.assertEqual(
            self._state.channels.daily.devices.nexus7.keyring.signature,
            '/daily/nexus7/device-keyring.tar.xz.asc')

    @configuration
    def test_load_channel_blacklisted_signature(self, config_d):
        # We get an error if the signature on the channels.json file is good
        # but the key is blacklisted.
        sign(self._channels_path, 'image-signing.gpg')
        setup_keyrings()
        setup_keyring_txz(
            'image-signing.gpg', 'image-master.gpg', dict(type='blacklist'),
            os.path.join(self._serverdir, 'gpg', 'blacklist.tar.xz'))
        self._state.run_thru('get_channel')
        # We now have an image-signing key which is blacklisted.  This will
        # cause the state machine to try to download a new image signing key,
        # so let's put the cached one up on the server.  This will still be
        # backlisted though.
        config = Configuration(config_d)
        key_path = os.path.join(self._serverdir, 'gpg', 'image-signing.tar.xz')
        shutil.copy(config.gpg.image_signing, key_path)
        shutil.copy(config.gpg.image_signing + '.asc', key_path + '.asc')
        # Run the state machine through _get_channel() again, only this time
        # because the key is still blacklisted, we'll get an exception.
        self.assertRaises(SignatureError, self._state.run_thru, 'get_channel')
Ejemplo n.º 19
0
 def __init__(self, callback=None):
     self._state = State()
     self._config = config
     self._update = None
     self._channels = None
     self._callback = callback
Ejemplo n.º 20
0
class Mediator:
    """This is the DBus API mediator.

    It essentially implements the entire DBus API, but at a level below the
    mechanics of DBus.  Methods of this class are hooked directly into the
    DBus layer to satisfy that interface.
    """
    def __init__(self, callback=None):
        self._state = State()
        self._config = config
        self._update = None
        self._channels = None
        self._callback = callback

    def __repr__(self):  # pragma: no cover
        fmt = '<Mediator at 0x{:x} | State at 0x{:x} | Downloader at {}>'
        args = [
            id(self),
            id(self._state),
            'None' if self._state.downloader is None else '0x{:x}'.format(
                id(self._state.downloader))
        ]
        return fmt.format(*args)

    def cancel(self):
        self._state.downloader.cancel()

    def pause(self):
        self._state.downloader.pause()

    def resume(self):
        self._state.downloader.resume()

    def check_for_update(self):
        """Is there an update available for this machine?

        :return: Flag indicating whether an update is available or not.
        :rtype: bool
        """
        if self._update is None:
            try:
                self._state.run_until('download_files')
            except Exception as error:
                # Rather than letting this percolate up, eventually reaching
                # the GLib main loop and thus triggering apport, Let's log the
                # error and set the relevant information in the class.
                log.exception('check_for_update failed')
                self._update = Update(error=str(error))
            else:
                self._update = Update(self._state.winner)
                self._channels = list()
                for key in sorted(self._state.channels):
                    channel = self._state.channels[key]

                    # Ignore channels that are not installable on this device
                    if self._config.device not in channel["devices"]:
                        continue

                    self._channels.append(
                        dict(hidden=channel.get('hidden'),
                             alias=channel.get('alias'),
                             redirect=channel.get('redirect'),
                             name=key))
        return self._update

    def download(self):
        """Download the available update."""
        # We only want callback progress during the actual download.
        old_callbacks = self._state.downloader.callbacks[:]
        try:
            self._state.downloader.callbacks = [self._callback]
            self._state.run_until('apply')
        finally:
            self._state.downloader.callbacks = old_callbacks

    def apply(self):
        """Apply the update."""
        # Transition through all remaining states.
        list(self._state)

    def factory_reset(self):
        factory_reset()

    def production_reset(self):
        production_reset()

    def allow_gsm(self):
        self._state.downloader.allow_gsm()  # pragma: no curl

    def get_channels(self):
        """List channels which are installable on this device.

        This is not valid until check_for_update has succeeded.
        """
        if self._channels is None:
            log.warn("get_channels called before check_for_update succeeded!")
        return self._channels

    def set_channel(self, channel):
        found = False
        if self._channels:
            for key in self._channels:
                if key["name"] == channel:
                    found = True
                    self._config.channel = channel
                    break
        return found

    def set_build(self, build):
        self._config.build_number = build

    def get_channel(self):
        return self._config.channel

    def get_build(self):
        return self._config.build_number

    @property
    def supports_firmware_update(self):
        """Determines whether the system firmware can be updated using system-image

        :returns: ``True`` if firmware can be updated, ``False`` if it cannot.

        :rtype: bool
        """
        try:
            p = check_output(['afirmflasher', '-jd']).rstrip()
        except CalledProcessError as e:
            log.warning("afirmflasher returned non-zero exit status {}",
                        e.returncode)
            return False
        except OSError as e:
            # afirmflasher isn't installed so this device obviously doesn't support it
            return False

        try:
            return p.decode('utf8') == "OK"
        except Exception as e:
            log.warning(
                ("Exception occurred while checking whether this device",
                 " supports firmware update."))
            log.exception(e)
            return False

    def check_for_firmware_update(self):
        """Get information about available firmware updates

        :returns: JSON from afirmflasher with available update status
        """

        if not self.supports_firmware_update:
            log.error(("check_for_firmware_update called but device does not ",
                       "support firmware update."))
            return "ERR"

        try:
            p = check_output(['afirmflasher', '-jc']).rstrip()
        except CalledProcessError as e:
            log.warning("afirmflasher returned non-zero exit status {}",
                        e.returncode)
            return "ERR"
        except OSError as e:
            log.exception(e)
            return "ERR"

        try:
            json.loads(p.decode('utf8'))
            return p
        except Exception as e:
            log.warning(
                ("Exception occurred while checking whether this device",
                 " has a firmware update."))
            log.exception(e)
            return "ERR"

    def update_firmware(self):
        """Attempt to update system firmware

        :returns: JSON from afirmflasher with update results
        """

        if not self.supports_firmware_update:
            log.error(("check_for_firmware_update called but device does not ",
                       "support firmware update."))
            return "ERR"

        try:
            p = check_output(['afirmflasher', '-jf']).rstrip()
        except CalledProcessError as e:
            log.warning("afirmflasher returned non-zero exit status {}",
                        e.returncode)
            return "ERR"
        except OSError as e:
            log.exception(e)
            return "ERR"

        try:
            json.loads(p.decode('utf8'))
            return p
        except Exception as e:
            log.warning(
                "Exception occurred while attempting to update firmware")
            log.exception(e)
            return "ERR"

    def reboot(self):
        try:
            check_output(['/sbin/reboot']).rstrip()
        except CalledProcessError:
            log.error("Failed to reboot")
Ejemplo n.º 21
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')