blob: 286e114fe4f8d480b765304c8c0158f84b0ea228 [file] [log] [blame]
# Copyright 2024 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Converts ChromeOS disk image to LVM stateful."""
import contextlib
import logging
import math
from pathlib import Path
import tempfile
from typing import Iterator, List, Optional
import uuid
from chromite.lib import commandline
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import image_lib
from chromite.lib import osutils
def get_parser() -> commandline.ArgumentParser:
"""Creates an argument parser for this script."""
parser = commandline.ArgumentParser(description=__doc__)
parser.add_argument(
"--from-image",
required=True,
type="file_exists",
help="Path to chromiumos_test_image.bin.",
)
parser.add_argument(
"--to-image",
required=True,
type="path",
help="Destination image name.",
)
return parser
@contextlib.contextmanager
def activate_lvm_vg(lvm_vg_name: str) -> Iterator[None]:
"""Context manager for activating an LVM volume group.
Without deactivation we won't be able to cleanly detach the loopback device.
Args:
lvm_vg_name: Name of the LVM volume group to activate.
Yields:
None. This context manager is used for its side effects.
"""
cros_build_lib.sudo_run(["vgchange", "-ay", lvm_vg_name])
try:
yield
finally:
cros_build_lib.sudo_run(["vgchange", "-an", lvm_vg_name])
@contextlib.contextmanager
def create_lvm_state_partition(image_name: str) -> Iterator[Path]:
"""Creates a logical volume management (LVM) partition inside for stateful.
Args:
image_name: Name of the ChromeOS image.
Yields:
An unencrypted partition device name that's valid until the context
manager exits.
"""
with contextlib.ExitStack() as stack:
logging.info("Creating logical volume setup...")
loop = stack.enter_context(image_lib.LoopbackPartitions(image_name))
partition_size = loop.GetPartitionInfo(constants.PART_STATE).size
lvm_dev = loop.GetPartitionDevName(constants.PART_STATE)
cros_build_lib.sudo_run(["pvcreate", "-ff", "--yes", lvm_dev])
lvm_vg_name = str(uuid.uuid4())
cros_build_lib.sudo_run(["vgcreate", "-p", "1", lvm_vg_name, lvm_dev])
stack.enter_context(activate_lvm_vg(lvm_vg_name))
# https://crsrc.org/o/src/platform2/chromeos-common-script/share/lvm-utils.sh;l=59-64;drc=a8f359bd28881351a729cc6069f1646522b9aee3
thinpool_size = math.floor(partition_size * 98 / (100 * 1024 * 1024))
ret = cros_build_lib.sudo_run(
[
"thin_metadata_size",
"--block-size",
"4k",
"--pool-size",
f"{thinpool_size}M",
"--max-thins",
"200",
"--numeric-only",
"-u",
"M",
],
capture_output=True,
encoding="utf-8",
)
thinpool_metadata_size = ret.stdout.strip()
cros_build_lib.sudo_run(
[
"lvcreate",
"--zero",
"n",
"--size",
f"{thinpool_size}M",
"--poolmetadatasize",
f"{thinpool_metadata_size}M",
"--thinpool",
"thinpool",
f"{lvm_vg_name}/thinpool",
]
)
# https://crsrc.org/o/src/platform2/chromeos-common-script/share/lvm-utils.sh;l=79;drc=a8f359bd28881351a729cc6069f1646522b9aee3
lv_size = math.floor(thinpool_size * 95 / 100)
unencrypted_name = "unencrypted"
cros_build_lib.sudo_run(
[
"lvcreate",
"--thin",
"-V",
f"{lv_size}M",
"-n",
unencrypted_name,
f"{lvm_vg_name}/thinpool",
]
)
# We use paths the LVM tools guarantee for us.
# https://man7.org/linux/man-pages/man8/lvm.8.html#VALID_NAMES
unencrypted_partition_device_name = (
f"/dev/{lvm_vg_name}/{unencrypted_name}"
)
cros_build_lib.sudo_run(
["mkfs.ext4", unencrypted_partition_device_name]
)
yield unencrypted_partition_device_name
def convert_to_lvm_stateful(in_image_name: Path, out_image_name: Path) -> int:
"""Converts a CrOS image to an LVM stateful format.
Takes a source CrOS image, converts it to an LVM stateful format,
and writes it to the specified destination.
Args:
in_image_name: A string representing the path to the source CrOS image.
out_image_name: A string representing the path where the converted image
should be saved.
Returns:
A return code indicating whether the conversion was successful (0 means
success).
"""
with contextlib.ExitStack() as stack:
in_loop = stack.enter_context(
image_lib.LoopbackPartitions(in_image_name)
)
lvm_temp_in_mount_dir = in_loop.Mount([constants.PART_STATE])[0]
logging.info("Creating a copy of the image...")
cros_build_lib.run(
[
"dd",
f"if={in_image_name}",
f"of={out_image_name}",
"conv=sparse",
"bs=2M",
]
)
lvm_unencrypted_partition_device_name = stack.enter_context(
create_lvm_state_partition(out_image_name)
)
logging.info("Copying files from original stateful filesystem...")
lvm_temp_out_mount_dir = stack.enter_context(
tempfile.TemporaryDirectory(prefix="lvm_temp_out_mount_")
)
stack.enter_context(
osutils.MountDirContext(
lvm_unencrypted_partition_device_name, lvm_temp_out_mount_dir
)
)
cros_build_lib.sudo_run(
[
"cp",
"--sparse=auto",
"-a",
f"{lvm_temp_in_mount_dir}/.",
lvm_temp_out_mount_dir,
]
)
return 0
def main(argv: Optional[List[str]] = None) -> Optional[int]:
parser = get_parser()
opts = parser.parse_args(argv)
opts.Freeze()
return convert_to_lvm_stateful(opts.from_image, opts.to_image)