storage: when enabled, raise NRE when from POST v2/add_boot_partition

When the request to v2/add_boot_partition can not be processed by the
server because it is not valid, raise a FilesystemValueError exception.

Depending on configuration, it will be treated as a normal ValueError or
as a NonReportableError.

Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
This commit is contained in:
Olivier Gayot 2024-04-26 23:57:38 +02:00
parent 1cca91f9fe
commit d04c01a068
4 changed files with 72 additions and 5 deletions

View File

@ -23,6 +23,7 @@ import sys
import attr import attr
from subiquity.server.controllers.filesystem import set_user_error_reportable
from subiquitycore.log import setup_logger from subiquitycore.log import setup_logger
from .common import LOGDIR, setup_environment from .common import LOGDIR, setup_environment
@ -137,6 +138,16 @@ def make_server_args_parser():
) )
parser.add_argument("--storage-version", action="store", type=int) parser.add_argument("--storage-version", action="store", type=int)
parser.add_argument(
"--no-report-storage-user-errors",
action="store_true",
help="""\
When False (the default), exceptions raised by /storage/v2/ POST handlers when
the client sends bad information will cause the creation of a crash report and
a HTTP 500 error to be returned.
When True, no crash report will be created and a HTTP 422 error will be
returned. """,
)
parser.add_argument("--use-os-prober", action="store_true", default=False) parser.add_argument("--use-os-prober", action="store_true", default=False)
parser.add_argument( parser.add_argument(
"--postinst-hooks-dir", default="/etc/subiquity/postinst.d", type=pathlib.Path "--postinst-hooks-dir", default="/etc/subiquity/postinst.d", type=pathlib.Path
@ -158,6 +169,7 @@ def main():
opts.storage_version = int( opts.storage_version = int(
opts.kernel_cmdline.get("subiquity-storage-version", 1) opts.kernel_cmdline.get("subiquity-storage-version", 1)
) )
set_user_error_reportable(not opts.no_report_storage_user_errors)
logdir = LOGDIR logdir = LOGDIR
if opts.dry_run: if opts.dry_run:
if opts.dry_run_config: if opts.dry_run_config:

View File

@ -22,7 +22,7 @@ import os
import pathlib import pathlib
import subprocess import subprocess
import time import time
from typing import Any, Callable, Dict, List, Optional, Union from typing import Any, Callable, Dict, List, Optional, Type, Union
import attr import attr
import pyudev import pyudev
@ -79,6 +79,7 @@ from subiquity.server.autoinstall import AutoinstallError
from subiquity.server.controller import SubiquityController from subiquity.server.controller import SubiquityController
from subiquity.server.controllers.source import SEARCH_DRIVERS_AUTOINSTALL_DEFAULT from subiquity.server.controllers.source import SEARCH_DRIVERS_AUTOINSTALL_DEFAULT
from subiquity.server.mounter import Mounter from subiquity.server.mounter import Mounter
from subiquity.server.nonreportable import NonReportableException
from subiquity.server.snapdapi import ( from subiquity.server.snapdapi import (
StorageEncryptionSupport, StorageEncryptionSupport,
StorageSafety, StorageSafety,
@ -122,6 +123,23 @@ class NoSnapdSystemsOnSource(Exception):
pass pass
class NonReportableSVE(NonReportableException):
"""Non reportable storage value error"""
class ReportableSVE(ValueError):
"""Reportable storage value error"""
# Depending on config, we will let the SVE fail on the server side
StorageValueError: Type[NonReportableSVE | ReportableSVE] = ReportableSVE
def set_user_error_reportable(reportable: bool) -> None:
global StorageValueError
StorageValueError = ReportableSVE if reportable else NonReportableSVE
@attr.s(auto_attribs=True) @attr.s(auto_attribs=True)
class CapabilityInfo: class CapabilityInfo:
allowed: List[GuidedCapability] = attr.Factory(list) allowed: List[GuidedCapability] = attr.Factory(list)
@ -1193,9 +1211,9 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
self.locked_probe_data = True self.locked_probe_data = True
disk = self.model._one(id=disk_id) disk = self.model._one(id=disk_id)
if boot.is_boot_device(disk): if boot.is_boot_device(disk):
raise ValueError("device already has bootloader partition") raise StorageValueError("device already has bootloader partition")
if DeviceAction.TOGGLE_BOOT not in DeviceAction.supported(disk): if DeviceAction.TOGGLE_BOOT not in DeviceAction.supported(disk):
raise ValueError("disk does not support boot partiton") raise StorageValueError("disk does not support boot partiton")
self.add_boot_disk(disk) self.add_boot_disk(disk)
return await self.v2_GET() return await self.v2_GET()

View File

@ -60,6 +60,7 @@ from subiquity.server.autoinstall import AutoinstallError
from subiquity.server.controllers.filesystem import ( from subiquity.server.controllers.filesystem import (
DRY_RUN_RESET_SIZE, DRY_RUN_RESET_SIZE,
FilesystemController, FilesystemController,
StorageValueError,
VariationInfo, VariationInfo,
) )
from subiquity.server.dryrun import DRConfig from subiquity.server.dryrun import DRConfig
@ -220,7 +221,9 @@ class TestSubiquityControllerFilesystem(IsolatedAsyncioTestCase):
async def test_v2_add_boot_partition_POST_existing_bootloader(self): async def test_v2_add_boot_partition_POST_existing_bootloader(self):
self.fsc.locked_probe_data = False self.fsc.locked_probe_data = False
with mock.patch.object(self.fsc, "add_boot_disk") as add_boot_disk: with mock.patch.object(self.fsc, "add_boot_disk") as add_boot_disk:
with self.assertRaisesRegex(ValueError, r"already\ has\ bootloader"): with self.assertRaises(
StorageValueError, msg="device already has bootloader"
):
await self.fsc.v2_add_boot_partition_POST("dev-sda") await self.fsc.v2_add_boot_partition_POST("dev-sda")
self.assertTrue(self.fsc.locked_probe_data) self.assertTrue(self.fsc.locked_probe_data)
add_boot_disk.assert_not_called() add_boot_disk.assert_not_called()
@ -230,7 +233,7 @@ class TestSubiquityControllerFilesystem(IsolatedAsyncioTestCase):
async def test_v2_add_boot_partition_POST_not_supported(self): async def test_v2_add_boot_partition_POST_not_supported(self):
self.fsc.locked_probe_data = False self.fsc.locked_probe_data = False
with mock.patch.object(self.fsc, "add_boot_disk") as add_boot_disk: with mock.patch.object(self.fsc, "add_boot_disk") as add_boot_disk:
with self.assertRaisesRegex(ValueError, r"does\ not\ support\ boot"): with self.assertRaises(StorageValueError, msg="disk does not support boot"):
await self.fsc.v2_add_boot_partition_POST("dev-sda") await self.fsc.v2_add_boot_partition_POST("dev-sda")
self.assertTrue(self.fsc.locked_probe_data) self.assertTrue(self.fsc.locked_probe_data)
add_boot_disk.assert_not_called() add_boot_disk.assert_not_called()

View File

@ -2272,3 +2272,37 @@ class TestMountDetection(TestAPI):
self.assertTrue(disk1["has_in_use_partition"]) self.assertTrue(disk1["has_in_use_partition"])
disk1p2 = disk1["partitions"][1] disk1p2 = disk1["partitions"][1]
self.assertTrue(disk1p2["is_in_use"]) self.assertTrue(disk1p2["is_in_use"])
class TestFilesystemUserErrors(TestAPI):
@timeout()
async def test_add_boot_partition__with_error_report(self):
cfg = "examples/machines/simple.json"
extra = ["--storage-version", "2"]
async with start_server(cfg, extra_args=extra) as inst:
await inst.post("/storage/v2/add_boot_partition", disk_id="disk-sda")
try:
await inst.post("/storage/v2/add_boot_partition", disk_id="disk-sda")
except ClientResponseError as cre:
self.assertEqual(500, cre.status)
self.assertIn("x-error-report", cre.headers)
self.assertEqual(
"device already has bootloader partition",
json.loads(cre.headers["x-error-msg"]),
)
@timeout()
async def test_add_boot_partition__no_error_report(self):
cfg = "examples/machines/simple.json"
extra = ["--storage-version", "2", "--no-report-storage-user-error"]
async with start_server(cfg, extra_args=extra) as inst:
await inst.post("/storage/v2/add_boot_partition", disk_id="disk-sda")
try:
await inst.post("/storage/v2/add_boot_partition", disk_id="disk-sda")
except ClientResponseError as cre:
self.assertEqual(422, cre.status)
self.assertNotIn("x-error-report", cre.headers)
self.assertEqual(
"device already has bootloader partition",
json.loads(cre.headers["x-error-msg"]),
)