blob: c321ca749af991200efc1b01d731b5f97c781f17 [file] [log] [blame]
# Copyright 2022 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Test chromite.lib.disk_layout"""
import os
from chromite.lib import constants
from chromite.lib import cros_test_lib
from chromite.lib import disk_layout
from chromite.lib import osutils
class JSONLoadingTest(cros_test_lib.MockTempDirTestCase):
"""Test stacked JSON loading functions."""
def setUp(self):
self.layout_json = os.path.join(self.tempdir, "test_layout.json")
self.parent_layout_json = os.path.join(
self.tempdir, "test_layout_parent.json"
)
self.another_parent_layout_json = os.path.join(
self.tempdir, "test_layout_another_parent.json"
)
self.parent_layout_content = """{
"metadata": {
"fs_block_size": 4096
},
"layouts": {
"base": [
{
"num": 3,
"label": "Part 3",
"type": "rootfs"
},
{
"num": 2,
"label": "Part 2",
"type": "data"
},
{
"num": 1,
"label": "Part 1",
"type": "efi"
}
]
}
}"""
def testJSONComments(self):
"""Test that we ignore comments in JSON in lines starting with #."""
osutils.WriteFile(
self.layout_json,
"""# This line is a comment.
{
# comment with some whitespaces on the left.
"metadata": {
"fs_block_size": 4096
},
"layouts": {
"common": [],
"base": []
}
}
""",
)
layout = disk_layout.DiskLayout(self.layout_json)
# pylint: disable-msg=W0212
self.assertEqual(
layout._disk_layout_config,
{
"metadata": {"fs_block_size": 4096, "fs_align": 4096},
"layouts": {"common": [], "base": []},
},
)
def testJSONCommentsLimitations(self):
"""Test that we can't parse inline comments in JSON.
If we ever enable this, we need to change the README.disk_layout
documentation to mention it.
"""
osutils.WriteFile(
self.layout_json,
"""{
"layouts": { # This is an inline comment.
"common": []
}
}""",
)
self.assertRaises(
ValueError,
disk_layout.DiskLayout,
self.layout_json,
)
def testPartitionOrderPreserved(self):
"""Test the order of the partitions is the same as in the parent."""
osutils.WriteFile(self.parent_layout_json, self.parent_layout_content)
parent_layout = disk_layout.DiskLayout(self.parent_layout_json)
osutils.WriteFile(
self.layout_json,
"""{
"parent": "%s",
"layouts": {
"base": []
}
}"""
% self.parent_layout_json,
)
layout = disk_layout.DiskLayout(self.layout_json)
# pylint: disable-msg=W0212
self.assertEqual(
parent_layout._disk_layout_config, layout._disk_layout_config
)
# Test also that even overriding one partition keeps all of them in
# order.
osutils.WriteFile(
self.layout_json,
"""{
"parent": "%s",
"layouts": {
"base": [
{
"num": 2,
"label": "Part 2"
}
]
}
}"""
% self.parent_layout_json,
)
layout = disk_layout.DiskLayout(self.layout_json)
# pylint: disable-msg=W0212
self.assertEqual(
parent_layout._disk_layout_config, layout._disk_layout_config
)
def testJSONEmptyParent(self):
"""Test that absence of layout section in parent is supported."""
osutils.WriteFile(self.parent_layout_json, "{}")
osutils.WriteFile(
self.layout_json,
"""{
"parent": "%s",
"metadata": {
"fs_block_size": 4096
},
"layouts": {
"base": [
{
"num": 2,
"label": "Part 2",
"type": "rootfs"
}
]
}
}"""
% self.parent_layout_json,
)
disk_layout.DiskLayout(self.layout_json)
def testJSONEmptyLayout(self):
"""Test that absence of layout section in child is supported."""
osutils.WriteFile(self.parent_layout_json, self.parent_layout_content)
parent_layout = disk_layout.DiskLayout(self.parent_layout_json)
osutils.WriteFile(
self.layout_json,
"""{
"parent": "%s"
}"""
% self.parent_layout_json,
)
layout = disk_layout.DiskLayout(self.layout_json)
# pylint: disable-msg=W0212
self.assertEqual(
parent_layout._disk_layout_config, layout._disk_layout_config
)
def testJSONrequiredFields(self):
"""Test required fields."""
# Need Metadata field.
osutils.WriteFile(self.layout_json, "{}")
with self.assertRaisesRegex(
disk_layout.InvalidLayoutError,
"Layout is missing required entries: " "'metadata'",
):
disk_layout.DiskLayout(self.layout_json)
# Need fs_block_size in Metadata.
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {}
}""",
)
with self.assertRaisesRegex(
disk_layout.InvalidLayoutError,
"Layout is missing required entries: " "'fs_block_size'",
):
disk_layout.DiskLayout(self.layout_json)
# Need base layout.
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"fs_block_size": 4096
},
"layouts": {}
}""",
)
with self.assertRaisesRegex(
disk_layout.InvalidLayoutError, 'Missing "base" config.*'
):
disk_layout.DiskLayout(self.layout_json)
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"fs_block_size": 4096
},
"layouts": {
"common": []
}
}""",
)
with self.assertRaisesRegex(
disk_layout.InvalidLayoutError, 'Missing "base" config.*'
):
disk_layout.DiskLayout(self.layout_json)
def testJSONPartitionRequiredFields(self):
"""Test partition required fields."""
# Need partition type.
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"fs_block_size": 4096
},
"layouts": {
"base": [
{
"num": 1
}
]
}
}""",
)
with self.assertRaisesRegex(
disk_layout.InvalidLayoutError,
"Layout is missing required entries: " "'type'",
):
disk_layout.DiskLayout(self.layout_json)
# Need partition label.
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"fs_block_size": 4096
},
"layouts": {
"base": [
{
"num": 1,
"type": "data"
}
]
}
}""",
)
with self.assertRaisesRegex(
disk_layout.InvalidLayoutError, 'Layout "base" missing "label"'
):
disk_layout.DiskLayout(self.layout_json)
# unknown entry.
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"fs_block_size": 4096
},
"layouts": {
"base": [
{
"num": 1,
"type": "data",
"label": "rootfs",
"unknown": 1
}
]
}
}""",
)
with self.assertRaisesRegex(
disk_layout.InvalidLayoutError,
"Unknown items in layout base: " "{'unknown'}",
):
disk_layout.DiskLayout(self.layout_json)
def testJSONFileSystemFields(self):
"""Test filesystem fields."""
# fs_size > fs_size_min size.
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"fs_block_size": 4096
},
"layouts": {
"base": [
{
"num": 1,
"type": "data",
"label": "rootfs",
"size": "24 MB",
"fs_size": "3MB",
"fs_align": "2MB"
}
]
}
}""",
)
with self.assertRaisesRegex(
disk_layout.InvalidSizeError,
".*is not an even multiple of fs_align.*",
):
disk_layout.DiskLayout(self.layout_json)
# test fs_align in metadata
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"fs_block_size": 4096
},
"layouts": {
"base": []
}
}""",
)
layout = disk_layout.DiskLayout(self.layout_json)
# pylint: disable-msg=W0212
self.assertEqual(
layout._disk_layout_config["metadata"]["fs_align"], 4096
)
# test invalid fs_align in metadata
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"fs_block_size": 400,
"fs_align": 300
},
"layouts": {
"base": []
}
}""",
)
with self.assertRaisesRegex(
disk_layout.InvalidLayoutError, "fs_align.*"
):
disk_layout.DiskLayout(self.layout_json)
def testPartitionOrderPreservedWithBase(self):
"""Test the order of the partitions is the same as in the parent."""
osutils.WriteFile(
self.parent_layout_json,
"""{
"metadata": {
"fs_block_size": 4096
},
"layouts": {
"common": [
{
"num": 3,
"label": "Part 3",
"type": "data"
},
{
"num": 2,
"label": "Part 2",
"type": "data"
},
{
"num": 1,
"label": "Part 1",
"type": "data"
}
],
"base": []
}
}""",
)
parent_layout = disk_layout.DiskLayout(self.parent_layout_json)
# Test also that even overriding one partition keeps all of them in
# order.
osutils.WriteFile(
self.layout_json,
"""{
"parent": "%s",
"layouts": {
"common": [
{
"num": 2,
"label": "Part 2"
}
],
"base": [
{
"num": 1,
"label": "Part 1"
}
]
}
}"""
% self.parent_layout_json,
)
layout = disk_layout.DiskLayout(self.layout_json)
# pylint: disable-msg=W0212
self.assertEqual(
parent_layout._disk_layout_config, layout._disk_layout_config
)
def testGetTableTotalsSizeIsAccurate(self):
"""Test primary_entry_array_lba results in an accurate block count."""
test_params = (
# block_size, primary_entry_array_padding_bytes (in blocks),
# partition size (MiB)
(512, 2, 32),
(1024, 2, 32),
(512, 2, 64),
(512, 32768, 32),
(1024, 32768, 32),
(1024, 32768, 64),
)
for i in test_params:
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"block_size": %d,
"fs_block_size": 4096,
"primary_entry_array_padding_bytes": %d
},
"layouts": {
"base": [
{
"num": 1,
"label": "data",
"type": "blank",
"size": "%d MiB"
}
]
}
}"""
% (i[0], i[1] * i[0], i[2]),
)
layout = disk_layout.DiskLayout(self.layout_json)
# pylint: disable-msg=W0212
partitions = layout._image_partitions["base"]
totals = layout.GetTableTotals(partitions)
self.assertEqual(
totals["byte_count"],
disk_layout.START_SECTOR
+ i[1] * i[0]
+ sum([x["bytes"] for x in partitions])
+ disk_layout.SECONDARY_GPT_BYTES,
)
def testMultipleParents(self):
"""Test that multiple inheritance works."""
osutils.WriteFile(
self.parent_layout_json,
"""{
"metadata": {
"fs_block_size": 1000
},
"layouts": {
"common": [
{
"num": 1,
"label": "Part 1",
"type": "rootfs"
}
],
"base": [
{
"num": 2,
"label": "Part 2",
"type": "data",
"fs_size_min": 1000
},
{
"num": 12,
"label": "Part 12",
"type": "kernel",
"size": 2000,
"fs_size": 1000
}
]
}
}""",
)
osutils.WriteFile(
self.another_parent_layout_json,
"""{
"layouts": {
"common": [
{
"num": 1,
"label": "Part 1",
"type": "rootfs",
"size": 3000,
"fs_size_min": 2000
}
],
"base": [
{
"num": 2,
"label": "new Part 2",
"type": "data",
"fs_size_min": 2000
}
]
}
}""",
)
osutils.WriteFile(
self.layout_json,
"""{
"parent": "%s %s",
"layouts": {
"common": [
{
"num": 1,
"label": "new Part 1",
"size": 2000
}
]
}
}"""
% (self.parent_layout_json, self.another_parent_layout_json),
)
layout = disk_layout.DiskLayout(self.layout_json)
# pylint: disable-msg=W0212
self.assertDictEqual(
layout._disk_layout_config,
{
"layouts": {
"common": [
{
"num": 1,
"label": "new Part 1",
"type": "rootfs",
"fs_size_min": 2000,
"bytes": 2000,
"size": 2000,
"features": [],
}
],
"base": [
{
"num": 2,
"label": "new Part 2",
"type": "data",
"fs_size_min": 2000,
"bytes": 1,
"features": [],
},
{
"num": 12,
"label": "Part 12",
"type": "kernel",
"size": 2000,
"fs_size": 1000,
"fs_bytes": 1000,
"bytes": 2000,
"features": [],
},
{
"num": 1,
"label": "new Part 1",
"type": "rootfs",
"fs_size_min": 2000,
"bytes": 2000,
"size": 2000,
"features": [],
},
],
},
"metadata": {"fs_block_size": 1000, "fs_align": 1000},
},
)
def testGapPartitionsAreIncluded(self):
"""Test empty partitions (gaps) can be included in the child layout."""
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"fs_block_size": 1000
},
"layouts": {
"base": [
{
"num": 2,
"label": "Part 2",
"type": "rootfs"
},
{
# Pad out, but not sure why.
"type": "blank",
"size": "64 MiB"
},
{
"num": 1,
"label": "Part 1",
"type": "data"
}
]
}
}""",
)
layout = disk_layout.DiskLayout(self.layout_json)
# pylint: disable-msg=W0212
self.assertDictEqual(
layout._disk_layout_config,
{
"metadata": {"fs_block_size": 1000, "fs_align": 1000},
"layouts": {
"base": [
{
"num": 2,
"label": "Part 2",
"type": "rootfs",
"bytes": 1,
"features": [],
},
{
"type": "blank",
"size": "64 MiB",
"bytes": 67108864,
"features": [],
},
{
"num": 1,
"label": "Part 1",
"type": "data",
"bytes": 1,
"features": [],
},
],
"common": [],
},
},
)
def testPartitionOrderShouldMatch(self):
"""Test the partition order in parent and child layouts must match."""
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"fs_block_size": 1000
},
"layouts": {
"common": [
{"num": 1, "type": "rootfs", "label": "Part1"},
{"num": 2, "type": "data", "label": "Part2"}
],
"base": [
{"num": 2, "type": "data", "label": "Part2"},
{"num": 1, "type": "rootfs", "label": "Part1"}
]
}
}""",
)
with self.assertRaises(disk_layout.ConflictingPartitionOrderError):
disk_layout.DiskLayout(self.layout_json)
def testOnlySharedPartitionsOrderMatters(self):
"""Test that only the order of the partition in both layouts matters."""
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"fs_block_size": 1000
},
"layouts": {
"common": [
{"num": 1, "type": "rootfs", "label": "Part1"},
{"num": 2, "type": "data", "label": "Part2"},
{"num": 3, "type": "firmware", "label": "Part3"}
],
"base": [
{"num": 2, "type": "data", "label": "Part2"},
{"num": 5, "type": "bootloader", "label": "Part5"},
{"num": 3, "type": "firmware", "label": "Part3"},
{"num": 12, "type": "kernel", "label": "Part12"}
]
}
}""",
)
layout = disk_layout.DiskLayout(self.layout_json)
# pylint: disable-msg=W0212
self.assertDictEqual(
layout._disk_layout_config,
{
"metadata": {"fs_block_size": 1000, "fs_align": 1000},
"layouts": {
"common": [
{
"num": 1,
"type": "rootfs",
"label": "Part1",
"bytes": 1,
"features": [],
},
{
"num": 2,
"type": "data",
"label": "Part2",
"bytes": 1,
"features": [],
},
{
"num": 3,
"type": "firmware",
"label": "Part3",
"bytes": 1,
"features": [],
},
],
"base": [
{
"num": 1,
"type": "rootfs",
"label": "Part1",
"bytes": 1,
"features": [],
},
{
"num": 2,
"type": "data",
"label": "Part2",
"bytes": 1,
"features": [],
},
{
"num": 5,
"type": "bootloader",
"label": "Part5",
"bytes": 1,
"features": [],
},
{
"num": 3,
"type": "firmware",
"label": "Part3",
"bytes": 1,
"features": [],
},
{
"num": 12,
"type": "kernel",
"label": "Part12",
"bytes": 1,
"features": [],
},
],
},
},
)
def testFileSystemSizeMustBePositive(self):
"""Test that zero or negative file system size will raise exception."""
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"block_size": "512",
"fs_block_size": "4 KiB"
},
"layouts": {
"base": [
{
"num": 1,
"type": "rootfs",
"label": "ROOT-A",
"fs_size": "0 KiB"
}
]
}
}""",
)
with self.assertRaisesRegex(
disk_layout.InvalidSizeError, ".*must be positive"
):
disk_layout.DiskLayout(self.layout_json)
def testFileSystemSizeLargerThanPartition(self):
"""Test that file system size must not be greater than partition."""
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"block_size": "512",
"fs_block_size": "4 KiB"
},
"layouts": {
"base": [
{
"num": 1,
"type": "rootfs",
"label": "ROOT-A",
"size": "4 KiB",
"fs_size": "8 KiB"
}
]
}
}""",
)
with self.assertRaisesRegex(
disk_layout.InvalidSizeError, ".*may not be larger than partition.*"
):
disk_layout.DiskLayout(self.layout_json)
def testFileSystemSizeNotMultipleBlocks(self):
"""Test file system size must be multiples of file system blocks."""
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"block_size": "512",
"fs_block_size": "4 KiB"
},
"layouts": {
"base": [
{
"num": 1,
"type": "rootfs",
"label": "ROOT-A",
"size": "4 KiB",
"fs_size": "3 KiB"
}
]
}
}""",
)
with self.assertRaisesRegex(
disk_layout.InvalidSizeError, ".*not an even multiple of fs_align.*"
):
disk_layout.DiskLayout(self.layout_json)
def testFileSystemSizeForUbiWithNoPageSize(self):
"""Test that "page_size" must be present to calculate UBI fs size."""
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"block_size": "512",
"fs_block_size": "4 KiB"
},
"layouts": {
"base": [
{
"num": 1,
"type": "rootfs",
"format": "ubi",
"label": "ROOT-A",
"size": "4 KiB",
"fs_size": "4 KiB"
}
]
}
}""",
)
with self.assertRaisesRegex(
disk_layout.InvalidLayoutError, ".*page_size.*"
):
disk_layout.DiskLayout(self.layout_json)
def testFileSystemSizeForUbiWithNoEraseBlockSize(self):
"""Test "erase_block_size" must be present to calculate UBI fs size."""
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"block_size": "512",
"fs_block_size": "4 KiB"
},
"layouts": {
"base": [
{
"num": "metadata",
"page_size": "4 KiB"
},
{
"num": 1,
"type": "rootfs",
"format": "ubi",
"label": "ROOT-A",
"size": "4 KiB",
"fs_size": "4 KiB"
}
]
}
}""",
)
with self.assertRaisesRegex(
disk_layout.InvalidLayoutError, ".*erase_block_size.*"
):
disk_layout.DiskLayout(self.layout_json)
def testFileSystemSizeForUbiIsNotMultipleOfUbiEraseBlockSize(self):
"""Test that we raise when fs_size is not multiple of eraseblocks."""
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"block_size": "512",
"fs_block_size": "4 KiB"
},
"layouts": {
"base": [
{
"num": "metadata",
"page_size": "4 KiB",
"erase_block_size": "262144"
},
{
"num": 1,
"type": "rootfs",
"format": "ubi",
"label": "ROOT-A",
"size": "256 KiB",
"fs_size": "256 KiB"
}
]
}
}""",
)
with self.assertRaisesRegex(
disk_layout.InvalidSizeError,
'.*to "248 KiB" in the "common" layout.*',
):
disk_layout.DiskLayout(self.layout_json)
def testFileSystemSizeForUbiIsMultipleOfUbiEraseBlockSize(self):
"""Test everything is okay when fs_size is multiple of eraseblocks."""
osutils.WriteFile(
self.layout_json,
"""{
"metadata": {
"block_size": "512",
"fs_block_size": "4 KiB"
},
"layouts": {
"base": [
{
"num": "metadata",
"page_size": "4 KiB",
"erase_block_size": "262144"
},
{
"num": 1,
"type": "rootfs",
"format": "ubi",
"label": "ROOT-A",
"size": "256 KiB",
"fs_size": "253952"
}
]
}
}""",
)
layout = disk_layout.DiskLayout(self.layout_json)
# pylint: disable-msg=W0212
self.assertEqual(
layout._disk_layout_config,
{
"layouts": {
"base": [
{
"erase_block_size": 262144,
"features": [],
"num": "metadata",
"page_size": 4096,
"type": "blank",
},
{
"bytes": 262144,
"features": [],
"format": "ubi",
"fs_bytes": 253952,
"fs_size": "253952",
"label": "ROOT-A",
"num": 1,
"size": "256 KiB",
"type": "rootfs",
},
],
"common": [],
},
"metadata": {
"block_size": "512",
"fs_align": 4096,
"fs_block_size": 4096,
},
},
)
class UtilityTest(cros_test_lib.MockTestCase):
"""Test various utility functions in disk_layout.py."""
def testParseHumanNumber(self):
"""Test that ParseHumanNumber is correct."""
test_cases = [
("1", 1),
("2", 2),
("1KB", 1000),
("1KiB", 1024),
("1 K", 1024),
("1 KiB", 1024),
("3 MB", 3000000),
("4 MiB", 4 * 2**20),
("5GB", 5 * 10**9),
("6GiB", 6 * 2**30),
("7TB", 7 * 10**12),
("8TiB", 8 * 2**40),
("-4", -4),
("-5GB", -5 * 10**9),
("-6GiB", -6 * 2**30),
("-7TB", -7 * 10**12),
("-8TiB", -8 * 2**40),
]
for inp, exp in test_cases:
self.assertEqual(disk_layout.ParseHumanNumber(inp), exp)
def testParseHumanNumberInvalid(self):
"""Test that ParseHumanNumber raises exception for invalid input."""
test_cases = ["10uB", "30 BT", "40 Tg", "TiB", "-GB"]
for inp in test_cases:
with self.assertRaises(disk_layout.InvalidAdjustmentError):
disk_layout.ParseHumanNumber(inp)
def testProduceHumanNumber(self):
"""Test that ProduceHumanNumber is correct."""
test_cases = [
("1", 1),
("2", 2),
("1 KB", 1000),
("1 KiB", 1024),
("3 MB", 3 * 10**6),
("4 MiB", 4 * 2**20),
("5 GB", 5 * 10**9),
("6 GiB", 6 * 2**30),
("7 TB", 7 * 10**12),
("8 TiB", 8 * 2**40),
]
for exp, inp in test_cases:
self.assertEqual(disk_layout.ProduceHumanNumber(inp), exp)
def testGetScriptShell(self):
"""Verify GetScriptShell works."""
data = disk_layout.GetScriptShell()
self.assertIn("#!/bin/sh", data)
self.assertIn(
str(constants.CHROMITE_DIR / "sdk" / "cgpt_shell.sh"), data
)
def testParseProduce(self):
"""Test ParseHumanNumber(ProduceHumanNumber()) yields same value."""
test_cases = [
1,
2,
-2,
1000,
1024,
2 * 10**6,
2 * 2**20,
3 * 10**9,
3 * 2**30,
4 * 10**12,
4 * 2**40,
-4 * 2**40,
]
for n in test_cases:
self.assertEqual(
disk_layout.ParseHumanNumber(disk_layout.ProduceHumanNumber(n)),
n,
)
def testParseRelativeNumber(self):
"""Verify ParseRelativeNumber()."""
test_cases = [
((1000, "90%"), 900),
((1000, "-10"), 990),
((1000, "80"), 80),
((1000, "8 TiB"), 8 * 2**40),
]
for inp, output in test_cases:
self.assertEqual(
disk_layout.ParseRelativeNumber(inp[0], inp[1]), output
)