Merge pull request #1686 from mwhudson/recovery-part
allow creation and population of a recovery partition via autoinstall
This commit is contained in:
commit
211ee4e7b5
|
@ -61,7 +61,7 @@ parts:
|
|||
|
||||
source: https://git.launchpad.net/curtin
|
||||
source-type: git
|
||||
source-commit: fb70e57035375abc63c7c2c757c4c64ab928d5b4
|
||||
source-commit: 8e74f621e1bd84f804bda280a370440b150f9f40
|
||||
|
||||
override-pull: |
|
||||
craftctl default
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
Loading…
Reference in New Issue