Exemple #1
0
 def test_get(self):
     # You can get a single key.  If missing, None or a supplied default is
     # returned.
     bag = Bag(c=1, b=2, a=3)
     self.assertEqual(bag.get('b'), 2)
     self.assertIsNone(bag.get('missing'))
     missing = object()
     self.assertIs(bag.get('missing', missing), missing)
Exemple #2
0
 def test_update(self):
     # Bags can be updated, similar to dicts.
     bag = Bag(a=1, b=2, c=3)
     bag.update(b=7, d=9)
     self.assertEqual(bag.a, 1)
     self.assertEqual(bag.b, 7)
     self.assertEqual(bag.c, 3)
     self.assertEqual(bag.d, 9)
Exemple #3
0
def _parse_device_mappings(device_mapping):
    devices = {}
    # e.g. keys: nexus7, nexus4
    for device_name, mapping_1 in device_mapping.items():
        # Most of the keys at this level (e.g. index) have flat values,
        # however the keyring key is itself a mapping.
        keyring = mapping_1.pop('keyring', None)
        if keyring is not None:
            mapping_1['keyring'] = Bag(**keyring)
        # e.g. nexus7 -> {index, keyring}
        devices[device_name] = Bag(**mapping_1)
    return Bag(**devices)
Exemple #4
0
 def test_pickle(self):
     # Bags can be pickled and unpickled.
     bag = Bag(a=1, b=2, c=3)
     pck = pickle.dumps(bag)
     new_bag = pickle.loads(pck)
     self.assertEqual(new_bag.a, 1)
     self.assertEqual(new_bag.b, 2)
     self.assertEqual(new_bag.c, 3)
Exemple #5
0
 def from_json(cls, data):
     """Parse the JSON data and produce an index."""
     mapping = json.loads(data)
     # Parse the global data, of which there is only the timestamp.  Even
     # though the string will contain 'UTC' (which we assert is so since we
     # can only handle UTC timestamps), strptime() will return a naive
     # datetime.  We'll turn it into an aware datetime in UTC, which is the
     # only thing that can possibly make sense.
     timestamp_str = mapping['global']['generated_at']
     assert 'UTC' in timestamp_str.split(), 'timestamps must be UTC'
     naive_generated_at = datetime.strptime(timestamp_str, IN_FMT)
     generated_at = naive_generated_at.replace(tzinfo=timezone.utc)
     global_ = Bag(generated_at=generated_at)
     # Parse the images.
     images = []
     for image_data in mapping['images']:
         # Descriptions can be any of:
         #
         # * description
         # * description-xx (e.g. description-en)
         # * description-xx_CC (e.g. description-en_US)
         #
         # We want to preserve the keys exactly as given, and because the
         # extended forms are not Python identifiers, we'll pull these out
         # into a separate, non-Bag dictionary.
         descriptions = {}
         # We're going to mutate the dictionary during iteration.
         for key in list(image_data):
             if key.startswith('description'):
                 descriptions[key] = image_data.pop(key)
         files = image_data.pop('files', [])
         bundles = [Bag(**bundle_data) for bundle_data in files]
         image = Image(files=bundles,
                       descriptions=descriptions,
                       **image_data)
         images.append(image)
     return cls(global_=global_, images=images)
Exemple #6
0
 def from_json(cls, data):
     mapping = json.loads(data)
     channels = {}
     for channel_name, mapping_1 in mapping.items():
         hidden = mapping_1.pop('hidden', None)
         if hidden is None:
             hidden = False
         else:
             assert hidden in (True, False), (
                 "Unexpected value for 'hidden': {}".format(hidden))
         mapping_1['hidden'] = hidden
         device_mapping = mapping_1.pop('devices')
         mapping_1['devices'] = _parse_device_mappings(device_mapping)
         channels[channel_name] = Bag(**mapping_1)
     return cls(**channels)
Exemple #7
0
 def test_update_converter_overrides(self):
     # Converters in the update method permanently override ctor converters.
     converters = dict(a=int, b=int)
     bag = Bag(converters=converters, a='1', b='2')
     self.assertEqual(bag.a, 1)
     self.assertEqual(bag.b, 2)
     new_converters = dict(a=str)
     bag.update(converters=new_converters, a='3', b='4')
     self.assertEqual(bag.a, '3')
     self.assertEqual(bag.b, 4)
     bag.update(a='5', b='6')
     self.assertEqual(bag.a, '5')
     self.assertEqual(bag.b, 6)
Exemple #8
0
 def test_dash_translation(self):
     # Dashes in keys get turned into underscore in attributes.
     bag = Bag(**{'a-b': 1, 'c-d': 2, 'e-f': 3})
     self.assertEqual(bag.a_b, 1)
     self.assertEqual(bag.c_d, 2)
     self.assertEqual(bag.e_f, 3)
Exemple #9
0
 def test_converters(self):
     # The Bag ctor accepts a mapping of type converter functions.
     bag = Bag(converters=dict(a=int, b=int), a='1', b='2', c='3')
     self.assertEqual(bag.a, 1)
     self.assertEqual(bag.b, 2)
     self.assertEqual(bag.c, '3')
Exemple #10
0
 def test_add_new_key(self):
     # A key added by setitem can be changed.
     bag = Bag(a=1, b=2, c=3)
     bag['d'] = 4
     bag['d'] = 5
     self.assertEqual(bag.d, 5)
Exemple #11
0
 def test_add_existing_key(self):
     # A key set in the original ctor cannot be changed.
     bag = Bag(a=1, b=2, c=3)
     self.assertRaises(ValueError, setitem, bag, 'b', 5)
     self.assertEqual(bag.b, 2)
Exemple #12
0
 def test_add_key(self):
     # We can add new keys/attributes via setitem.
     bag = Bag(a=1, b=2, c=3)
     bag['d'] = bag.b + bag.c
     self.assertEqual(bag.d, 5)
Exemple #13
0
 def test_original(self):
     # There's a magical attribute containing the original ctor arguments.
     source = {'a-b': 1, 'global': 2, 'foo': 3}
     bag = Bag(**source)
     self.assertEqual(bag.__original__, source)
Exemple #14
0
 def test_repr(self):
     # The repr of a bag includes its translated keys.
     bag = Bag(**{'a-b': 1, 'global': 2, 'foo': 3})
     self.assertEqual(repr(bag), '<Bag: a_b, foo, global_>')
Exemple #15
0
 def test_keyword_translation(self):
     # Python keywords get a trailing underscore.
     bag = Bag(**{'global': 1, 'with': 2, 'import': 3})
     self.assertEqual(bag.global_, 1)
     self.assertEqual(bag.with_, 2)
     self.assertEqual(bag.import_, 3)
Exemple #16
0
 def _set_defaults(self):
     self.service = Bag(
         base='system-image.ubports.com',
         http_port=80,
         https_port=443,
         channel='daily',
         build_number=0,
     )
     self.system = Bag(
         timeout=as_timedelta('1h'),
         tempdir='/tmp',
         logfile='/var/log/system-image/client.log',
         loglevel=as_loglevel('info'),
         settings_db='/var/lib/system-image/settings.db',
     )
     self.gpg = Bag(
         archive_master='/usr/share/system-image/archive-master.tar.xz',
         image_master='/var/lib/system-image/keyrings/image-master.tar.xz',
         image_signing='/var/lib/system-image/keyrings/image-signing.tar.xz',
         device_signing=
         '/var/lib/system-image/keyrings/device-signing.tar.xz',
     )
     self.updater = Bag(
         cache_partition='/android/cache/recovery',
         data_partition='/var/lib/system-image',
     )
     self.hooks = Bag(
         device=as_object('systemimage.device.SystemProperty'),
         scorer=as_object('systemimage.scores.WeightedScorer'),
         apply=as_object('systemimage.apply.Reboot'),
     )
     self.dbus = Bag(lifetime=as_timedelta('10m'), )
Exemple #17
0
 def test_update_converters(self):
     # The update method also accepts converters.
     bag = Bag(a=1, b=2, c=3)
     bag.update(converters=dict(d=int), d='4', e='5')
     self.assertEqual(bag.d, 4)
     self.assertEqual(bag.e, '5')
Exemple #18
0
 def test_simple(self):
     # Initialize a bag; its attributes are the keywords of the ctor.
     bag = Bag(a=1, b=2, c=3)
     self.assertEqual(bag.a, 1)
     self.assertEqual(bag.b, 2)
     self.assertEqual(bag.c, 3)
Exemple #19
0
 def test_iter(self):
     # Iteration is over the available keys.
     bag = Bag(c=1, b=2, a=3)
     self.assertEqual(sorted(bag, reverse=True), ['c', 'b', 'a'])
Exemple #20
0
 def test_keys(self):
     bag = Bag(c=1, b=2, a=3)
     self.assertEqual(sorted(bag.keys()), ['a', 'b', 'c'])
Exemple #21
0
 def test_dash_literal_access(self):
     # For keys with dashes, the original name is preserved in getitem.
     bag = Bag(**{'a-b': 1, 'c-d': 2, 'e-f': 3})
     self.assertEqual(bag['a-b'], 1)
     self.assertEqual(bag['c-d'], 2)
     self.assertEqual(bag['e-f'], 3)
Exemple #22
0
class Configuration:
    def __init__(self, directory=None):
        self._set_defaults()
        # Because the configuration object is a global singleton, it makes for
        # a convenient place to stash information used by widely separate
        # components.  For example, this is a placeholder for rendezvous
        # between the downloader and the D-Bus service.  When running under
        # D-Bus and we get a `paused` signal from the download manager, we need
        # this to plumb through an UpdatePaused signal to our clients.  It
        # rather sucks that we need a global for this, but I can't get the
        # plumbing to work otherwise.  This seems like the least horrible place
        # to stash this global.
        self.dbus_service = None
        # These are used to plumb command line arguments from the main() to
        # other parts of the system.
        self.skip_gpg_verification = False
        self.override_gsm = False
        # Cache.
        self._device = None
        self._build_number = None
        self.build_number_override = False
        self._channel = None
        # This is used only to override the phased percentage via command line
        # and the property setter.
        self._phase_override = None
        self._tempdir = None
        self.config_d = None
        self.ini_files = []
        self.http_base = None
        self.https_base = None
        if directory is not None:
            self.load(directory)
        self._calculate_http_bases()
        self._resources = ExitStack()
        atexit.register(self._resources.close)

    def _set_defaults(self):
        self.service = Bag(
            base='system-image.ubports.com',
            http_port=80,
            https_port=443,
            channel='daily',
            build_number=0,
        )
        self.system = Bag(
            timeout=as_timedelta('1h'),
            tempdir='/tmp',
            logfile='/var/log/system-image/client.log',
            loglevel=as_loglevel('info'),
            settings_db='/var/lib/system-image/settings.db',
        )
        self.gpg = Bag(
            archive_master='/usr/share/system-image/archive-master.tar.xz',
            image_master='/var/lib/system-image/keyrings/image-master.tar.xz',
            image_signing='/var/lib/system-image/keyrings/image-signing.tar.xz',
            device_signing=
            '/var/lib/system-image/keyrings/device-signing.tar.xz',
        )
        self.updater = Bag(
            cache_partition='/android/cache/recovery',
            data_partition='/var/lib/system-image',
        )
        self.hooks = Bag(
            device=as_object('systemimage.device.SystemProperty'),
            scorer=as_object('systemimage.scores.WeightedScorer'),
            apply=as_object('systemimage.apply.Reboot'),
        )
        self.dbus = Bag(lifetime=as_timedelta('10m'), )

    def _load_file(self, path):
        parser = SafeConfigParser()
        str_path = str(path)
        parser.read(str_path)
        self.ini_files.append(path)
        self.service.update(converters=dict(
            http_port=as_port,
            https_port=as_port,
            build_number=int,
            device=as_stripped,
        ),
                            **parser['service'])
        self.system.update(converters=dict(timeout=as_timedelta,
                                           loglevel=as_loglevel,
                                           settings_db=expand_path,
                                           tempdir=expand_path),
                           **parser['system'])
        self.gpg.update(**parser['gpg'])
        self.updater.update(**parser['updater'])
        self.hooks.update(converters=dict(device=as_object,
                                          scorer=as_object,
                                          apply=as_object),
                          **parser['hooks'])
        self.dbus.update(converters=dict(lifetime=as_timedelta),
                         **parser['dbus'])

    def load(self, directory):
        """Load up the configuration from a config.d directory."""
        # Look for all the files in the given directory with .ini or .cfg
        # suffixes.  The files must start with a number, and the files are
        # loaded in numeric order.
        if self.config_d is not None:
            raise RuntimeError('Configuration already loaded; use .reload()')
        self.config_d = directory
        if not Path(directory).is_dir():
            raise TypeError(
                '.load() requires a directory: {}'.format(directory))
        candidates = []
        for child in Path(directory).glob('*.ini'):
            order, _, base = child.stem.partition('_')
            # XXX 2014-10-03: The logging system isn't initialized when we get
            # here, so we can't log that these files are being ignored.
            if len(_) == 0:
                continue
            try:
                serial = int(order)
            except ValueError:
                continue
            candidates.append((serial, child))
        for serial, path in sorted(candidates):
            self._load_file(path)
        self._calculate_http_bases()

    def reload(self):
        """Reload the configuration directory."""
        # Reset some cached attributes.
        directory = self.config_d
        self.ini_files = []
        self.config_d = None
        self._build_number = None
        # Now load the defaults, then reload the previous config.d directory.
        self._set_defaults()
        self.load(directory)

    def _calculate_http_bases(self):
        if (self.service.http_port is NO_PORT
                and self.service.https_port is NO_PORT):
            raise ValueError('Cannot disable both http and https ports')
        # Construct the HTTP and HTTPS base urls, which most applications will
        # actually use.  We do this in two steps, in order to support disabling
        # one or the other (but not both) protocols.
        if self.service.http_port == 80:
            http_base = 'http://{}'.format(self.service.base)
        elif self.service.http_port is NO_PORT:
            http_base = None
        else:
            http_base = 'http://{}:{}'.format(self.service.base,
                                              self.service.http_port)
        # HTTPS.
        if self.service.https_port == 443:
            https_base = 'https://{}'.format(self.service.base)
        elif self.service.https_port is NO_PORT:
            https_base = None
        else:
            https_base = 'https://{}:{}'.format(self.service.base,
                                                self.service.https_port)
        # Sanity check and final settings.
        if http_base is None:
            assert https_base is not None
            http_base = https_base
        if https_base is None:
            assert http_base is not None
            https_base = http_base
        self.http_base = http_base
        self.https_base = https_base

    @property
    def build_number(self):
        if self._build_number is None:
            self._build_number = self.service.build_number
        return self._build_number

    @build_number.setter
    def build_number(self, value):
        if not isinstance(value, int):
            raise ValueError('integer is required, got: {}'.format(
                type(value).__name__))
        self._build_number = value
        self.build_number_override = True

    @build_number.deleter
    def build_number(self):
        self._build_number = None

    @property
    def device(self):
        if self._device is None:
            # Start by looking for a [service]device setting.  Use this if it
            # exists, otherwise fall back to calling the hook.
            self._device = getattr(self.service, 'device', None)
            if not self._device:
                self._device = self.hooks.device().get_device()
        return self._device

    @device.setter
    def device(self, value):
        self._device = value

    @property
    def channel(self):
        if self._channel is None:
            self._channel = self.service.channel
        return self._channel

    @channel.setter
    def channel(self, value):
        self._channel = value

    @property
    def phase_override(self):
        return self._phase_override

    @phase_override.setter
    def phase_override(self, value):
        self._phase_override = max(0, min(100, int(value)))

    @phase_override.deleter
    def phase_override(self):
        self._phase_override = None

    @property
    def tempdir(self):
        if self._tempdir is None:
            makedirs(self.system.tempdir)
            self._tempdir = self._resources.enter_context(
                temporary_directory(prefix='system-image-',
                                    dir=self.system.tempdir))
        return self._tempdir

    @property
    def user_agent(self):
        return USER_AGENT.format(self)