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_copy_blob_install_grub_to_mbr(self): # Install GRUB to MBR # TODO: this has to be represented in the image.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_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 _populate_one_volume(self, name, volume): for partnum, part in enumerate(volume.structures): part_img = volume.part_images[partnum] part_dir = os.path.join(volume.basedir, 'part{}'.format(partnum)) if part.role is StructureRole.system_data: # The root partition needs to be ext4, which may or may not be # populated at creation time, depending on the version of # e2fsprogs. mkfs_ext4(part_img, self.rootfs, self.args.cmd, part.filesystem_label, preserve_ownership=True) elif part.filesystem is FileSystemType.none: image = Image(part_img, part.size) offset = 0 for content in part.content: src = os.path.join(self.unpackdir, 'gadget', content.image) file_size = os.path.getsize(src) assert content.size is None or content.size >= file_size, ( 'Spec size {} < actual size {} of: {}'.format( content.size, file_size, content.image)) if content.size is not None: file_size = content.size # TODO: We need to check for overlapping images. if content.offset is not None: offset = content.offset end = offset + file_size if end > part.size: if part.name is None: if part.role is None: whats_wrong = part.type else: whats_wrong = part.role.value else: whats_wrong = part.name part_path = 'volumes:<{}>:structure:<{}>'.format( name, whats_wrong) self.exitcode = 1 raise DoesNotFit(partnum, part_path, end - part.size) image.copy_blob(src, bs=1, seek=offset, conv='notrunc') offset += file_size elif part.filesystem is FileSystemType.vfat: sourcefiles = SPACE.join( os.path.join(part_dir, filename) for filename in os.listdir(part_dir)) env = dict(MTOOLS_SKIP_CHECK='1') env.update(os.environ) run('mcopy -s -i {} {} ::'.format(part_img, sourcefiles), env=env) elif part.filesystem is FileSystemType.ext4: mkfs_ext4(part_img, part_dir, self.args.cmd, part.filesystem_label) else: raise AssertionError('Invalid part filesystem type: {}'.format( part.filesystem))
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 _populate_one_volume(self, name, volume): for partnum, part in enumerate(volume.structures): part_img = volume.part_images[partnum] part_dir = os.path.join(volume.basedir, 'part{}'.format(partnum)) if part.role is StructureRole.system_data: # The root partition needs to be ext4, which may or may not be # populated at creation time, depending on the version of # e2fsprogs. mkfs_ext4(part_img, self.rootfs, self.args.cmd, part.filesystem_label, preserve_ownership=True) elif part.filesystem is FileSystemType.none: image = Image(part_img, part.size) offset = 0 for content in part.content: src = os.path.join(self.unpackdir, 'gadget', content.image) file_size = os.path.getsize(src) assert content.size is None or content.size >= file_size, ( 'Spec size {} < actual size {} of: {}'.format( content.size, file_size, content.image)) if content.size is not None: file_size = content.size # TODO: We need to check for overlapping images. if content.offset is not None: offset = content.offset end = offset + file_size if end > part.size: if part.name is None: if part.role is None: whats_wrong = part.type else: whats_wrong = part.role.value else: whats_wrong = part.name part_path = 'volumes:<{}>:structure:<{}>'.format( name, whats_wrong) self.exitcode = 1 raise DoesNotFit(partnum, part_path, end - part.size) image.copy_blob(src, bs=1, seek=offset, conv='notrunc') offset += file_size elif part.filesystem is FileSystemType.vfat: sourcefiles = SPACE.join( os.path.join(part_dir, filename) for filename in os.listdir(part_dir) ) env = dict(MTOOLS_SKIP_CHECK='1') env.update(os.environ) run('mcopy -s -i {} {} ::'.format(part_img, sourcefiles), env=env) elif part.filesystem is FileSystemType.ext4: mkfs_ext4(part_img, part_dir, self.args.cmd, part.filesystem_label) else: raise AssertionError('Invalid part filesystem type: {}'.format( part.filesystem))
def test_write_value_at_offsets_near_end(self): image = Image(self.img, 10000) # Attempt to write a bunch of values near the end of the file. Since # the value will always be a 32-bit value, any positions farther out # than 4 bytes before the end will fail. results = set() for pos in range(9995, 10002): with suppress(ValueError): image.write_value_at_offset(801, pos) self.assertEqual(os.path.getsize(self.img), 10000) results.add(pos) self.assertEqual(results, {9995, 9996})
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_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_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_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 _make_one_disk(self, imgfile, name, volume): part_id = 1 # Create the image object for the selected volume schema image = Image(imgfile, volume.image_size, volume.schema) offset_writes = [] part_offsets = {} # We first create all the needed partitions. # For regular core16 and core18 builds, this means creating all of the # defined partitions. For core20 (the so called 'seeded images'), we # only create all the role-less partitions and mbr + system-seed. # The rest is created dynamically by snapd on first boot. for part in volume.structures: if part.name is not None: part_offsets[part.name] = part.offset if part.offset_write is not None: offset_writes.append((part.offset, part.offset_write)) if (part.role is StructureRole.mbr or part.type == 'bare' or self._should_skip_partition(part)): continue activate = False if (volume.schema is VolumeSchema.mbr and part.role is StructureRole.system_boot): activate = True elif (volume.schema is VolumeSchema.gpt and part.role is StructureRole.system_data and part.name is None): part.name = 'writable' image.partition(part.offset, part.size, part.name, activate) # Now since we're done, we need to do a second pass to copy the data # and set all the partition types. This needs to be done like this as # libparted's commit() operation resets type GUIDs to defaults and # clobbers things like hybrid MBR partitions. part_id = 1 for i, part in enumerate(volume.structures): if self._should_skip_partition(part): continue image.copy_blob(volume.part_images[i], bs=image.sector_size, seek=part.offset // image.sector_size, count=ceil(part.size / image.sector_size), conv='notrunc') if part.role is StructureRole.mbr or part.type == 'bare': continue image.set_parition_type(part_id, part.type) part_id += 1 for value, dest in offset_writes: # Decipher non-numeric offset_write values. if isinstance(dest, tuple): dest = part_offsets[dest[0]] + dest[1] image.write_value_at_offset(value // image.sector_size, dest)
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_initialize_partition_table_mbr_too_small(self): # Creating a super small image should fail as there's no place for # a partition table. self.assertRaises(IOException, Image, self.img, 25, VolumeSchema.gpt) # ...but we can create an Image object without the partition table. image = Image(self.img, 25, None) self.assertTrue(os.path.exists(image.path)) self.assertEqual(os.stat(image.path).st_size, 25)
def test_partition(self): # Create BIOS boot partition # # The partition is 1MiB in size, as recommended by various # partitioning guides. The actual required size is much, much # smaller. image = Image(self.img, MiB(10)) image.partition(new='1:4MiB:+1MiB') image.partition(typecode='1:21686148-6449-6E6F-744E-656564454649') image.partition(change_name='1:grub') mbr = image.diagnostics(Diagnostics.mbr) # We should see that the disk size is 10MiB. self.assertRegex(mbr, '10.0 MiB') gpt = image.diagnostics(Diagnostics.gpt) # We should see that there is 1 partition named grub. self.assertRegex(gpt, 'grub')
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 populate_filesystems(self): 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 = self.boot_images[partnum] part_dir = os.path.join(self.workdir, 'part{}'.format(partnum)) if part.filesystem is FileSystemType.none: # pragma: nocover image = Image(part_img, part.size) offset = 0 for file in part.content: src = os.path.join(self.unpackdir, 'gadget', file.image) file_size = os.path.getsize(src) if file.size is not None and file.size < file_size: raise AssertionError('Size {} < size of {}'.format( file.size, file.image)) if file.size is not None: file_size = file.size # XXX: We need to check for overlapping images. if file.offset is not None: offset = file.offset # XXX: We must check offset+size vs. the target image. image.copy_blob(src, bs=1, seek=offset, conv='notrunc') offset += file_size elif part.filesystem is FileSystemType.vfat: # pragma: nobranch sourcefiles = SPACE.join( os.path.join(part_dir, filename) for filename in os.listdir(part_dir)) env = dict(MTOOLS_SKIP_CHECK='1') env.update(os.environ) run('mcopy -s -i {} {} ::'.format(part_img, sourcefiles), env=env) elif part.filesystem is FileSystemType.ext4: # pragma: nocover _mkfs_ext4(self.part_img, part_dir, part.filesystem_label) # The root partition needs to be ext4, which may or may not be # populated at creation time, depending on the version of e2fsprogs. _mkfs_ext4(self.root_img, self.rootfs) self._next.append(self.make_disk)
def _make_one_disk(self, imgfile, name, volume): part_id = 1 # Create the image object for the selected volume schema image = Image(imgfile, volume.image_size, volume.schema) offset_writes = [] part_offsets = {} # We first create all the partitions. for part in volume.structures: if part.name is not None: part_offsets[part.name] = part.offset if part.offset_write is not None: offset_writes.append((part.offset, part.offset_write)) if part.role is StructureRole.mbr or part.type == 'bare': continue activate = False if (volume.schema is VolumeSchema.mbr and part.role is StructureRole.system_boot): activate = True elif (volume.schema is VolumeSchema.gpt and part.role is StructureRole.system_data and part.name is None): part.name = 'writable' image.partition(part.offset, part.size, part.name, activate) # Now since we're done, we need to do a second pass to copy the data # and set all the partition types. This needs to be done like this as # libparted's commit() operation resets type GUIDs to defaults and # clobbers things like hybrid MBR partitions. part_id = 1 for i, part in enumerate(volume.structures): image.copy_blob(volume.part_images[i], bs=image.sector_size, seek=part.offset // image.sector_size, count=ceil(part.size / image.sector_size), conv='notrunc') if part.role is StructureRole.mbr or part.type == 'bare': continue image.set_parition_type(part_id, part.type) part_id += 1 for value, dest in offset_writes: # Decipher non-numeric offset_write values. if isinstance(dest, tuple): dest = part_offsets[dest[0]] + dest[1] image.write_value_at_offset(value // image.sector_size, dest)
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') # 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', }], })
#!/usr/bin/python3 from ubuntu_image.image import Diagnostics, GiB, Image # Create empty image of a fixed size # TODO: compute rough good size after seeing all the required snaps. image = Image('img', GiB(4)) # Install GRUB to MBR # TODO: this has to be represented in the image.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 image.copy_blob('blogs/img.mbr', bs=446, count=1, conv='notrunc') # Create BIOS boot partition # # The partition is 1MiB in size, as recommended by various partitioning guides. # The actual required size is much, much smaller. # # https://www.gnu.org/software/grub/manual/html_node/BIOS-installation.html#BIOS-installation image.partition(new='1:4MiB:+1MiB') image.partition(typecode='1:21686148-6449-6E6F-744E-656564454649') image.partition(change_name='1:grub') image.copy_blob('blobs/img.bios-boot', bs='1MiB', seek=4, count=1, conv='notrunc') # Create EFI system partition
def _populate_one_volume(self, name, volume): # For the LK bootloader we need to copy boot.img and snapbootsel.bin to # the gadget folder so they can be used as partition content. The first # one comes from the kernel snap, while the second one is modified by # 'snap prepare-image' to set the right core and kernel for the kernel # command line. if volume.bootloader is BootLoader.lk: boot = os.path.join(self.unpackdir, 'image', 'boot', 'lk') gadget = os.path.join(self.unpackdir, 'gadget') if os.path.isdir(boot): os.makedirs(gadget, exist_ok=True) for filename in os.listdir(boot): src = os.path.join(boot, filename) dst = os.path.join(gadget, filename) shutil.copy(src, dst) for partnum, part in enumerate(volume.structures): part_img = volume.part_images[partnum] # In seeded images, the system-seed partition is basically the # rootfs partition - at least from the ubuntu-image POV. if part.role is StructureRole.system_seed: part_dir = self.rootfs else: part_dir = os.path.join(volume.basedir, 'part{}'.format(partnum)) if part.role is StructureRole.system_data: # The root partition needs to be ext4, which may or may not be # populated at creation time, depending on the version of # e2fsprogs. mkfs_ext4(part_img, self.rootfs, self.args.cmd, part.filesystem_label, preserve_ownership=True) elif part.filesystem is FileSystemType.none: image = Image(part_img, part.size) offset = 0 for content in part.content: src = os.path.join(self.unpackdir, 'gadget', content.image) file_size = os.path.getsize(src) assert content.size is None or content.size >= file_size, ( 'Spec size {} < actual size {} of: {}'.format( content.size, file_size, content.image)) if content.size is not None: file_size = content.size # TODO: We need to check for overlapping images. if content.offset is not None: offset = content.offset end = offset + file_size if end > part.size: if part.name is None: if part.role is None: whats_wrong = part.type else: whats_wrong = part.role.value else: whats_wrong = part.name part_path = 'volumes:<{}>:structure:<{}>'.format( name, whats_wrong) self.exitcode = 1 raise DoesNotFit(partnum, part_path, end - part.size) image.copy_blob(src, bs=1, seek=offset, conv='notrunc') offset += file_size elif part.filesystem is FileSystemType.vfat: sourcefiles = SPACE.join( os.path.join(part_dir, filename) for filename in os.listdir(part_dir)) env = dict(MTOOLS_SKIP_CHECK='1') env.update(os.environ) run('mcopy -s -i {} {} ::'.format(part_img, sourcefiles), env=env) elif part.filesystem is FileSystemType.ext4: mkfs_ext4(part_img, part_dir, self.args.cmd, part.filesystem_label) else: raise AssertionError('Invalid part filesystem type: {}'.format( part.filesystem))
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)
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_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 make_disk(self): self.disk_img = os.path.join(self.images, 'disk.img') image = Image(self.disk_img, GiB(4)) # Create BIOS boot partition # # The partition is 1MiB in size, as recommended by various # partitioning guides. The actual required size is much, much # smaller. # # https://www.gnu.org/software/grub/manual/html_node/BIOS-installation.html#BIOS-installation # image.partition(new='1:4MiB:+1MiB') # image.partition(typecode='1:21686148-6449-6E6F-744E-656564454649') # image.partition(change_name='1:grub') # image.copy_blob(self.boot_img, # bs='1MiB', seek=4, count=1, conv='notrunc') # Create EFI system partition # # TODO: switch to 512MiB as recommended by the standard image.partition(new='2:5MiB:+64MiB') image.partition(typecode='2:C12A7328-F81F-11D2-BA4B-00A0C93EC93B') image.partition(change_name='2:system-boot') image.copy_blob(self.boot_img, bs='1MB', seek=4, count=64, conv='notrunc') # Create main snappy writable partition image.partition(new='3:72MiB:+3646MiB') image.partition(typecode='3:0FC63DAF-8483-4772-8E79-3D69D8477DE4') image.partition(change_name='3:writable') image.copy_blob(self.root_img, bs='1MiB', seek=72, count=3646, conv='notrunc') self._next.append(self.finish)
#!/usr/bin/python3 from ubuntu_image.image import Diagnostics, GiB, Image # Create empty image of a fixed size # TODO: compute rough good size after seeing all the required snaps. image = Image('img', GiB(4)) # Install GRUB to MBR # TODO: this has to be represented in the image.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 image.copy_blob('blogs/img.mbr', bs=446, count=1, conv='notrunc') # Create BIOS boot partition # # The partition is 1MiB in size, as recommended by various partitioning guides. # The actual required size is much, much smaller. # # https://www.gnu.org/software/grub/manual/html_node/BIOS-installation.html#BIOS-installation image.partition(new='1:4MiB:+1MiB') image.partition(typecode='1:21686148-6449-6E6F-744E-656564454649') image.partition(change_name='1:grub') image.copy_blob('blobs/img.bios-boot', bs='1MiB', seek=4, count=1, conv='notrunc') # Create EFI system partition # # TODO: switch to 512MiB as recommended by the standard image.partition(new='2:5MiB:+64MiB')
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_initialize(self): image = Image(self.img, GiB(1.25)) self.assertTrue(os.path.exists(image.path)) # GiB == 1024**3; 1.25GiB == 1342177280 bytes. self.assertEqual(os.stat(image.path).st_size, 1342177280)
def test_write_value_at_offset_past_end(self): image = Image(self.img, 10000) self.assertRaises(ValueError, image.write_value_at_offset, 801, 130031) # And the file's size hasn't changed. self.assertEqual(os.path.getsize(self.img), 10000)
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') 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 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 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', }], })