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)
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)
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)
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')
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')
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')
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)
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', }], })
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)
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)
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)
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)
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))
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)
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', }], })
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))
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', }], })
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)
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)
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)
def test_m(self): self.assertEqual(as_size('25M'), MiB(25))
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)
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)
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)