filesystem: add GUI support for recovery-key

Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
This commit is contained in:
Olivier Gayot 2023-08-30 11:06:45 +02:00
parent d8ebc56b69
commit 2d8a2b669e
3 changed files with 82 additions and 1 deletions

View File

@ -14,6 +14,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import pathlib
from typing import Optional
import attr
from urwid import Text, connect_signal
@ -26,6 +28,7 @@ from subiquity.common.types import (
GuidedStorageTargetManual,
GuidedStorageTargetReformat,
Partition,
RecoveryKey,
)
from subiquity.models.filesystem import humanize_size
from subiquitycore.ui.buttons import other_btn
@ -53,6 +56,15 @@ subtitle = _("Configure a guided storage layout, or create a custom one:")
class LUKSOptionsForm(SubForm):
passphrase = PasswordField(_("Passphrase:"))
confirm_passphrase = PasswordField(_("Confirm passphrase:"))
recovery_key = BooleanField(
("Also create a recovery key"),
help=_(
"The key will be stored as"
" ~/recovery-key.txt in the live system and will"
" be copied to /var/log/installer/ in the target"
" system."
),
)
def validate_passphrase(self):
if len(self.passphrase.value) < 1:
@ -368,6 +380,7 @@ class GuidedDiskSelectionView(BaseView):
target = guided_choice["disk"]
tpm_choice = self.form.guided_choice.widget.form.tpm_choice
password = None
recovery_key: Optional[RecoveryKey] = None
if tpm_choice is not None:
if guided_choice.get("use_tpm", tpm_choice.default):
capability = GuidedCapability.CORE_BOOT_ENCRYPTED
@ -378,6 +391,16 @@ class GuidedDiskSelectionView(BaseView):
if opts.get("encrypt", False):
capability = GuidedCapability.LVM_LUKS
password = opts["luks_options"]["passphrase"]
if opts["luks_options"]["recovery_key"]:
# There is only one encrypted LUKS (at max) in guided
# so no need to prefix the locations with the name of
# the VG.
recovery_key = RecoveryKey(
live_location=str(
pathlib.Path("~/recovery-key.txt").expanduser()
),
backup_location="var/log/installer/recovery-key.txt",
)
else:
capability = GuidedCapability.LVM
else:
@ -389,6 +412,7 @@ class GuidedDiskSelectionView(BaseView):
target=target,
capability=capability,
password=password,
recovery_key=recovery_key,
)
else:
choice = GuidedChoiceV2(

View File

@ -15,11 +15,12 @@
import logging
import os
import pathlib
import re
from urwid import Text, connect_signal
from subiquity.models.filesystem import get_lvm_size, humanize_size
from subiquity.models.filesystem import RecoveryKeyHandler, get_lvm_size, humanize_size
from subiquity.ui.views.filesystem.compound import (
CompoundDiskForm,
MultiDeviceField,
@ -78,10 +79,22 @@ class VolGroupForm(CompoundDiskForm):
encrypt = BooleanField(_("Create encrypted volume"))
passphrase = PasswordField(_("Passphrase:"))
confirm_passphrase = PasswordField(_("Confirm passphrase:"))
# TODO replace the placeholders in the help - also potentially replacing
# "~" with the actual home directory.
create_recovery_key = BooleanField(
_("Also create a recovery key:"),
help=_(
"The key will be stored as"
" ~/recovery-key-{name}.txt in the live system and will"
" be copied to /var/log/installer/ in the target"
" system."
),
)
def _change_encrypt(self, sender, new_value):
self.passphrase.enabled = new_value
self.confirm_passphrase.enabled = new_value
self.create_recovery_key.enabled = new_value
if not new_value:
self.passphrase.validate()
self.confirm_passphrase.validate()
@ -147,6 +160,7 @@ class VolGroupStretchy(Stretchy):
devices = {}
key = ""
encrypt = False
create_recovery_key = False
for d in existing.devices:
if d.type == "dm_crypt":
encrypt = True
@ -161,6 +175,7 @@ class VolGroupStretchy(Stretchy):
# TODO make this more user friendly.
if d.key is not None:
key = d.key
create_recovery_key = d.recovery_key is not None
d = d.volume
devices[d] = "active"
initial = {
@ -169,6 +184,7 @@ class VolGroupStretchy(Stretchy):
"encrypt": encrypt,
"passphrase": key,
"confirm_passphrase": key,
"create_recovery_key": create_recovery_key,
}
possible_components = get_possible_components(
@ -205,6 +221,15 @@ class VolGroupStretchy(Stretchy):
del result["size"]
mdc = self.form.devices.widget
result["devices"] = mdc.active_devices
if "create_recovery_key" in result:
if result["create_recovery_key"]:
backup_prefix = pathlib.Path("/var/log/installer")
filename = pathlib.Path(f"recovery-key-{result['name']}.txt")
result["recovery-key"] = RecoveryKeyHandler(
live_location=pathlib.Path("~").expanduser() / filename,
backup_location=backup_prefix / filename,
)
del result["create_recovery_key"]
if "confirm_passphrase" in result:
del result["confirm_passphrase"]
safe_result = result.copy()

View File

@ -13,12 +13,15 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import pathlib
import unittest
from unittest import mock
import urwid
from subiquity.client.controllers.filesystem import FilesystemController
from subiquity.models.filesystem import RecoveryKeyHandler
from subiquity.ui.views.filesystem.lvm import VolGroupStretchy
from subiquity.ui.views.filesystem.tests.test_partition import make_model_and_disk
from subiquitycore.testing import view_helpers
@ -66,6 +69,7 @@ class LVMViewTests(unittest.TestCase):
"encrypt": True,
"passphrase": "passw0rd",
"confirm_passphrase": "passw0rd",
"create_recovery_key": False,
}
expected_data = {
"name": "vg1",
@ -76,3 +80,31 @@ class LVMViewTests(unittest.TestCase):
view_helpers.enter_data(stretchy.form, form_data)
view_helpers.click(stretchy.form.done_btn.base_widget)
view.controller.volgroup_handler.assert_called_once_with(None, expected_data)
def test_create_vg_encrypted_with_recovery(self):
model, disk = make_model_and_disk()
part1 = model.add_partition(disk, size=10 * (2**30), offset=0)
part2 = model.add_partition(disk, size=10 * (2**30), offset=10 * (2**30))
view, stretchy = make_view(model)
form_data = {
"name": "vg1",
"devices": {part1: "active", part2: "active"},
"encrypt": True,
"passphrase": "passw0rd",
"confirm_passphrase": "passw0rd",
"create_recovery_key": True,
}
expected_data = {
"name": "vg1",
"devices": {part1, part2},
"encrypt": True,
"passphrase": "passw0rd",
"recovery-key": RecoveryKeyHandler(
live_location=pathlib.Path("/home/ubuntu/recovery-key-vg1.txt"),
backup_location=pathlib.Path("/var/log/installer/recovery-key-vg1.txt"),
),
}
view_helpers.enter_data(stretchy.form, form_data)
with mock.patch.dict(os.environ, {"HOME": "/home/ubuntu"}):
view_helpers.click(stretchy.form.done_btn.base_widget)
view.controller.volgroup_handler.assert_called_once_with(None, expected_data)