Merge pull request #1686 from mwhudson/recovery-part

allow creation and population of a recovery partition via autoinstall
This commit is contained in:
Michael Hudson-Doyle 2023-06-09 09:33:57 +12:00 committed by GitHub
commit 211ee4e7b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 97 additions and 12 deletions

View File

@ -61,7 +61,7 @@ parts:
source: https://git.launchpad.net/curtin
source-type: git
source-commit: fb70e57035375abc63c7c2c757c4c64ab928d5b4
source-commit: 8e74f621e1bd84f804bda280a370440b150f9f40
override-pull: |
craftctl default

View File

@ -451,6 +451,7 @@ class GuidedChoiceV2:
password: Optional[str] = attr.ib(default=None, repr=False)
sizing_policy: Optional[SizingPolicy] = \
attr.ib(default=SizingPolicy.SCALED)
reset_partition: bool = False
@staticmethod
def from_guided_choice(choice: GuidedChoice):

View File

@ -38,6 +38,7 @@ from subiquitycore.async_helpers import (
)
from subiquitycore.context import with_context
from subiquitycore.utils import (
arun_command,
run_command,
)
from subiquitycore.lsb_release import lsb_release
@ -83,6 +84,8 @@ from subiquity.models.filesystem import (
align_down,
_Device,
Disk as ModelDisk,
MiB,
Partition as ModelPartition,
LVM_CHUNK_SIZE,
Raid,
)
@ -196,6 +199,7 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
# If probe data come in while we are doing partitioning, store it in
# this variable. It will be picked up on next reset.
self.queued_probe_data: Optional[Dict[str, Any]] = None
self.reset_partition: Optional[ModelPartition] = None
def is_core_boot_classic(self):
return self._info.is_core_boot_classic()
@ -493,6 +497,16 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
if gap is None:
raise Exception('failed to locate gap after adding boot')
if choice.reset_partition:
cp = await arun_command(['du', '-sb', '/cdrom'])
reset_size = int(cp.stdout.strip().split()[0])
reset_size = align_up(int(reset_size * 1.10), 256 * MiB)
reset_gap, gap = gap.split(reset_size)
self.reset_partition = self.create_partition(
device=reset_gap.device, gap=reset_gap,
spec={'fstype': 'fat32'})
# Should probably set some kind of flag on reset_partition
if choice.capability.is_lvm():
self.guided_lvm(gap, choice)
elif choice.capability == GuidedCapability.DIRECT:
@ -1116,8 +1130,10 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
log.info(f'autoinstall: running guided {capability} install in '
f'mode {mode} using {target}')
await self.guided(
GuidedChoiceV2(target=target, capability=capability,
password=password, sizing_policy=sizing_policy))
GuidedChoiceV2(
target=target, capability=capability,
password=password, sizing_policy=sizing_policy,
reset_partition=layout.get('reset-partition', False)))
def validate_layout_mode(self, mode):
if mode not in ('reformat_disk', 'use_gap'):

View File

@ -23,7 +23,7 @@ import re
import shutil
import subprocess
import tempfile
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional
from curtin.config import merge_config
import yaml
@ -51,6 +51,9 @@ from subiquity.server.curtin import (
run_curtin_command,
start_curtin_command,
)
from subiquity.server.mounter import (
Mounter,
)
from subiquity.server.types import (
InstallerChannels,
)
@ -147,6 +150,17 @@ class InstallController(SubiquityController):
config.update(kw)
return config
def rp_config(self, logs_dir: Path, target: str) -> Dict[str, Any]:
"""Return configuration to be used as part of populating a recovery
partition."""
return {
"install": {
"target": target,
"resume_data": None,
"extra_rsync_args": ['--no-links'],
}
}
@with_context(description="umounting /target dir")
async def unmount_target(self, *, context, target):
await run_curtin_command(self.app, context, 'unmount', '-t', target,
@ -174,11 +188,11 @@ class InstallController(SubiquityController):
name: str,
stages: List[str],
config_file: Path,
source: str,
source: Optional[str],
config: Dict[str, Any]):
"""Run a curtin install step."""
self.app.note_file_for_apport(
f"Curtin{name}Config", str(config_file))
f"Curtin{name.title().replace(' ', '')}Config", str(config_file))
self.write_config(config_file=config_file, config=config)
@ -191,9 +205,15 @@ class InstallController(SubiquityController):
with open(str(log_file), mode="a") as fh:
fh.write(f"\n---- [[ subiquity step {name} ]] ----\n")
if source is not None:
source_args = (source, )
else:
source_args = ()
await run_curtin_command(
self.app, context, "install", source,
self.app, context, "install",
"--set", f'json:stages={json.dumps(stages)}',
*source_args,
config=str(config_file), private_mounts=False)
device_map_path = config.get('storage', {}).get('device_map_path')
@ -224,14 +244,15 @@ class InstallController(SubiquityController):
fs_controller = self.app.controllers.Filesystem
async def run_curtin_step(name, stages, step_config):
async def run_curtin_step(name, stages, step_config, source=None):
config = copy.deepcopy(base_config)
filename = f"subiquity-{name.replace(' ', '-')}.conf"
merge_config(config, copy.deepcopy(step_config))
await self.run_curtin_step(
context=context,
name=name,
stages=stages,
config_file=config_dir / f"subiquity-{name}.conf",
config_file=config_dir / filename,
source=source,
config=config,
)
@ -257,6 +278,7 @@ class InstallController(SubiquityController):
await run_curtin_step(
name="extract", stages=["extract"],
step_config=self.generic_config(),
source=source,
)
await self.create_core_boot_classic_fstab(context=context)
await run_curtin_step(
@ -281,6 +303,7 @@ class InstallController(SubiquityController):
await run_curtin_step(
name="extract", stages=["extract"],
step_config=self.generic_config(),
source=source,
)
await self.setup_target(context=context)
await run_curtin_step(
@ -291,6 +314,15 @@ class InstallController(SubiquityController):
# really write recovery_system={snapd_system_label} to
# {target}/var/lib/snapd/modeenv to get snapd to pick it up on
# first boot. But not needed for now.
rp = fs_controller.reset_partition
if rp is not None:
mounter = Mounter(self.app)
async with mounter.mounted(rp.path) as mp:
await run_curtin_step(
name="populate recovery", stages=["extract"],
step_config=self.rp_config(logs_dir, mp.p()),
source='cp:///cdrom',
)
@with_context(description="creating fstab")
async def create_core_boot_classic_fstab(self, *, context):

View File

@ -54,7 +54,32 @@ class TestWriteConfig(unittest.IsolatedAsyncioTestCase):
run_cmd.assert_called_once_with(
self.controller.app,
ANY,
"install", "/source",
"install",
"--set", 'json:stages=["partitioning", "extract"]',
"/source",
config="/config.yaml",
private_mounts=False)
@patch("subiquity.server.controllers.install.run_curtin_command")
async def test_run_curtin_install_step_no_src(self, run_cmd):
with patch("subiquity.server.controllers.install.open",
mock_open()) as m_open:
await self.controller.run_curtin_step(
name='MyStep',
stages=["partitioning", "extract"],
config_file=Path("/config.yaml"),
source=None,
config=self.controller.base_config(
logs_dir=Path("/"), resume_data_file=Path("resume-data"))
)
m_open.assert_called_once_with("/curtin-install.log", mode="a")
run_cmd.assert_called_once_with(
self.controller.app,
ANY,
"install",
"--set", 'json:stages=["partitioning", "extract"]',
config="/config.yaml",
private_mounts=False)

View File

@ -118,13 +118,16 @@ class Mounter:
self.tmpfiles = TmpFileSet()
self._mounts: List[Mountpoint] = []
async def mount(self, device, mountpoint, options=None, type=None):
async def mount(self, device, mountpoint=None, options=None, type=None):
opts = []
if options is not None:
opts.extend(['-o', options])
if type is not None:
opts.extend(['-t', type])
if os.path.exists(mountpoint):
if mountpoint is None:
mountpoint = tempfile.mkdtemp()
created = True
elif os.path.exists(mountpoint):
created = False
else:
path = Path(device)
@ -194,6 +197,14 @@ class Mounter:
if name in dirnames:
dirnames.remove(name)
@contextlib.asynccontextmanager
async def mounted(self, device, mountpoint=None, options=None, type=None):
mp = await self.mount(device, mountpoint, options, type)
try:
yield mp
finally:
await self.unmount(mp)
class DryRunMounter(Mounter):