add support for 'dd' image sources

I think only a core dd image source will work for now. Probably.
This commit is contained in:
Michael Hudson-Doyle 2023-08-02 22:17:54 +12:00
parent d77ac16dd0
commit 0395b6e9b0
10 changed files with 73 additions and 10 deletions

View File

@ -4,7 +4,7 @@
locale_support: none locale_support: none
name: name:
en: Ubuntu Server en: Ubuntu Server
path: ubuntu-core.squashfs path: pc.img
size: 530485248 size: 530485248
type: fsimage type: 'dd-raw:file'
variant: core variant: core

View File

@ -351,6 +351,8 @@ class GuidedCapability(enum.Enum):
CORE_BOOT_PREFER_ENCRYPTED = enum.auto() CORE_BOOT_PREFER_ENCRYPTED = enum.auto()
CORE_BOOT_PREFER_UNENCRYPTED = enum.auto() CORE_BOOT_PREFER_UNENCRYPTED = enum.auto()
DD = enum.auto()
def is_lvm(self) -> bool: def is_lvm(self) -> bool:
return self in [GuidedCapability.LVM, GuidedCapability.LVM_LUKS] return self in [GuidedCapability.LVM, GuidedCapability.LVM_LUKS]

View File

@ -1259,6 +1259,7 @@ class FilesystemModel(object):
self.bootloader = bootloader self.bootloader = bootloader
self.storage_version = 1 self.storage_version = 1
self._probe_data = None self._probe_data = None
self.dd_target: Optional[Disk] = None
self.reset() self.reset()
def reset(self): def reset(self):
@ -1718,6 +1719,18 @@ class FilesystemModel(object):
return r return r
def render(self, mode: ActionRenderMode = ActionRenderMode.DEFAULT): def render(self, mode: ActionRenderMode = ActionRenderMode.DEFAULT):
if self.dd_target is not None:
return {
"partitioning_commands": {
"builtin": [
"curtin",
"block-meta",
"simple",
"--devices",
self.dd_target.path,
],
},
}
config = { config = {
"storage": { "storage": {
"version": self.storage_version, "version": self.storage_version,

View File

@ -186,6 +186,19 @@ class VariationInfo:
), ),
) )
@classmethod
def dd(cls, name: str, min_size: int):
return cls(
name=name,
label=None,
min_size=min_size,
capability_info=CapabilityInfo(
allowed=[
GuidedCapability.DD,
]
),
)
class FilesystemController(SubiquityController, FilesystemManipulator): class FilesystemController(SubiquityController, FilesystemManipulator):
endpoint = API.storage endpoint = API.storage
@ -363,6 +376,11 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
info = self.info_for_system(name, label, system) info = self.info_for_system(name, label, system)
if info is not None: if info is not None:
self._variation_info[name] = info self._variation_info[name] = info
elif catalog_entry.type.startswith("dd-"):
min_size = variation.size
self._variation_info[name] = VariationInfo.dd(
name=name, min_size=min_size
)
else: else:
self._variation_info[name] = VariationInfo.classic( self._variation_info[name] = VariationInfo.classic(
name=name, min_size=variation.size name=name, min_size=variation.size
@ -418,6 +436,9 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
spec = dict(fstype="ext4", mount="/") spec = dict(fstype="ext4", mount="/")
self.create_partition(device=gap.device, gap=gap, spec=spec) self.create_partition(device=gap.device, gap=gap, spec=spec)
def guided_dd(self, disk: ModelDisk):
self.model.dd_target = disk
def guided_lvm(self, gap, choice: GuidedChoiceV2): def guided_lvm(self, gap, choice: GuidedChoiceV2):
device = gap.device device = gap.device
part_align = device.alignment_data().part_align part_align = device.alignment_data().part_align
@ -552,6 +573,7 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
async def guided( async def guided(
self, choice: GuidedChoiceV2, reset_partition_only: bool = False self, choice: GuidedChoiceV2, reset_partition_only: bool = False
) -> None: ) -> None:
self.model.dd_target = None
if choice.capability == GuidedCapability.MANUAL: if choice.capability == GuidedCapability.MANUAL:
return return
@ -602,6 +624,8 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
self.guided_zfs(gap, choice) self.guided_zfs(gap, choice)
elif choice.capability == GuidedCapability.DIRECT: elif choice.capability == GuidedCapability.DIRECT:
self.guided_direct(gap) self.guided_direct(gap)
elif choice.capability == GuidedCapability.DD:
self.guided_dd(disk)
else: else:
raise ValueError("cannot process capability") raise ValueError("cannot process capability")
@ -1218,6 +1242,9 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
capability = GuidedCapability.LVM_LUKS capability = GuidedCapability.LVM_LUKS
else: else:
capability = GuidedCapability.LVM capability = GuidedCapability.LVM
elif name == "dd":
capability = GuidedCapability.DD
assert mode == "reformat_disk"
elif name == "zfs": elif name == "zfs":
capability = GuidedCapability.ZFS capability = GuidedCapability.ZFS
else: else:
@ -1274,6 +1301,8 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
) )
await self.run_autoinstall_guided(self.ai_data["layout"]) await self.run_autoinstall_guided(self.ai_data["layout"])
elif "config" in self.ai_data: elif "config" in self.ai_data:
if self.app.base_model.source.current.type.startswith("dd-"):
raise Exception("must not use config: when installing a disk image")
# XXX in principle should there be a way to influence the # XXX in principle should there be a way to influence the
# variation chosen here? Not with current use cases for # variation chosen here? Not with current use cases for
# variations anyway. # variations anyway.
@ -1342,10 +1371,20 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
self.ensure_probing() self.ensure_probing()
def make_autoinstall(self): def make_autoinstall(self):
rendered = self.model.render() if self.model.dd_target is None:
r = {"config": rendered["storage"]["config"]} rendered = self.model.render()
if "swap" in rendered: r = {"config": rendered["storage"]["config"]}
r["swap"] = rendered["swap"] if "swap" in rendered:
r["swap"] = rendered["swap"]
else:
r = {
"layout": {
"name": "dd",
"match": {
"path": self.model.dd_target.path,
},
},
}
return r return r
async def _pre_shutdown(self): async def _pre_shutdown(self):

View File

@ -125,7 +125,7 @@ class InstallController(SubiquityController):
"""Return configuration to be used as part of a curtin 'block-meta' """Return configuration to be used as part of a curtin 'block-meta'
step.""" step."""
cfg = self.model.filesystem.render(mode=mode) cfg = self.model.filesystem.render(mode=mode)
if device_map_path is not None: if "storage" in cfg and device_map_path is not None:
cfg["storage"]["device_map_path"] = str(device_map_path) cfg["storage"]["device_map_path"] = str(device_map_path)
return cfg return cfg
@ -318,6 +318,7 @@ class InstallController(SubiquityController):
step_config=self.filesystem_config( step_config=self.filesystem_config(
device_map_path=logs_dir / "device-map.json", device_map_path=logs_dir / "device-map.json",
), ),
source=source,
) )
await run_curtin_step( await run_curtin_step(
name="extract", name="extract",

View File

@ -134,7 +134,7 @@ class SourceController(SubiquityController):
handler = get_handler_for_source( handler = get_handler_for_source(
sanitize_source(self.model.get_source(variation_name)) sanitize_source(self.model.get_source(variation_name))
) )
if self.app.opts.dry_run: if handler is not None and self.app.opts.dry_run:
handler = TrivialSourceHandler("/") handler = TrivialSourceHandler("/")
return handler return handler

View File

@ -361,6 +361,7 @@ class TestGuided(IsolatedAsyncioTestCase):
self.controller.supports_resilient_boot = True self.controller.supports_resilient_boot = True
self.controller._examine_systems_task.start_sync() self.controller._examine_systems_task.start_sync()
self.app.dr_cfg = DRConfig() self.app.dr_cfg = DRConfig()
self.app.base_model.source.current.type = "fsimage"
self.app.base_model.source.current.variations = { self.app.base_model.source.current.variations = {
"default": CatalogEntryVariation(path="", size=1), "default": CatalogEntryVariation(path="", size=1),
} }
@ -627,6 +628,7 @@ class TestGuidedV2(IsolatedAsyncioTestCase):
self.fsc.model = self.model = make_model(bootloader) self.fsc.model = self.model = make_model(bootloader)
self.fsc._examine_systems_task.start_sync() self.fsc._examine_systems_task.start_sync()
self.app.dr_cfg = DRConfig() self.app.dr_cfg = DRConfig()
self.app.base_model.source.current.type = "fsimage"
self.app.base_model.source.current.variations = { self.app.base_model.source.current.variations = {
"default": CatalogEntryVariation(path="", size=1), "default": CatalogEntryVariation(path="", size=1),
} }
@ -1245,6 +1247,7 @@ class TestCoreBootInstallMethods(IsolatedAsyncioTestCase):
# runs much more quickly than the integration test! # runs much more quickly than the integration test!
self.fsc.model = model = make_model(Bootloader.UEFI) self.fsc.model = model = make_model(Bootloader.UEFI)
disk = make_disk(model) disk = make_disk(model)
self.app.base_model.source.current.type = "fsimage"
self.app.base_model.source.current.variations = { self.app.base_model.source.current.variations = {
"default": CatalogEntryVariation( "default": CatalogEntryVariation(
path="", size=1, snapd_system_label="prefer-encrypted" path="", size=1, snapd_system_label="prefer-encrypted"
@ -1338,6 +1341,7 @@ class TestCoreBootInstallMethods(IsolatedAsyncioTestCase):
async def test_from_sample_data_defective(self): async def test_from_sample_data_defective(self):
self.fsc.model = model = make_model(Bootloader.UEFI) self.fsc.model = model = make_model(Bootloader.UEFI)
make_disk(model) make_disk(model)
self.app.base_model.source.current.type = "fsimage"
self.app.base_model.source.current.variations = { self.app.base_model.source.current.variations = {
"default": CatalogEntryVariation( "default": CatalogEntryVariation(
path="", size=1, snapd_system_label="defective" path="", size=1, snapd_system_label="defective"

View File

@ -148,6 +148,7 @@ class _DryRunCurtinCommand(_CurtinCommand):
def make_command(self, command, *args, config=None): def make_command(self, command, *args, config=None):
if command == "install": if command == "install":
log.debug("creating substitute for curtin install %s", args)
# Lookup the log file from the config if specified # Lookup the log file from the config if specified
try: try:
with open(config, mode="r") as fh: with open(config, mode="r") as fh:

View File

@ -381,7 +381,10 @@ class GuidedDiskSelectionView(BaseView):
else: else:
capability = GuidedCapability.LVM capability = GuidedCapability.LVM
else: else:
capability = GuidedCapability.DIRECT if GuidedCapability.DD in target.allowed:
capability = GuidedCapability.DD
else:
capability = GuidedCapability.DIRECT
choice = GuidedChoiceV2( choice = GuidedChoiceV2(
target=target, target=target,
capability=capability, capability=capability,

View File

@ -242,7 +242,7 @@ def _compute_widths_for_size(maxcol, table_rows, colspecs, default_spacing):
widths[underlying_i] = max(w, widths.get(underlying_i, 0)) widths[underlying_i] = max(w, widths.get(underlying_i, 0))
# count the columns... # count the columns...
colcount = max(widths.keys()) // 2 + 1 colcount = max(widths.keys(), default=1) // 2 + 1
if unpacked_user_indices: if unpacked_user_indices:
colcount = max(colcount, max(unpacked_user_indices) + 1) colcount = max(colcount, max(unpacked_user_indices) + 1)