Exemplo n.º 1
0
    def test_minimal(self):
        gadget_spec = parse("""\
volumes:
  first-image:
    bootloader: u-boot
    structure:
        - type: 00000000-0000-0000-0000-0000deadbeef
          size: 400M
""")
        self.assertEqual(gadget_spec.device_tree_origin, 'gadget')
        self.assertIsNone(gadget_spec.device_tree)
        self.assertEqual(gadget_spec.volumes.keys(), {'first-image'})
        volume0 = gadget_spec.volumes['first-image']
        self.assertEqual(volume0.schema, VolumeSchema.gpt)
        self.assertEqual(volume0.bootloader, BootLoader.uboot)
        self.assertIsNone(volume0.id)
        self.assertEqual(len(volume0.structures), 1)
        structure0 = volume0.structures[0]
        self.assertIsNone(structure0.name)
        self.assertEqual(structure0.offset, MiB(1))
        self.assertIsNone(structure0.offset_write)
        self.assertEqual(structure0.size, MiB(400))
        self.assertEqual(
            structure0.type, UUID(hex='00000000-0000-0000-0000-0000deadbeef'))
        self.assertIsNone(structure0.id)
        self.assertEqual(structure0.filesystem, FileSystemType.none)
        self.assertIsNone(structure0.filesystem_label)
        self.assertEqual(len(structure0.content), 0)
Exemplo n.º 2
0
 def prepare_filesystems(self):
     self.images = os.path.join(self.workdir, '.images')
     os.makedirs(self.images)
     # The image for the boot partition.
     self.boot_images = []
     volumes = self.gadget.volumes.values()
     assert len(volumes) == 1, 'For now, only one volume is allowed'
     volume = list(volumes)[0]
     for partnum, part in enumerate(volume.structures):
         part_img = os.path.join(self.images, 'part{}.img'.format(partnum))
         self.boot_images.append(part_img)
         run('dd if=/dev/zero of={} count=0 bs={} seek=1'.format(
             part_img, part.size))
         if part.filesystem is FileSystemType.vfat:  # pragma: nobranch
             run('mkfs.vfat {}'.format(part_img))
         # XXX: Does not handle the case of partitions at the end of the
         # image.
         next_avail = part.offset + part.size
     # The image for the root partition.
     #
     # XXX: Hard-codes 4GB image size.   Hard-codes last sector for backup
     # GPT.
     avail_space = (4000000000 - next_avail - 4 * 1024) // MiB(1)
     if self.rootfs_size / MiB(1) > avail_space:  # pragma: nocover
         raise AssertionError('No room for root filesystem data')
     self.rootfs_size = avail_space
     self.root_img = os.path.join(self.images, 'root.img')
     run('dd if=/dev/zero of={} count=0 bs={}M seek=1'.format(
         self.root_img, avail_space))
     # We defer creating the root file system image because we have to
     # populate it at the same time.  See mkfs.ext4(8) for details.
     self._next.append(self.populate_filesystems)
Exemplo n.º 3
0
 def test_sector_conversion(self):
     # For empty non-partitioned images we default to a 512 sector size.
     image = Image(self.img, MiB(1))
     self.assertEqual(image.sector(10), 5120)
     # In case of using partitioning, be sure we use the sector size as
     # returned by pyparted.
     image = Image(self.img, MiB(5), VolumeSchema.mbr)
     self.assertEqual(image.sector(10), 10 * image.device.sectorSize)
Exemplo n.º 4
0
 def test_set_partition_type_mbr(self):
     image = Image(self.img, MiB(6), VolumeSchema.mbr)
     image.partition(offset=MiB(1), size=MiB(1))
     self.assertEqual(len(image.disk.partitions), 1)
     image.set_parition_type(1, '83')
     disk_info = image.diagnostics()
     self.assertEqual(disk_info['partitiontable']['partitions'][0]['type'],
                      '83')
     image.set_parition_type(1, 'da')
     disk_info = image.diagnostics()
     self.assertEqual(disk_info['partitiontable']['partitions'][0]['type'],
                      'da')
Exemplo n.º 5
0
 def test_set_partition_type_gpt(self):
     image = Image(self.img, MiB(6), VolumeSchema.gpt)
     image.partition(offset=MiB(1), size=MiB(1))
     self.assertEqual(len(image.disk.partitions), 1)
     image.set_parition_type(1, '21686148-6449-6E6F-744E-656564454649')
     disk_info = image.diagnostics()
     self.assertEqual(disk_info['partitiontable']['partitions'][0]['type'],
                      '21686148-6449-6E6F-744E-656564454649')
     image.set_parition_type(1, '00000000-0000-0000-0000-0000DEADBEEF')
     disk_info = image.diagnostics()
     self.assertEqual(disk_info['partitiontable']['partitions'][0]['type'],
                      '00000000-0000-0000-0000-0000DEADBEEF')
Exemplo n.º 6
0
 def test_image_size_option_suffixes(self):
     args = parseargs(['snap', '--image-size', '45G', 'model.assertion'])
     self.assertEqual(args.image_size, GiB(45))
     self.assertEqual(args.given_image_size, '45G')
     args = parseargs(['snap', '--image-size', '45M', 'model.assertion'])
     self.assertEqual(args.image_size, MiB(45))
     self.assertEqual(args.given_image_size, '45M')
Exemplo n.º 7
0
 def test_copy_blob_install_grub_to_mbr(self):
     # Install GRUB to MBR
     # TODO: this has to be represented in the gadget.yaml
     # NOTE: the boot.img has to be a part of the gadget snap itself
     # FIXME: embed a pointer to 2nd stage in bios-boot partition
     #
     # dd if=blobs/img.mbr of=img bs=446 count=1 conv=notrunc
     #
     # Start by creating a blob of the requested size.
     blob_file = os.path.join(self.tmpdir, 'mbr.blob')
     with open(blob_file, 'wb') as fp:
         fp.write(b'happyhappyjoyjoy' * 27)
         fp.write(b'happyhappyjoyj')
     self.assertEqual(os.stat(blob_file).st_size, 446)
     image = Image(self.img, MiB(1))
     image.copy_blob(blob_file, bs=446, count=1, conv='notrunc')
     # At the top of the image file, there should be 27 Stimpy
     # Exclamations, followed by a happyhappyjoyj.
     with open(image.path, 'rb') as fp:
         complete_stimpys = fp.read(432)
         partial_stimpys = fp.read(14)
         # Spot check.
         zeros = fp.read(108)
     self.assertEqual(complete_stimpys, b'happyhappyjoyjoy' * 27)
     self.assertEqual(partial_stimpys, b'happyhappyjoyj')
     # Stevens $4.13 - the extended file should read as zeros.
     self.assertEqual(zeros, b'\0' * 108)
Exemplo n.º 8
0
 def test_gpt_image_partitions(self):
     image = Image(self.img, MiB(10), VolumeSchema.gpt)
     image.partition(offset=MiB(4), size=MiB(1), name='grub')
     self.assertEqual(len(image.disk.partitions), 1)
     image.partition(offset=MiB(5), size=MiB(4))
     self.assertEqual(len(image.disk.partitions), 2)
     image.set_parition_type(1, '21686148-6449-6E6F-744E-656564454649')
     image.set_parition_type(2, '0FC63DAF-8483-4772-8E79-3D69D8477DE4')
     # Use an external tool for checking the partition table to be sure
     # that it's indeed correct as suspected.
     disk_info = image.diagnostics()
     partitions = disk_info['partitiontable']
     # The device id is unpredictable.
     partitions.pop('id')
     # Newer sfdisk displays an additional field of 'sectorsize' that
     # we're not really interested in.
     partitions.pop('sectorsize', None)
     # The partition uuids as well.
     [p.pop('uuid') for p in partitions['partitions']]
     self.maxDiff = None
     self.assertEqual(
         partitions, {
             'label':
             'gpt',
             'device':
             self.img,
             'unit':
             'sectors',
             'firstlba':
             34,
             'lastlba':
             20446,
             'partitions': [{
                 'node': '{}1'.format(self.img),
                 'start': 8192,
                 'size': 2048,
                 'type': '21686148-6449-6E6F-744E-656564454649',
                 'name': 'grub',
             }, {
                 'node': '{}2'.format(self.img),
                 'start': 10240,
                 'size': 8192,
                 'type': '0FC63DAF-8483-4772-8E79-3D69D8477DE4',
             }],
         })
Exemplo n.º 9
0
 def calculate_rootfs_size(self):
     # Calculate the size of the root file system.  Basically, I'm trying
     # to reproduce du(1) close enough without having to call out to it and
     # parse its output.
     #
     # On a 100MiB filesystem, ext4 takes a little over 7MiB for the
     # metadata.  Use 8MiB as a minimum padding here.
     self.rootfs_size = self._calculate_dirsize(self.rootfs) + MiB(8)
     self._next.append(self.pre_populate_bootfs_contents)
Exemplo n.º 10
0
 def test_write_value_at_offset(self):
     image = Image(self.img, MiB(2))
     image.write_value_at_offset(801, 130031)
     # Now open the path independently, seek to the given offset, and read
     # 4 bytes, then interpret it as a little-endian 32-bit integer.
     with open(image.path, 'rb') as fp:
         fp.seek(130031)
         # Unpack always returns a tuple, but there's only one item there.
         value, *ignore = unpack('<I', fp.read(4))
     self.assertEqual(value, 801)
Exemplo n.º 11
0
 def test_copy_blob_with_seek(self):
     # dd if=blobs/img.bios-boot of=img bs=1MiB seek=4 count=1 conv=notrunc
     blob_file = os.path.join(self.tmpdir, 'img.bios-boot')
     with open(blob_file, 'wb') as fp:
         fp.write(b'x' * 100)
     image = Image(self.img, MiB(2))
     image.copy_blob(blob_file, bs=773, seek=4, count=1, conv='notrunc')
     # The seek=4 skipped 4 blocks of 773 bytes.
     with open(image.path, 'rb') as fp:
         self.assertEqual(fp.read(3092), b'\0' * 3092)
         self.assertEqual(fp.read(100), b'x' * 100)
         self.assertEqual(fp.read(25), b'\0' * 25)
Exemplo n.º 12
0
 def test_small_partition_size_and_offset(self):
     # LP: #1630709 - structure parts with size and offset < 1MB.
     image = Image(self.img, MiB(2), VolumeSchema.mbr)
     image.partition(offset=256, size=512)
     disk_info = image.diagnostics()
     # Even though the offset and size are set at 256 bytes and 512 bytes
     # respectively, the minimum granularity is one sector (i.e. 512
     # bytes).  The start and size returned by diagnostics() are in sector
     # units.
     self.assertEqual(disk_info['partitiontable']['partitions'][0]['start'],
                      1)
     self.assertEqual(disk_info['partitiontable']['partitions'][0]['size'],
                      1)
Exemplo n.º 13
0
    def test_size_offset_suffix(self):
        gadget_spec = parse("""\
volumes:
  first-image:
    schema: gpt
    bootloader: u-boot
    structure:
        - type: 00000000-0000-0000-0000-0000deadbeef
          size: 3M
""")
        volume0 = gadget_spec.volumes['first-image']
        partition0 = volume0.structures[0]
        self.assertEqual(partition0.size, MiB(3))
Exemplo n.º 14
0
 def calculate_rootfs_size(self):
     # Calculate the size of the root file system.
     #
     # On a 100MiB filesystem, ext4 takes a little over 7MiB for the
     # metadata.  Use 8MiB as a minimum padding here.
     try:
         self.rootfs_size = self._calculate_dirsize(self.rootfs) + MiB(8)
     except CalledProcessError:
         if self.args.debug:
             _logger.exception('Full debug traceback follows')
         self.exitcode = 1
         # Stop the state machine right here by not appending a next step.
     else:
         self._next.append(self.pre_populate_bootfs_contents)
Exemplo n.º 15
0
 def test_mbr_image_partitions(self):
     image = Image(self.img, MiB(2), VolumeSchema.mbr)
     # Create the first partition.
     image.partition(offset=image.sector(33),
                     size=image.sector(3000),
                     is_bootable=True)
     self.assertEqual(len(image.disk.partitions), 1)
     # Append the next one.
     image.partition(offset=image.sector(3033), size=image.sector(1000))
     self.assertEqual(len(image.disk.partitions), 2)
     image.set_parition_type(1, '83')
     image.set_parition_type(2, 'dd')
     disk_info = image.diagnostics()
     partitions = disk_info['partitiontable']
     # The device id is unpredictable.
     partitions.pop('id')
     # XXX: In later versions of pyparted the partitiontable structure
     #  added a 'grain' entry that we're not really interested in.
     #  Remove it so we can have the tests working for all series.
     if 'grain' in partitions:
         partitions.pop('grain')
     # Newer sfdisk displays an additional field of 'sectorsize' that
     # we're not really interested in.
     partitions.pop('sectorsize', None)
     self.assertEqual(
         partitions, {
             'label':
             'dos',
             'device':
             self.img,
             'unit':
             'sectors',
             'partitions': [{
                 'node': '{}1'.format(self.img),
                 'start': 33,
                 'size': 3000,
                 'type': '83',
                 'bootable': True,
             }, {
                 'node': '{}2'.format(self.img),
                 'start': 3033,
                 'size': 1000,
                 'type': 'dd',
             }],
         })
Exemplo n.º 16
0
    def test_content_spec_b_size_suffix(self):
        gadget_spec = parse("""\
volumes:
  first-image:
    schema: gpt
    bootloader: u-boot
    structure:
        - type: 00000000-0000-0000-0000-0000deadbeef
          size: 400M
          content:
          - image: foo.img
            size: 1M
""")
        volume0 = gadget_spec.volumes['first-image']
        partition0 = volume0.structures[0]
        self.assertEqual(len(partition0.content), 1)
        content0 = partition0.content[0]
        self.assertEqual(content0.image, 'foo.img')
        self.assertIsNone(content0.offset)
        self.assertIsNone(content0.offset_write)
        self.assertEqual(content0.size, MiB(1))
Exemplo n.º 17
0
 def test_mbr_image_partitions(self):
     image = Image(self.img, MiB(2), VolumeSchema.mbr)
     # Create the first partition.
     image.partition(offset=image.sector(33),
                     size=image.sector(3000),
                     is_bootable=True)
     self.assertEqual(len(image.disk.partitions), 1)
     # Append the next one.
     image.partition(offset=image.sector(3033), size=image.sector(1000))
     self.assertEqual(len(image.disk.partitions), 2)
     image.set_parition_type(1, '83')
     image.set_parition_type(2, 'dd')
     disk_info = image.diagnostics()
     partitions = disk_info['partitiontable']
     # The device id is unpredictable.
     partitions.pop('id')
     self.assertEqual(
         partitions, {
             'label':
             'dos',
             'device':
             self.img,
             'unit':
             'sectors',
             'partitions': [{
                 'node': '{}1'.format(self.img),
                 'start': 33,
                 'size': 3000,
                 'type': '83',
                 'bootable': True,
             }, {
                 'node': '{}2'.format(self.img),
                 'start': 3033,
                 'size': 1000,
                 'type': 'dd',
             }],
         })
Exemplo n.º 18
0
def parse(stream_or_string):
    """Parse the YAML read from the stream or string.

    The YAML is parsed and validated against the schema defined in
    docs/gadget-yaml.rst.

    :param stream_or_string: Either a string or a file-like object containing
        a gadget.yaml specification.  If stream is given, it must be open for
        reading with a UTF-8 encoding.
    :type stream_or_string: str or file-like object
    :return: A specification of the gadget.
    :rtype: GadgetSpec
    :raises ValueError: If the schema is violated.
    """
    # Do the basic schema validation steps.  There some interdependencies that
    # require post-validation.  E.g. you cannot define the fs-type if the role
    # is ESP.
    stream = (StringIO(stream_or_string)
              if isinstance(stream_or_string, str)
              else stream_or_string)
    yaml = load(stream, Loader=StrictLoader)
    validated = GadgetYAML(yaml)
    device_tree_origin = validated.get('device-tree-origin')
    device_tree = validated.get('device-tree')
    volume_specs = {}
    bootloader_seen = False
    # This item is a dictionary so it can't possibly have duplicate keys.
    # That's okay because our StrictLoader above will already raise an
    # exception if it sees a duplicate key.
    for image_name, image_spec in validated['volumes'].items():
        schema = image_spec['schema']
        bootloader = image_spec.get('bootloader')
        bootloader_seen |= (bootloader is not None)
        image_id = image_spec.get('id')
        structures = []
        last_offset = 0
        for structure in image_spec['structure']:
            name = structure.get('name')
            offset = structure.get('offset')
            offset_write = structure.get('offset-write')
            size = structure['size']
            structure_type = structure['type']
            # Validate structure types.  These can be either GUIDs, two hex
            # digits, hybrids, or the special 'mbr' type.  The basic syntactic
            # validation happens above in the Voluptuous schema, but here we
            # need to ensure cross-attribute constraints.  Specifically,
            # hybrids and 'mbr' are allowed for either schema, but GUID-only
            # is only allowed for GPT, while 2-digit-only is only allowed for
            # MBR.  Note too that 2-item tuples are also already ensured.
            if (isinstance(structure_type, UUID) and
                    schema is not VolumeSchema.gpt):
                raise ValueError('GUID structure type with non-GPT')
            elif (isinstance(structure_type, str) and
                    structure_type != 'mbr' and
                    schema is not VolumeSchema.mbr):
                raise ValueError('MBR structure type with non-MBR')
            # XXX: Ensure the special case of the 'mbr' type doesn't extend
            # beyond the confines of the mbr.
            if not offset and structure_type != 'mbr' and last_offset < MiB(1):
                offset = MiB(1)
            if not offset:
                offset = last_offset
            last_offset = offset + size
            # Extract the rest of the structure data.
            structure_id = structure.get('id')
            filesystem = structure['filesystem']
            if (structure_type == 'mbr' and
                    filesystem is not FileSystemType.none):
                raise ValueError('mbr type must not specify a file system')
            filesystem_label = structure.get('filesystem-label', name)
            content = structure.get('content')
            content_specs = []
            content_spec_class = (
                ContentSpecB if filesystem is FileSystemType.none
                else ContentSpecA)
            if content is not None:
                for item in content:
                    content_specs.append(content_spec_class.from_yaml(item))
            structures.append(StructureSpec(
                name, offset, offset_write, size,
                structure_type, structure_id, filesystem, filesystem_label,
                content_specs))
        volume_specs[image_name] = VolumeSpec(
            schema, bootloader, image_id, structures)
    if not bootloader_seen:
        raise ValueError('No bootloader volume named')
    return GadgetSpec(device_tree_origin, device_tree, volume_specs)
Exemplo n.º 19
0
    def make_disk(self):
        self.disk_img = os.path.join(self.images, 'disk.img')
        part_id = 1
        # Walk through all partitions and write them to the disk image at the
        # lowest permissible offset.  We should not have any overlapping
        # partitions, the parser should have already rejected such as invalid.
        #
        # XXX: The parser should sort these partitions for us in disk order as
        # part of checking for overlaps, so we should not need to sort them
        # here.
        volumes = self.gadget.volumes.values()
        assert len(volumes) == 1, 'For now, only one volume is allowed'
        volume = list(volumes)[0]
        # XXX: This ought to be a single constructor that figures out the
        # class for us when we pass in the schema.
        if volume.schema == VolumeSchema.mbr:
            image = MBRImage(self.disk_img, 4000000000)
        else:
            image = Image(self.disk_img, 4000000000)

        structures = sorted(volume.structures, key=attrgetter('offset'))
        offset_writes = []
        part_offsets = {}
        for i, part in enumerate(structures):
            if part.name:  # pragma: nocover
                part_offsets[part.name] = part.offset
            if part.offset_write:  # pragma: nocover
                offset_writes.append((part.offset, part.offset_write))
            image.copy_blob(self.boot_images[i],
                            bs='1M',
                            seek=part.offset // MiB(1),
                            count=ceil(part.size / MiB(1)),
                            conv='notrunc')
            if part.type == 'mbr':
                continue  # pragma: nocover
            # sgdisk takes either a sector or a KiB/MiB argument; assume
            # that the offset and size are always multiples of 1MiB.
            partdef = '{}M:+{}M'.format(part.offset // MiB(1),
                                        part.size // MiB(1))
            part_args = {}
            part_args['new'] = partdef
            part_args['typecode'] = part.type
            # XXX: special-casing.
            if (volume.schema == VolumeSchema.mbr
                    and part.filesystem_label == 'system-boot'):
                part_args['activate'] = True
            if part.name is not None:  # pragma: nobranch
                part_args['change_name'] = part.name
            image.partition(part_id, **part_args)
            part_id += 1
            next_offset = (part.offset + part.size) // MiB(1)
        # Create main snappy writable partition
        image.partition(part_id,
                        new='{}M:+{}M'.format(next_offset, self.rootfs_size),
                        typecode=('83',
                                  '0FC63DAF-8483-4772-8E79-3D69D8477DE4'))
        if volume.schema == VolumeSchema.gpt:
            image.partition(part_id, change_name='writable')
        image.copy_blob(self.root_img,
                        bs='1M',
                        seek=next_offset,
                        count=self.rootfs_size,
                        conv='notrunc')
        for value, dest in offset_writes:  # pragma: nobranch
            # decipher non-numeric offset_write values
            if isinstance(dest, tuple):  # pragma: nocover
                dest = part_offsets[dest[0]] + dest[1]
            # XXX: Hard-coding of 512-byte sectors.
            image.write_value_at_offset(value // 512, dest)
        self._next.append(self.finish)
Exemplo n.º 20
0
def parse(stream_or_string):
    """Parse the YAML read from the stream or string.

    The YAML is parsed and validated against the schema defined in
    docs/gadget-yaml.rst.

    :param stream_or_string: Either a string or a file-like object containing
        a gadget.yaml specification.  If stream is given, it must be open for
        reading with a UTF-8 encoding.
    :type stream_or_string: str or file-like object
    :return: A specification of the gadget.
    :rtype: GadgetSpec
    :raises GadgetSpecificationError: If the schema is violated.
    """
    # Do the basic schema validation steps.  There some interdependencies that
    # require post-validation.  E.g. you cannot define the fs-type if the role
    # is ESP.
    stream = (StringIO(stream_or_string)
              if isinstance(stream_or_string, str) else stream_or_string)
    try:
        yaml = load(stream, Loader=StrictLoader)
    except (ParserError, ScannerError) as error:
        raise GadgetSpecificationError(
            'gadget.yaml file is not valid YAML') from error
    try:
        validated = GadgetYAML(yaml)
    except Invalid as error:
        if len(error.path) == 0:
            raise GadgetSpecificationError('Empty gadget.yaml')
        path = COLON.join(str(component) for component in error.path)
        # It doesn't look like voluptuous gives us the bogus value, but it
        # does give us the path to it.  The str(error) contains some
        # additional information of dubious value, so just use the path.
        raise GadgetSpecificationError('Invalid gadget.yaml @ {}'.format(path))
    device_tree_origin = validated.get('device-tree-origin')
    device_tree = validated.get('device-tree')
    defaults = validated.get('defaults')
    format = validated.get('format')
    volume_specs = {}
    bootloader_seen = False
    sector_size = get_default_sector_size()
    # These two variables only exist to support backwards compatibility in the
    # single-volume, implicit-root-fs case, and are ignored when multiple
    # volumes are defined.  We have no b/c considerations for implicit-root-fs
    # in the multi-volume case.
    rootfs_seen = False
    # For UC20 a new gadget layout is used, in which only the seed partition
    # needs to be explicitly created by ubuntu-image.  In ubuntu-image we will
    # call this state as 'seeded'.
    is_seeded = False
    farthest_offset = 0
    # This item is a dictionary so it can't possibly have duplicate keys.
    # That's okay because our StrictLoader above will already raise an
    # exception if it sees a duplicate key.
    for image_name, image_spec in validated['volumes'].items():
        schema = image_spec['schema']
        bootloader = image_spec.get('bootloader')
        bootloader_seen |= (bootloader is not None)
        image_id = image_spec.get('id')
        structures = []
        structure_names = set()
        last_offset = 0
        for structure in image_spec['structure']:
            name = structure.get('name')
            if name is not None:
                if name in structure_names:
                    raise GadgetSpecificationError(
                        'Structure name "{}" is not unique'.format(name))
                structure_names.add(name)
            offset = structure.get('offset')
            offset_write = structure.get('offset-write')
            size = structure['size']
            structure_type = structure['type']
            structure_role = structure.get('role')
            # Structure types and roles work together to define how the
            # structure is laid out on disk, along with any disk partitions
            # wrapping the structure.  In general, the type field names the
            # disk partition type code for the wrapping partition, and it will
            # either be a GUID for GPT disk schemas, a two hex digit string
            # for MBR disk schemas, or a hybrid type where a tuple-like string
            # names both type codes.
            #
            # The role specifies how the structure is to be used.  It may be a
            # partition holding boot assets, or a partition holding the
            # operating system data.  Without a role specification, we drop
            # back to the filesystem label to determine this.
            #
            # There are two complications.  Disks can have a special
            # non-partition wrapped Master Boot Record section on the disk
            # containing bootstrapping code.  MBRs must start at offset 0 and
            # be no larger than 446 bytes (there is some variability for other
            # MBR layouts, but 446 is the max).  The Wikipedia page has some
            # good diagrams: https://en.wikipedia.org/wiki/Master_boot_record
            #
            # MBR sections are identified by a role:mbr key, and in that case,
            # the gadget.yaml may not include a type field (since the MBR
            # isn't a partition).
            #
            # Some use cases involve putting bootstrapping code at other
            # locations on the disk, with offsets other than zero and
            # arbitrary sizes.  This bootstrapping code is also not wrapped in
            # a disk partition.  For these, type:none is the way to specify
            # that, but in that case, you cannot include a role key.
            # Technically speaking though, role:mbr is allowed, but somewhat
            # redundant.  All other roles with type:none are prohibited.
            #
            # For backward compatibility, we still allow the type:mbr field,
            # which is exactly equivalent to the preferred role:mbr field,
            # however a deprecation warning is issued in the former case.
            if structure_type == 'mbr':
                if structure_role is not None:
                    raise GadgetSpecificationError(
                        'Type mbr and role fields assigned at the same time, '
                        'please use the mbr role instead')
                warn(
                    "volumes:<volume name>:structure:<N>:type = 'mbr' is "
                    'deprecated; use role instead', DeprecationWarning)
                structure_role = StructureRole.mbr
            # For now, the structure type value can be of several Python
            # types. 1) a UUID for GPT schemas; 2) a 2-letter str for MBR
            # schemas; 3) a 2-tuple of #1 and #2 for mixed schemas; 4) the
            # special strings 'mbr' and 'bare' which can appear for either GPT
            # or MBR schemas.  type:mbr is deprecated and will eventually go
            # away.  What we're doing here is some simple validation of #1 and
            # #2.
            if (isinstance(structure_type, UUID)
                    and schema is not VolumeSchema.gpt):
                raise GadgetSpecificationError(
                    'MBR structure type with non-MBR schema')
            elif structure_type == 'bare':
                if structure_role not in (None, StructureRole.mbr):
                    raise GadgetSpecificationError(
                        'Invalid gadget.yaml: structure role/type conflict')
            elif (isinstance(structure_type, str)
                  and structure_role is not StructureRole.mbr
                  and schema is not VolumeSchema.mbr):
                raise GadgetSpecificationError(
                    'GUID structure type with non-GPT schema')
            # Check for implicit vs. explicit partition offset.
            if offset is None:
                # XXX: Ensure the special case of the mbr role doesn't
                # extend beyond the confines of the mbr.
                if (structure_role is not StructureRole.mbr
                        and last_offset < MiB(1)):
                    offset = MiB(1)
                else:
                    offset = last_offset
            # Extract the rest of the structure data.
            structure_id = structure.get('id')
            filesystem = structure['filesystem']
            if structure_role is StructureRole.mbr:
                if size > 446:
                    raise GadgetSpecificationError(
                        'mbr structures cannot be larger than 446 bytes.')
                if offset != 0:
                    raise GadgetSpecificationError(
                        'mbr structure must start at offset 0')
                if structure_id is not None:
                    raise GadgetSpecificationError(
                        'mbr structures must not specify partition id')
                if filesystem is not FileSystemType.none:
                    raise GadgetSpecificationError(
                        'mbr structures must not specify a file system')
            else:
                # Size and offset constraints on other partitions mandate
                # sector size alignment.
                if (size % sector_size) != 0 or (offset % sector_size) != 0:
                    # Provide some hint as to which partition is unaligned.
                    # Only the structure type is required, but if the name or
                    if name is None:
                        if structure_role is None:
                            whats_wrong = 'type {}'.format(structure_type)
                        else:
                            whats_wrong = 'role {}'.format(
                                structure_role.value)
                    else:
                        whats_wrong = name

                    _logger.warning(
                        'Partition {} size/offset need to be a multiple of '
                        'sector size ({}).  The size/offset will be rounded '
                        'up to the nearest sector.'.format(
                            whats_wrong, sector_size))
            last_offset = offset + size
            farthest_offset = max(farthest_offset, last_offset)
            filesystem_label = structure.get('filesystem-label', name)
            # Support the legacy mode setting of partition roles through
            # filesystem labels.
            if structure_role is None:
                if filesystem_label == 'system-boot':
                    structure_role = StructureRole.system_boot
                    warn(
                        'volumes:<volume name>:structure:<N>:filesystem_label'
                        ' used for defining partition roles; use role '
                        'instead.', DeprecationWarning)
            elif structure_role is StructureRole.system_data:
                rootfs_seen = True
                # For images to work the system-data (rootfs) partition needs
                # to have the 'writable' filesystem label set.
                # For UC20 this requirement no longer stands.
                if (filesystem_label not in (None, 'writable')
                        and not is_seeded):
                    raise GadgetSpecificationError(
                        '`role: system-data` structure must have an implicit '
                        "label, or 'writable': {}".format(filesystem_label))
            elif structure_role is StructureRole.system_seed:
                # The seed is good enough as a rootfs, snapd will create the
                # writable partition on demand
                rootfs_seen = True
                # Also, since the gadget.yaml defines a system-seed partition,
                # we can consider the image to be 'seeded'.  This basically
                # changes the u-i build mechanism to only create the
                # system-seed partition + all the the mbr/role-less partitions
                # defined on the gadget.  All the others (system-boot,
                # system-data etc.) will be created by snapd.
                is_seeded = True
                # Check if there is a filesystem label defined and, if not,
                # use the implicit 'ubuntu-seed' label.
                if filesystem_label is None:
                    filesystem_label = 'ubuntu-seed'
            # The content will be one of two formats, and no mixing is
            # allowed.  I.e. even though multiple content sections are allowed
            # in a single structure, they must all be of type A or type B.  If
            # the filesystem type is vfat or ext4, then type A *must* be used;
            # likewise if filesystem is none or missing, type B must be used.
            content = structure.get('content')
            content_specs = []
            if content is not None:
                if filesystem is FileSystemType.none:
                    for item in content:
                        try:
                            spec = ContentSpecB.from_yaml(item)
                        except KeyError:
                            raise GadgetSpecificationError(
                                'filesystem: none missing image file name')
                        else:
                            content_specs.append(spec)
                else:
                    for item in content:
                        try:
                            spec = ContentSpecA.from_yaml(item)
                        except KeyError:
                            raise GadgetSpecificationError(
                                'filesystem: vfat|ext4 missing source/target')
                        else:
                            content_specs.append(spec)
            structures.append(
                StructureSpec(name, offset, offset_write, size, structure_type,
                              structure_id, structure_role, filesystem,
                              filesystem_label, content_specs))
        # Sort structures by their offset.
        volume_specs[image_name] = VolumeSpec(schema, bootloader, image_id,
                                              structures)
        # Sanity check the partition offsets to ensure that there is no
        # overlap conflict where a part's offset begins before the previous
        # part's end.
        last_end = -1
        for part in sorted(structures, key=attrgetter('offset')):
            if part.offset < last_end:
                raise GadgetSpecificationError(
                    'Structure conflict! {}: {} <  {}'.format(
                        part.type if part.name is None else part.name,
                        part.offset, last_end))
            last_end = part.offset + part.size
    if not rootfs_seen and len(volume_specs) == 1:
        # We still need to handle the case of unspecified system-data
        # partition where we simply attach the rootfs at the end of the
        # partition list.
        #
        # Since so far we have no knowledge of the rootfs contents, the
        # size is set to 0, knowing that the builder code will resize it
        # to fit all the contents.
        warn(
            'No role: system-data partition found, a implicit rootfs '
            'partition will be appended at the end of the partition '
            'list.  An explicit system-data partition is now required.',
            DeprecationWarning)
        structures.append(
            StructureSpec(
                None,  # name
                farthest_offset,
                None,  # offset, offset_write
                None,  # size; None == calculate
                (  # type; hybrid mbr/gpt
                    '83', '0FC63DAF-8483-4772-8E79-3D69D8477DE4'),
                None,
                StructureRole.system_data,  # id, role
                FileSystemType.ext4,  # file system type
                'writable',  # file system label
                []))  # contents
    if not bootloader_seen:
        raise GadgetSpecificationError('No bootloader structure named')
    return GadgetSpec(device_tree_origin, device_tree, volume_specs, defaults,
                      format, is_seeded)
Exemplo n.º 21
0
 def test_m(self):
     self.assertEqual(as_size('25M'), MiB(25))
Exemplo n.º 22
0
 def test_device_schema_required(self):
     # With no schema, the device cannot be partitioned.
     image = Image(self.img, MiB(1))
     self.assertRaises(TypeError, image.partition, 256, 512)
Exemplo n.º 23
0
 def test_initialize_smaller(self):
     image = Image(self.img, MiB(4.5))
     self.assertTrue(os.path.exists(image.path))
     # MiB == 1024**2; 4.5MiB == 4718592 bytes.
     self.assertEqual(os.stat(image.path).st_size, 4718592)
Exemplo n.º 24
0
 def test_initialize_partition_table_mbr(self):
     image = Image(self.img, MiB(10), VolumeSchema.mbr)
     self.assertTrue(os.path.exists(image.path))
     self.assertEqual(os.stat(image.path).st_size, 10485760)
     self.assertEqual(image.disk.type, 'msdos')
     self.assertEqual(image.schema, VolumeSchema.mbr)