diff --git a/examples/autoinstall-reset-only.yaml b/examples/autoinstall-reset-only.yaml new file mode 100644 index 00000000..f9b19318 --- /dev/null +++ b/examples/autoinstall-reset-only.yaml @@ -0,0 +1,11 @@ +version: 1 +identity: + realname: '' + username: ubuntu + password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' + hostname: ubuntu +storage: + layout: + name: direct + reset-partition: yes + reset-partition-only: yes diff --git a/scripts/runtests.sh b/scripts/runtests.sh index 67bf4176..02f8e4aa 100755 --- a/scripts/runtests.sh +++ b/scripts/runtests.sh @@ -31,16 +31,25 @@ validate () { cfgs+=("$cfg") fi done - python3 scripts/validate-yaml.py "${cfgs[@]}" - if [ ! -e $tmpdir/subiquity-client-debug.log ] || [ ! -e $tmpdir/subiquity-server-debug.log ]; then - echo "log file not created" - exit 1 - fi - python3 scripts/validate-autoinstall-user-data.py < $tmpdir/var/log/installer/autoinstall-user-data if grep passw0rd $tmpdir/subiquity-client-debug.log $tmpdir/subiquity-server-debug.log | grep -v "Loaded answers" | grep -v "answers_action"; then echo "password leaked into log file" exit 1 fi + opt= + [ $# -gt 1 ] && opt="$2" + if [ $opt = reset-only ]; then + python3 scripts/validate-yaml.py --no-root-mount "${cfgs[@]}" + else + python3 scripts/validate-yaml.py "${cfgs[@]}" + fi + if [ ! -e $tmpdir/subiquity-client-debug.log ] || [ ! -e $tmpdir/subiquity-server-debug.log ]; then + echo "log file not created" + exit 1 + fi + if [ $opt = reset-only ]; then + return + fi + python3 scripts/validate-autoinstall-user-data.py < $tmpdir/var/log/installer/autoinstall-user-data netplan generate --root $tmpdir elif [ "${mode}" = "system_setup" ]; then setup_mode="$2" @@ -254,6 +263,17 @@ python3 scripts/check-yaml-fields.py "$tmpdir"/var/log/installer/autoinstall-use 'autoinstall.source.id="ubuntu-server-minimal"' grep -q 'finish: subiquity/Install/install/postinstall/run_unattended_upgrades: SUCCESS: downloading and installing security updates' $tmpdir/subiquity-server-debug.log +clean +LANG=C.UTF-8 timeout --foreground 60 \ + python3 -m subiquity.cmd.tui \ + --dry-run \ + --output-base "$tmpdir" \ + --machine-config examples/simple.json \ + --autoinstall examples/autoinstall-reset-only.yaml \ + --kernel-cmdline autoinstall \ + --source-catalog examples/install-sources.yaml +validate install reset-only + # The OOBE doesn't exist in WSL < 20.04 if [ "${RELEASE%.*}" -ge 20 ]; then # Test TCP connectivity (system_setup only) diff --git a/scripts/validate-yaml.py b/scripts/validate-yaml.py index 34df2199..6e787878 100644 --- a/scripts/validate-yaml.py +++ b/scripts/validate-yaml.py @@ -75,11 +75,15 @@ class StorageChecker: assert '/' in self.path_to_mount - - def main(): storage_checker = StorageChecker() + root_mount = True + + if sys.argv[1:2] == ['--no-root-mount']: + root_mount = False + sys.argv.pop(1) + actions = [] for path in sys.argv[1:]: config = yaml.safe_load(open(path)) @@ -92,7 +96,8 @@ def main(): print('checking {} failed'.format(action)) raise - storage_checker.final_checks() + if root_mount: + storage_checker.final_checks() main() diff --git a/subiquity/server/controllers/filesystem.py b/subiquity/server/controllers/filesystem.py index 615d64be..d0b5bc7c 100644 --- a/subiquity/server/controllers/filesystem.py +++ b/subiquity/server/controllers/filesystem.py @@ -125,6 +125,9 @@ system_non_gpt_text = _( ) +DRY_RUN_RESET_SIZE = 500*MiB + + class NoSnapdSystemsOnSource(Exception): pass @@ -243,6 +246,7 @@ class FilesystemController(SubiquityController, FilesystemManipulator): # 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 + self.reset_partition_only: bool = False def is_core_boot_classic(self): return self._info.is_core_boot_classic() @@ -413,6 +417,8 @@ class FilesystemController(SubiquityController, FilesystemManipulator): }, } await self.convert_autoinstall_config(context=context) + if self.reset_partition_only: + return if not self.model.is_root_mounted(): raise Exception("autoinstall config did not mount root") if self.model.needs_bootloader_partition(): @@ -535,7 +541,11 @@ class FilesystemController(SubiquityController, FilesystemManipulator): raise Exception( "could not find variation for {}".format(capability)) - async def guided(self, choice: GuidedChoiceV2): + async def guided( + self, + choice: GuidedChoiceV2, + reset_partition_only: bool = False + ) -> None: self.model.guided_configuration = choice self.set_info_for_capability(choice.capability) @@ -558,14 +568,22 @@ class FilesystemController(SubiquityController, FilesystemManipulator): 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) + if self.app.opts.dry_run: + reset_size = DRY_RUN_RESET_SIZE + else: + 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'}, flag='msftres') - # Should probably set some kind of flag on reset_partition + self.reset_partition_only = reset_partition_only + if reset_partition_only: + for mount in self.model._all(type='mount'): + self.delete_mount(mount) + self.model.target = self.app.base_model.target = None + return if choice.capability.is_lvm(): self.guided_lvm(gap, choice) @@ -1223,7 +1241,8 @@ class FilesystemController(SubiquityController, FilesystemManipulator): GuidedChoiceV2( target=target, capability=capability, password=password, sizing_policy=sizing_policy, - reset_partition=layout.get('reset-partition', False))) + reset_partition=layout.get('reset-partition', False)), + reset_partition_only=layout.get('reset-partition-only', False)) def validate_layout_mode(self, mode): if mode not in ('reformat_disk', 'use_gap'): @@ -1315,6 +1334,8 @@ class FilesystemController(SubiquityController, FilesystemManipulator): return r async def _pre_shutdown(self): - await self.app.command_runner.run(['umount', '--recursive', '/target']) + if not self.reset_partition_only: + await self.app.command_runner.run( + ['umount', '--recursive', '/target']) for pool in self.model._all(type='zpool'): await pool.pre_shutdown(self.app.command_runner) diff --git a/subiquity/server/controllers/install.py b/subiquity/server/controllers/install.py index d5553db1..df976fb2 100644 --- a/subiquity/server/controllers/install.py +++ b/subiquity/server/controllers/install.py @@ -259,7 +259,14 @@ class InstallController(SubiquityController): await run_curtin_step(name="initial", stages=[], step_config={}) - if fs_controller.is_core_boot_classic(): + if fs_controller.reset_partition_only: + await run_curtin_step( + name="partitioning", stages=["partitioning"], + step_config=self.filesystem_config( + device_map_path=logs_dir / "device-map.json", + ), + ) + elif fs_controller.is_core_boot_classic(): await run_curtin_step( name="partitioning", stages=["partitioning"], step_config=self.filesystem_config( @@ -349,24 +356,29 @@ class InstallController(SubiquityController): self.app.update_state(ApplicationState.RUNNING) - for_install_path = await self.configure_apt(context=context) + if not self.app.controllers.Filesystem.reset_partition_only: + for_install_path = await self.configure_apt(context=context) - await self.app.hub.abroadcast(InstallerChannels.APT_CONFIGURED) + await self.app.hub.abroadcast(InstallerChannels.APT_CONFIGURED) - if os.path.exists(self.model.target): - await self.unmount_target( - context=context, target=self.model.target) + if os.path.exists(self.model.target): + await self.unmount_target( + context=context, target=self.model.target) + else: + for_install_path = '' await self.curtin_install( context=context, source='cp://' + for_install_path) - self.app.update_state(ApplicationState.WAITING) + if not self.app.controllers.Filesystem.reset_partition_only: - await self.model.wait_postinstall() + self.app.update_state(ApplicationState.WAITING) - self.app.update_state(ApplicationState.RUNNING) + await self.model.wait_postinstall() - await self.postinstall(context=context) + self.app.update_state(ApplicationState.RUNNING) + + await self.postinstall(context=context) self.app.update_state(ApplicationState.DONE) except Exception: diff --git a/subiquity/server/controllers/shutdown.py b/subiquity/server/controllers/shutdown.py index d29186de..44961634 100644 --- a/subiquity/server/controllers/shutdown.py +++ b/subiquity/server/controllers/shutdown.py @@ -96,6 +96,8 @@ class ShutdownController(SubiquityController): async def copy_logs_to_target(self, context): if self.opts.dry_run and 'copy-logs-fail' in self.app.debug_flags: raise PermissionError() + if self.app.controllers.Filesystem.reset_partition_only: + return target_logs = os.path.join( self.app.base_model.target, 'var/log/installer') if self.opts.dry_run: diff --git a/subiquity/server/controllers/tests/test_filesystem.py b/subiquity/server/controllers/tests/test_filesystem.py index 724979a3..5c5927aa 100644 --- a/subiquity/server/controllers/tests/test_filesystem.py +++ b/subiquity/server/controllers/tests/test_filesystem.py @@ -54,6 +54,7 @@ from subiquity.models.tests.test_filesystem import ( ) from subiquity.server import snapdapi from subiquity.server.controllers.filesystem import ( + DRY_RUN_RESET_SIZE, FilesystemController, VariationInfo, ) @@ -340,6 +341,36 @@ class TestGuided(IsolatedAsyncioTestCase): self.assertFalse(d1p2.preserve) self.assertIsNone(gaps.largest_gap(self.d1)) + async def test_guided_reset_partition(self): + await self._guided_setup(Bootloader.UEFI, 'gpt') + target = GuidedStorageTargetReformat( + disk_id=self.d1.id, allowed=default_capabilities) + await self.controller.guided( + GuidedChoiceV2( + target=target, + capability=GuidedCapability.DIRECT, + reset_partition=True)) + [d1p1, d1p2, d1p3] = self.d1.partitions() + self.assertEqual('/boot/efi', d1p1.mount) + self.assertEqual(None, d1p2.mount) + self.assertEqual(DRY_RUN_RESET_SIZE, d1p2.size) + self.assertEqual('/', d1p3.mount) + + async def test_guided_reset_partition_only(self): + await self._guided_setup(Bootloader.UEFI, 'gpt') + target = GuidedStorageTargetReformat( + disk_id=self.d1.id, allowed=default_capabilities) + await self.controller.guided( + GuidedChoiceV2( + target=target, + capability=GuidedCapability.DIRECT, + reset_partition=True), + reset_partition_only=True) + [d1p1, d1p2] = self.d1.partitions() + self.assertEqual(None, d1p1.mount) + self.assertEqual(None, d1p2.mount) + self.assertEqual(DRY_RUN_RESET_SIZE, d1p2.size) + async def test_guided_direct_BIOS_MSDOS(self): await self._guided_setup(Bootloader.BIOS, 'msdos') target = GuidedStorageTargetReformat(