Merge remote-tracking branch 'origin/pull/500/head'

Merge using existing partitions. Revert to using archive
curtin/deps/etc.

Signed-off-by: Dimitri John Ledkov <xnox@ubuntu.com>
This commit is contained in:
Dimitri John Ledkov 2019-07-02 13:18:46 +01:00
commit 94de7dc522
20 changed files with 3349 additions and 1827 deletions

View File

@ -30,7 +30,7 @@ installer without having access to the machine. A few sample machine
profiles are available in the repository at ./examples/ and
can be loaded via the MACHINE make variable:
`make dryrun MACHINE=examples/mwhudson.json`
`make dryrun MACHINE=examples/simple.json`
# Generating machine profiles
Machine profiles are generated from the probert tool. To collect a machine profile:

View File

@ -18,7 +18,7 @@ Filesystem:
obj: [disk index 0]
action: PARTITION
data:
size: 1G
size: 0.5G
fstype: null
- *newpart
- *newpart

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

697
examples/simple.json Normal file
View File

@ -0,0 +1,697 @@
{
"network": {
"links": [
{
"addresses": [
{
"address": "10.0.2.15/24",
"family": 2,
"scope": "global",
"source": "dhcp"
},
{
"address": "fec0::5054:ff:fe12:3456/64",
"family": 10,
"scope": "site",
"source": "dhcp"
},
{
"address": "fe80::5054:ff:fe12:3456/64",
"family": 10,
"scope": "link",
"source": "static"
}
],
"bond": {
"is_master": false,
"is_slave": false,
"lacp_rate": null,
"master": null,
"mode": null,
"slaves": [],
"xmit_hash_policy": null
},
"bridge": {
"interfaces": [],
"is_bridge": false,
"is_port": false,
"options": {}
},
"netlink_data": {
"arptype": 1,
"family": 0,
"flags": 69699,
"ifindex": 2,
"is_vlan": false,
"name": "ens3"
},
"type": "eth",
"udev_data": {
"DEVPATH": "/devices/pci0000:00/0000:00:03.0/net/ens3",
"ID_BUS": "pci",
"ID_MM_CANDIDATE": "1",
"ID_MODEL_FROM_DATABASE": "82540EM Gigabit Ethernet Controller (QEMU Virtual Machine)",
"ID_MODEL_ID": "0x100e",
"ID_NET_NAME_MAC": "enx525400123456",
"ID_NET_NAME_PATH": "enp0s3",
"ID_NET_NAME_SLOT": "ens3",
"ID_NET_NAMING_SCHEME": "v240",
"ID_PATH": "pci-0000:00:03.0",
"ID_PATH_TAG": "pci-0000_00_03_0",
"ID_PCI_CLASS_FROM_DATABASE": "Network controller",
"ID_PCI_SUBCLASS_FROM_DATABASE": "Ethernet controller",
"ID_VENDOR_FROM_DATABASE": "Intel Corporation",
"ID_VENDOR_ID": "0x8086",
"IFINDEX": "2",
"INTERFACE": "ens3",
"SUBSYSTEM": "net",
"SYSTEMD_ALIAS": "/sys/subsystem/net/devices/ens3",
"TAGS": ":systemd:",
"USEC_INITIALIZED": "82651806",
"attrs": {
"addr_assign_type": "0",
"addr_len": "6",
"address": "52:54:00:12:34:56",
"broadcast": "ff:ff:ff:ff:ff:ff",
"carrier": "1",
"carrier_changes": "2",
"carrier_down_count": "1",
"carrier_up_count": "1",
"dev_id": "0x0",
"dev_port": "0",
"device": null,
"dormant": "0",
"duplex": "full",
"flags": "0x1003",
"gro_flush_timeout": "0",
"ifalias": "",
"ifindex": "2",
"iflink": "2",
"link_mode": "0",
"mtu": "1500",
"name_assign_type": "4",
"netdev_group": "0",
"operstate": "up",
"phys_port_id": null,
"phys_port_name": null,
"phys_switch_id": null,
"proto_down": "0",
"speed": "1000",
"subsystem": "net",
"tx_queue_len": "1000",
"type": "1",
"uevent": "INTERFACE=ens3\nIFINDEX=2"
}
}
},
{
"addresses": [
{
"address": "127.0.0.1/8",
"family": 2,
"scope": "host",
"source": "static"
},
{
"address": "::1/128",
"family": 10,
"scope": "host",
"source": "static"
}
],
"bond": {
"is_master": false,
"is_slave": false,
"lacp_rate": null,
"master": null,
"mode": null,
"slaves": [],
"xmit_hash_policy": null
},
"bridge": {
"interfaces": [],
"is_bridge": false,
"is_port": false,
"options": {}
},
"netlink_data": {
"arptype": 772,
"family": 0,
"flags": 65609,
"ifindex": 1,
"is_vlan": false,
"name": "lo"
},
"type": "lo",
"udev_data": {
"DEVPATH": "/devices/virtual/net/lo",
"ID_MM_CANDIDATE": "1",
"IFINDEX": "1",
"INTERFACE": "lo",
"SUBSYSTEM": "net",
"USEC_INITIALIZED": "89206115",
"attrs": {
"addr_assign_type": "0",
"addr_len": "6",
"address": "00:00:00:00:00:00",
"broadcast": "00:00:00:00:00:00",
"carrier": "1",
"carrier_changes": "0",
"carrier_down_count": "0",
"carrier_up_count": "0",
"dev_id": "0x0",
"dev_port": "0",
"dormant": "0",
"duplex": null,
"flags": "0x9",
"gro_flush_timeout": "0",
"ifalias": "",
"ifindex": "1",
"iflink": "1",
"link_mode": "0",
"mtu": "65536",
"name_assign_type": null,
"netdev_group": "0",
"operstate": "unknown",
"phys_port_id": null,
"phys_port_name": null,
"phys_switch_id": null,
"proto_down": "0",
"speed": null,
"subsystem": "net",
"tx_queue_len": "1000",
"type": "772",
"uevent": "INTERFACE=lo\nIFINDEX=1"
}
}
}
],
"routes": [
{
"dst": "default",
"family": 2,
"ifindex": 2,
"table": 254,
"type": 1
},
{
"dst": "10.0.2.0/24",
"family": 2,
"ifindex": 2,
"table": 254,
"type": 1
},
{
"dst": "10.0.2.2",
"family": 2,
"ifindex": 2,
"table": 254,
"type": 1
},
{
"dst": "10.0.2.0",
"family": 2,
"ifindex": 2,
"table": 255,
"type": 3
},
{
"dst": "10.0.2.15",
"family": 2,
"ifindex": 2,
"table": 255,
"type": 2
},
{
"dst": "10.0.2.255",
"family": 2,
"ifindex": 2,
"table": 255,
"type": 3
},
{
"dst": "127.0.0.0",
"family": 2,
"ifindex": 1,
"table": 255,
"type": 3
},
{
"dst": "127.0.0.0/8",
"family": 2,
"ifindex": 1,
"table": 255,
"type": 2
},
{
"dst": "127.0.0.1",
"family": 2,
"ifindex": 1,
"table": 255,
"type": 2
},
{
"dst": "127.255.255.255",
"family": 2,
"ifindex": 1,
"table": 255,
"type": 3
},
{
"dst": "::1",
"family": 10,
"ifindex": 1,
"table": 254,
"type": 1
},
{
"dst": "fe80::/64",
"family": 10,
"ifindex": 2,
"table": 254,
"type": 1
},
{
"dst": "fec0::/64",
"family": 10,
"ifindex": 2,
"table": 254,
"type": 1
},
{
"dst": "default",
"family": 10,
"ifindex": 2,
"table": 254,
"type": 1
},
{
"dst": "::1",
"family": 10,
"ifindex": 1,
"table": 255,
"type": 2
},
{
"dst": "fe80::5054:ff:fe12:3456",
"family": 10,
"ifindex": 2,
"table": 255,
"type": 2
},
{
"dst": "fec0::5054:ff:fe12:3456",
"family": 10,
"ifindex": 2,
"table": 255,
"type": 2
},
{
"dst": "ff00::/8",
"family": 10,
"ifindex": 2,
"table": 255,
"type": 1
}
]
},
"storage": {
"bcache": {
"backing": {},
"caching": {}
},
"blockdev": {
"/dev/fd0": {
"DEVNAME": "/dev/fd0",
"DEVPATH": "/devices/platform/floppy.0/block/fd0",
"DEVTYPE": "disk",
"MAJOR": "2",
"MINOR": "0",
"SUBSYSTEM": "block",
"TAGS": ":systemd:",
"USEC_INITIALIZED": "82216378",
"attrs": {
"alignment_offset": "0",
"bdi": null,
"capability": "11",
"dev": "2:0",
"device": null,
"discard_alignment": "0",
"events": "",
"events_async": "",
"events_poll_msecs": "-1",
"ext_range": "1",
"hidden": "0",
"inflight": " 0 0",
"range": "1",
"removable": "1",
"ro": "0",
"size": "4096",
"stat": " 1 0 8 68 0 0 0 0 0 8 68 0 0 0 0",
"subsystem": "block",
"uevent": "MAJOR=2\nMINOR=0\nDEVNAME=fd0\nDEVTYPE=disk"
}
},
"/dev/sda": {
"DEVLINKS": "/dev/disk/by-id/scsi-0ATA_QEMU_HARDDISK_QM00001 /dev/disk/by-id/ata-QEMU_HARDDISK_QM00001 /dev/disk/by-id/scsi-1ATA_QEMU_HARDDISK_QM00001 /dev/disk/by-id/scsi-SATA_QEMU_HARDDISK_QM00001 /dev/disk/by-path/pci-0000:00:01.1-ata-1",
"DEVNAME": "/dev/sda",
"DEVPATH": "/devices/pci0000:00/0000:00:01.1/ata1/host0/target0:0:0/0:0:0:0/block/sda",
"DEVTYPE": "disk",
"ID_ATA": "1",
"ID_BUS": "ata",
"ID_MODEL": "QEMU_HARDDISK",
"ID_MODEL_ENC": "QEMU\\x20HARDDISK\\x20\\x20\\x20",
"ID_PATH": "pci-0000:00:01.1-ata-1",
"ID_PATH_TAG": "pci-0000_00_01_1-ata-1",
"ID_REVISION": "2.5+",
"ID_SCSI": "1",
"ID_SCSI_DI": "1",
"ID_SCSI_SN": "1",
"ID_SERIAL": "QEMU_HARDDISK_QM00001",
"ID_SERIAL_SHORT": "QM00001",
"ID_TYPE": "disk",
"ID_VENDOR": "ATA",
"ID_VENDOR_ENC": "ATA\\x20\\x20\\x20\\x20\\x20",
"MAJOR": "8",
"MINOR": "0",
"MPATH_SBIN_PATH": "/sbin",
"SCSI_IDENT_LUN_ATA": "QEMU_HARDDISK_QM00001",
"SCSI_IDENT_LUN_T10": "ATA_QEMU_HARDDISK_QM00001",
"SCSI_IDENT_LUN_VENDOR": "QM00001",
"SCSI_IDENT_SERIAL": "QM00001",
"SCSI_MODEL": "QEMU_HARDDISK",
"SCSI_MODEL_ENC": "QEMU\\x20HARDDISK\\x20\\x20\\x20",
"SCSI_REVISION": "2.5+",
"SCSI_TPGS": "0",
"SCSI_TYPE": "disk",
"SCSI_VENDOR": "ATA",
"SCSI_VENDOR_ENC": "ATA\\x20\\x20\\x20\\x20\\x20",
"SUBSYSTEM": "block",
"TAGS": ":systemd:",
"USEC_INITIALIZED": "82301144",
"attrs": {
"alignment_offset": "0",
"bdi": null,
"capability": "50",
"dev": "8:0",
"device": null,
"discard_alignment": "0",
"events": "",
"events_async": "",
"events_poll_msecs": "-1",
"ext_range": "256",
"hidden": "0",
"inflight": " 0 0",
"range": "16",
"removable": "0",
"ro": "0",
"size": "10737418240",
"stat": " 411 0 16968 119 0 0 0 0 0 264 0 0 0 0 0",
"subsystem": "block",
"uevent": "MAJOR=8\nMINOR=0\nDEVNAME=sda\nDEVTYPE=disk"
}
}
},
"dmcrypt": {},
"filesystem": {
},
"lvm": {},
"mount": [
{
"children": [
{
"children": [
{
"fstype": "securityfs",
"options": "rw,nosuid,nodev,noexec,relatime",
"source": "securityfs",
"target": "/sys/kernel/security"
},
{
"children": [
{
"fstype": "cgroup2",
"options": "rw,nosuid,nodev,noexec,relatime,nsdelegate",
"source": "cgroup2",
"target": "/sys/fs/cgroup/unified"
},
{
"fstype": "cgroup",
"options": "rw,nosuid,nodev,noexec,relatime,xattr,name=systemd",
"source": "cgroup",
"target": "/sys/fs/cgroup/systemd"
},
{
"fstype": "cgroup",
"options": "rw,nosuid,nodev,noexec,relatime,rdma",
"source": "cgroup",
"target": "/sys/fs/cgroup/rdma"
},
{
"fstype": "cgroup",
"options": "rw,nosuid,nodev,noexec,relatime,hugetlb",
"source": "cgroup",
"target": "/sys/fs/cgroup/hugetlb"
},
{
"fstype": "cgroup",
"options": "rw,nosuid,nodev,noexec,relatime,pids",
"source": "cgroup",
"target": "/sys/fs/cgroup/pids"
},
{
"fstype": "cgroup",
"options": "rw,nosuid,nodev,noexec,relatime,freezer",
"source": "cgroup",
"target": "/sys/fs/cgroup/freezer"
},
{
"fstype": "cgroup",
"options": "rw,nosuid,nodev,noexec,relatime,memory",
"source": "cgroup",
"target": "/sys/fs/cgroup/memory"
},
{
"fstype": "cgroup",
"options": "rw,nosuid,nodev,noexec,relatime,cpu,cpuacct",
"source": "cgroup",
"target": "/sys/fs/cgroup/cpu,cpuacct"
},
{
"fstype": "cgroup",
"options": "rw,nosuid,nodev,noexec,relatime,cpuset",
"source": "cgroup",
"target": "/sys/fs/cgroup/cpuset"
},
{
"fstype": "cgroup",
"options": "rw,nosuid,nodev,noexec,relatime,blkio",
"source": "cgroup",
"target": "/sys/fs/cgroup/blkio"
},
{
"fstype": "cgroup",
"options": "rw,nosuid,nodev,noexec,relatime,perf_event",
"source": "cgroup",
"target": "/sys/fs/cgroup/perf_event"
},
{
"fstype": "cgroup",
"options": "rw,nosuid,nodev,noexec,relatime,devices",
"source": "cgroup",
"target": "/sys/fs/cgroup/devices"
},
{
"fstype": "cgroup",
"options": "rw,nosuid,nodev,noexec,relatime,net_cls,net_prio",
"source": "cgroup",
"target": "/sys/fs/cgroup/net_cls,net_prio"
}
],
"fstype": "tmpfs",
"options": "ro,nosuid,nodev,noexec,mode=755",
"source": "tmpfs",
"target": "/sys/fs/cgroup"
},
{
"fstype": "pstore",
"options": "rw,nosuid,nodev,noexec,relatime",
"source": "pstore",
"target": "/sys/fs/pstore"
},
{
"fstype": "efivarfs",
"options": "rw,nosuid,nodev,noexec,relatime",
"source": "efivarfs",
"target": "/sys/firmware/efi/efivars"
},
{
"fstype": "bpf",
"options": "rw,nosuid,nodev,noexec,relatime,mode=700",
"source": "bpf",
"target": "/sys/fs/bpf"
},
{
"fstype": "debugfs",
"options": "rw,relatime",
"source": "debugfs",
"target": "/sys/kernel/debug"
},
{
"fstype": "fusectl",
"options": "rw,relatime",
"source": "fusectl",
"target": "/sys/fs/fuse/connections"
},
{
"fstype": "configfs",
"options": "rw,relatime",
"source": "configfs",
"target": "/sys/kernel/config"
}
],
"fstype": "sysfs",
"options": "rw,nosuid,nodev,noexec,relatime",
"source": "sysfs",
"target": "/sys"
},
{
"children": [
{
"fstype": "autofs",
"options": "rw,relatime,fd=44,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=14037",
"source": "systemd-1",
"target": "/proc/sys/fs/binfmt_misc"
}
],
"fstype": "proc",
"options": "rw,nosuid,nodev,noexec,relatime",
"source": "proc",
"target": "/proc"
},
{
"children": [
{
"fstype": "devpts",
"options": "rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000",
"source": "devpts",
"target": "/dev/pts"
},
{
"fstype": "tmpfs",
"options": "rw,nosuid,nodev",
"source": "tmpfs",
"target": "/dev/shm"
},
{
"fstype": "mqueue",
"options": "rw,relatime",
"source": "mqueue",
"target": "/dev/mqueue"
},
{
"fstype": "hugetlbfs",
"options": "rw,relatime,pagesize=2M",
"source": "hugetlbfs",
"target": "/dev/hugepages"
}
],
"fstype": "devtmpfs",
"options": "rw,nosuid,relatime,size=465256k,nr_inodes=116314,mode=755",
"source": "udev",
"target": "/dev"
},
{
"children": [
{
"fstype": "tmpfs",
"options": "rw,nosuid,nodev,noexec,relatime,size=5120k",
"source": "tmpfs",
"target": "/run/lock"
},
{
"fstype": "tmpfs",
"options": "rw,nosuid,nodev,relatime,size=100148k,mode=700,uid=999,gid=999",
"source": "tmpfs",
"target": "/run/user/999"
}
],
"fstype": "tmpfs",
"options": "rw,nosuid,noexec,relatime,size=100152k,mode=755",
"source": "tmpfs",
"target": "/run"
},
{
"fstype": "squashfs",
"options": "ro,noatime",
"source": "/dev/loop0",
"target": "/rofs"
},
{
"fstype": "squashfs",
"options": "ro,relatime",
"source": "/dev/loop2",
"target": "/usr/lib/modules"
},
{
"fstype": "tmpfs",
"options": "rw,nosuid,nodev,relatime",
"source": "tmpfs",
"target": "/tmp"
},
{
"fstype": "squashfs",
"options": "ro,relatime",
"source": "/dev/loop3",
"target": "/media/rack.lower"
},
{
"fstype": "squashfs",
"options": "ro,relatime",
"source": "/dev/loop4",
"target": "/media/region.lower"
},
{
"fstype": "squashfs",
"options": "ro,relatime",
"source": "/dev/loop0",
"target": "/media/filesystem"
},
{
"fstype": "overlay",
"options": "ro,relatime,lowerdir=/media/region.lower:/media/rack.lower:/media/filesystem",
"source": "overlay",
"target": "/media/region"
},
{
"fstype": "overlay",
"options": "ro,relatime,lowerdir=/media/rack.lower:/media/filesystem",
"source": "overlay",
"target": "/media/rack"
},
{
"fstype": "squashfs",
"options": "ro,nodev,relatime",
"source": "/dev/loop5",
"target": "/snap/core/6673"
},
{
"fstype": "squashfs",
"options": "ro,nodev,relatime",
"source": "/dev/loop6",
"target": "/snap/subiquity/1012"
}
],
"fstype": "overlay",
"options": "rw,relatime,lowerdir=//installer.squashfs://filesystem.squashfs,upperdir=/cow/upper,workdir=/cow/work",
"source": "/cow",
"target": "/"
}
],
"multipath": {},
"raid": {},
"zfs": {
"zpools": {}
}
}
}

View File

@ -7,7 +7,7 @@ for answers in examples/answers*.yaml; do
rm -f .subiquity/subiquity-debug.log
rm -f .subiquity/run/subiquity/updating
# The --foreground is important to avoid subiquity getting SIGTTOU-ed.
timeout --foreground 60 sh -c "LANG=C.UTF-8 python3 -m subiquity.cmd.tui --answers $answers --dry-run --snaps-from-examples --machine-config examples/mwhudson.json"
timeout --foreground 60 sh -c "LANG=C.UTF-8 python3 -m subiquity.cmd.tui --answers $answers --dry-run --snaps-from-examples --machine-config examples/simple.json"
python3 scripts/validate-yaml.py .subiquity/subiquity-curtin-install.conf
if grep passw0rd .subiquity/subiquity-debug.log | grep -v "Loaded answers" | grep -v "answers_action"; then
echo "password leaked into log file"

View File

@ -110,10 +110,22 @@ def main():
if opts.snaps_from_examples is None:
opts.snaps_from_examples = True
LOGFILE = setup_logger(dir=LOGDIR)
logger = logging.getLogger('subiquity')
logger.info("Starting SUbiquity v{}".format(VERSION))
logger.info("Arguments passed: {}".format(sys.argv))
block_log_dir = os.path.join(LOGDIR, "block")
os.makedirs(block_log_dir, exist_ok=True)
handler = logging.FileHandler(os.path.join(block_log_dir, 'discover.log'))
handler.setLevel('DEBUG')
handler.setFormatter(
logging.Formatter("%(asctime)s %(name)s:%(lineno)d %(message)s"))
logging.getLogger('probert').addHandler(handler)
handler.addFilter(lambda rec: rec.name != 'probert.network')
logging.getLogger('curtin').addHandler(handler)
logging.getLogger('block-discover').addHandler(handler)
signal.signal(signal.SIGINT, signal.SIG_IGN)
signal.signal(signal.SIGQUIT, signal.SIG_IGN)
@ -140,7 +152,7 @@ def main():
ui = SubiquityUI()
try:
subiquity_interface = Subiquity(ui, opts)
subiquity_interface = Subiquity(ui, opts, block_log_dir)
except ApplicationError as e:
logger.exception('Failed to load Subiquity interface')
print(e)

View File

@ -14,9 +14,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import enum
import json
import logging
from probert.storage import StorageInfo
import os
from subiquitycore.controller import BaseController
@ -39,6 +39,7 @@ from subiquity.ui.views.filesystem.probing import (
log = logging.getLogger("subiquitycore.controller.filesystem")
block_discover_log = logging.getLogger('block-discover')
BIOS_GRUB_SIZE_BYTES = 1 * 1024 * 1024 # 1MiB
PREP_GRUB_SIZE_BYTES = 8 * 1024 * 1024 # 8MiB
@ -57,6 +58,7 @@ class FilesystemController(BaseController):
def __init__(self, common):
super().__init__(common)
self.block_log_dir = common.get('block_log_dir')
self.model = self.base_model.filesystem
if self.opts.dry_run and self.opts.bootloader:
name = self.opts.bootloader.upper()
@ -69,28 +71,34 @@ class FilesystemController(BaseController):
self._probe_state = ProbeState.NOT_STARTED
def start(self):
block_discover_log.info("starting probe")
self._probe_state = ProbeState.PROBING
self.run_in_bg(self._bg_probe, self._probed)
self.loop.set_alarm_in(
5.0, lambda loop, ud: self._check_probe_timeout())
def _bg_probe(self, probe_types=None):
probed_data = self.prober.get_storage(probe_types=probe_types)
storage = {}
for path, data in probed_data["blockdev"].items():
storage[path] = StorageInfo({path: data})
return storage
return self.prober.get_storage(probe_types=probe_types)
def _probed(self, fut, restricted=False):
if not restricted and self._probe_state != ProbeState.PROBING:
log.debug("ignoring result %s for timed out probe", fut)
block_discover_log.debug(
"ignoring result %s for timed out probe", fut)
return
try:
storage = fut.result()
if restricted:
fname = 'probe-data-restricted.json'
else:
fname = 'probe-data.json'
with open(os.path.join(self.block_log_dir, fname), 'w') as fp:
json.dump(storage, fp)
self.model.load_probe_data(storage)
except Exception:
log.exception("probing failed restricted=%s", restricted)
block_discover_log.exception(
"probing failed restricted=%s", restricted)
if not restricted:
log.info("reprobing for blockdev only")
block_discover_log.info("reprobing for blockdev only")
# Should make a crash file for apport, arrange for it to be
# copied onto the installed system and tell user all this
# happened!
@ -100,7 +108,6 @@ class FilesystemController(BaseController):
if self.showing:
self.default()
else:
self.model.load_probe_data(storage)
self._probe_state = ProbeState.DONE
# Should do something here if probing found no devices.
if self.showing:
@ -241,7 +248,7 @@ class FilesystemController(BaseController):
if self.answers['guided']:
index = self.answers['guided-index']
disk = self.model.all_disks()[index]
v.choose_disk(None, disk.path)
v.choose_disk(None, disk)
def reset(self):
log.info("Resetting Filesystem model")
@ -264,6 +271,11 @@ class FilesystemController(BaseController):
if spec.get('mount') is None:
return
mount = self.model.add_mount(fs, spec['mount'])
if self.model.needs_bootloader_partition():
vol = fs.volume
if vol.type == "partition" and vol.device.type == "disk":
if vol.device._can_be_boot_disk():
self.make_boot_disk(vol.device)
return mount
def delete_mount(self, mount):
@ -273,8 +285,12 @@ class FilesystemController(BaseController):
def create_filesystem(self, volume, spec):
if spec['fstype'] is None:
return
fs = self.model.add_filesystem(volume, spec['fstype'])
fs = volume.original_fs()
if fs is None:
return
self.model.re_add_filesystem(fs)
else:
fs = self.model.add_filesystem(volume, spec['fstype'])
if isinstance(volume, Partition):
if spec['fstype'] == "swap":
volume.flag = "swap"
@ -282,6 +298,8 @@ class FilesystemController(BaseController):
volume.flag = ""
if spec['fstype'] == "swap":
self.model.add_mount(fs, "")
if spec['fstype'] is None and spec['use_swap']:
self.model.add_mount(fs, "")
self.create_mount(fs, spec)
return fs
@ -391,18 +409,30 @@ class FilesystemController(BaseController):
for subobj in obj.fs(), obj.constructed_device():
self.delete(subobj)
def reformat(self, disk):
if disk.type == "disk":
disk.preserve = False
self.clear(disk)
for p in list(disk.partitions()):
self.delete(p)
def partition_disk_handler(self, disk, partition, spec):
log.debug('partition_disk_handler: %s %s %s', disk, partition, spec)
log.debug('disk.freespace: {}'.format(disk.free_for_partitions))
if partition is not None:
partition.size = align_up(spec['size'])
if disk.free_for_partitions < 0:
raise Exception("partition size too large")
if 'size' in spec:
partition.size = align_up(spec['size'])
if disk.free_for_partitions < 0:
raise Exception("partition size too large")
self.delete_filesystem(partition.fs())
self.create_filesystem(partition, spec)
return
if len(disk.partitions()) == 0:
if disk.type == "disk":
disk.preserve = False
needs_boot = self.model.needs_bootloader_partition()
log.debug('model needs a bootloader partition? {}'.format(needs_boot))
can_be_boot = DeviceAction.MAKE_BOOT in disk.supported_actions
@ -426,10 +456,12 @@ class FilesystemController(BaseController):
log.debug('vg.freespace: {}'.format(vg.free_for_partitions))
if lv is not None:
lv.name = spec['name']
lv.size = align_up(spec['size'])
if vg.free_for_partitions < 0:
raise Exception("lv size too large")
if 'name' in spec:
lv.name = spec['name']
if 'size' in spec:
lv.size = align_up(spec['size'])
if vg.free_for_partitions < 0:
raise Exception("lv size too large")
self.delete_filesystem(lv.fs())
self.create_filesystem(lv, spec)
return
@ -478,21 +510,48 @@ class FilesystemController(BaseController):
def make_boot_disk(self, new_boot_disk):
boot_partition = None
for disk in self.model.all_disks():
for part in disk.partitions():
if part.flag in ("bios_grub", "boot", "prep"):
boot_partition = part
if self.model.bootloader == Bootloader.BIOS:
install_dev = self.model.grub_install_device
if install_dev:
boot_partition = install_dev._potential_boot_partition()
elif self.model.bootloader == Bootloader.UEFI:
mount = self.model._mount_for_path("/boot/efi")
if mount is not None:
boot_partition = mount.device.volume
elif self.model.bootloader == Bootloader.PREP:
boot_partition = self.model.grub_install_device
if boot_partition is not None:
boot_disk = boot_partition.device
full = boot_disk.free_for_partitions == 0
self.delete_partition(boot_partition)
if full:
largest_part = max(
boot_disk.partitions(), key=lambda p: p.size)
largest_part.size += boot_partition.size
if new_boot_disk.free_for_partitions < boot_partition.size:
largest_part = max(
new_boot_disk.partitions(), key=lambda p: p.size)
largest_part.size -= (
boot_partition.size - new_boot_disk.free_for_partitions)
self._create_boot_partition(new_boot_disk)
if boot_partition.preserve:
if self.model.bootloader == Bootloader.PREP:
boot_partition.wipe = None
elif self.model.bootloader == Bootloader.UEFI:
self.delete_mount(boot_partition.fs().mount())
else:
boot_disk = boot_partition.device
full = boot_disk.free_for_partitions == 0
self.delete_partition(boot_partition)
if full:
largest_part = max(
boot_disk.partitions(), key=lambda p: p.size)
largest_part.size += boot_partition.size
if new_boot_disk.free_for_partitions < boot_partition.size:
largest_part = max(
new_boot_disk.partitions(), key=lambda p: p.size)
largest_part.size -= (
boot_partition.size -
new_boot_disk.free_for_partitions)
if new_boot_disk._has_preexisting_partition():
if self.model.bootloader == Bootloader.BIOS:
self.model.grub_install_device = new_boot_disk
elif self.model.bootloader == Bootloader.UEFI:
part = new_boot_disk._potential_boot_partition()
if part.fs() is None:
self.model.add_filesystem(part, 'fat32')
self.model.add_mount(part.fs(), '/boot/efi')
elif self.model.bootloader == Bootloader.PREP:
part = new_boot_disk._potential_boot_partition()
part.wipe = 'zero'
self.model.grub_install_device = part
else:
new_boot_disk.preserve = False
self._create_boot_partition(new_boot_disk)

View File

@ -83,8 +83,8 @@ class TestFilesystemController(unittest.TestCase):
def test_make_boot_disk_BIOS(self):
controller = make_controller(Bootloader.BIOS)
disk1 = make_disk(controller.model)
disk2 = make_disk(controller.model)
disk1 = make_disk(controller.model, preserve=False)
disk2 = make_disk(controller.model, preserve=False)
disk2p1 = controller.model.add_partition(
disk2, size=disk2.free_for_partitions)
@ -103,10 +103,28 @@ class TestFilesystemController(unittest.TestCase):
self.assertEqual(disk2.partitions()[0].flag, "bios_grub")
self.assertEqual(controller.model.grub_install_device, disk2)
def test_make_boot_disk_BIOS_existing(self):
controller = make_controller(Bootloader.BIOS)
disk1 = make_disk(controller.model, preserve=True)
disk1p1 = controller.model.add_partition(
disk1, size=1 << 20, flag="bios_grub")
disk1p1.preserve = True
disk2 = make_disk(controller.model, preserve=False)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertEqual(controller.model.grub_install_device, None)
controller.make_boot_disk(disk1)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertEqual(controller.model.grub_install_device, disk1)
controller.make_boot_disk(disk2)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertEqual(controller.model.grub_install_device, disk2)
def test_make_boot_disk_UEFI(self):
controller = make_controller(Bootloader.UEFI)
disk1 = make_disk(controller.model)
disk2 = make_disk(controller.model)
disk1 = make_disk(controller.model, preserve=False)
disk2 = make_disk(controller.model, preserve=False)
disk2p1 = controller.model.add_partition(
disk2, size=disk2.free_for_partitions)
@ -130,10 +148,35 @@ class TestFilesystemController(unittest.TestCase):
efi_mnt = controller.model._mount_for_path("/boot/efi")
self.assertEqual(efi_mnt.device.volume, disk2.partitions()[0])
def test_make_boot_disk_UEFI_existing(self):
controller = make_controller(Bootloader.UEFI)
disk1 = make_disk(controller.model, preserve=True)
disk1p1 = controller.model.add_partition(
disk1, size=512 << 20, flag="boot")
disk1p1.preserve = True
disk2 = make_disk(controller.model, preserve=True)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertEqual(controller.model.grub_install_device, None)
efi_mnt = controller.model._mount_for_path("/boot/efi")
self.assertEqual(efi_mnt, None)
controller.make_boot_disk(disk1)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertEqual(controller.model.grub_install_device, None)
efi_mnt = controller.model._mount_for_path("/boot/efi")
self.assertEqual(efi_mnt.device.volume, disk1p1)
self.assertEqual(disk1p1.fs().fstype, "fat32")
controller.make_boot_disk(disk2)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertEqual(controller.model.grub_install_device, None)
efi_mnt = controller.model._mount_for_path("/boot/efi")
self.assertEqual(efi_mnt.device.volume, disk2.partitions()[0])
def test_make_boot_disk_PREP(self):
controller = make_controller(Bootloader.PREP)
disk1 = make_disk(controller.model)
disk2 = make_disk(controller.model)
disk1 = make_disk(controller.model, preserve=False)
disk2 = make_disk(controller.model, preserve=False)
disk2p1 = controller.model.add_partition(
disk2, size=disk2.free_for_partitions)
@ -157,3 +200,42 @@ class TestFilesystemController(unittest.TestCase):
self.assertEqual(
controller.model.grub_install_device,
disk2.partitions()[0])
def test_make_boot_disk_PREP_existing(self):
controller = make_controller(Bootloader.PREP)
disk1 = make_disk(controller.model, preserve=True)
disk1p1 = controller.model.add_partition(
disk1, size=8 << 20, flag="prep")
disk1p1.preserve = True
disk2 = make_disk(controller.model, preserve=False)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertEqual(controller.model.grub_install_device, None)
controller.make_boot_disk(disk1)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertEqual(controller.model.grub_install_device, disk1p1)
self.assertEqual(disk1p1.wipe, 'zero')
controller.make_boot_disk(disk2)
self.assertEqual(disk1.partitions(), [disk1p1])
self.assertEqual(disk1p1.wipe, None)
self.assertEqual(
controller.model.grub_install_device, disk2.partitions()[0])
self.assertEqual(disk2.partitions()[0].flag, "prep")
self.assertEqual(
controller.model.grub_install_device,
disk2.partitions()[0])
def test_mounting_partition_makes_boot_disk(self):
controller = make_controller(Bootloader.UEFI)
disk1 = make_disk(controller.model, preserve=True)
disk1p1 = controller.model.add_partition(
disk1, size=512 << 20, flag="boot")
disk1p1.preserve = True
disk1p2 = controller.model.add_partition(
disk1, size=disk1.free_for_partitions)
disk1p2.preserve = True
controller.partition_disk_handler(
disk1, disk1p2, {'fstype': 'ext4', 'mount': '/'})
efi_mnt = controller.model._mount_for_path("/boot/efi")
self.assertEqual(efi_mnt.device.volume, disk1p1)

View File

@ -59,11 +59,14 @@ class Subiquity(Application):
"InstallProgress",
]
def __init__(self, ui, opts):
def __init__(self, ui, opts, block_log_dir):
if not opts.bootloader == 'none' and platform.machine() != 's390x':
self.controllers.remove("Zdev")
super().__init__(ui, opts)
self.common['ui'].progress_completion += 1
self.common['block_log_dir'] = block_log_dir
if opts.snaps_from_examples:
connection = FakeSnapdConnection(
os.path.join(

View File

@ -17,15 +17,17 @@ from abc import ABC, abstractmethod
import attr
import collections
import enum
import glob
import itertools
import logging
import math
import os
import pathlib
import platform
import sys
from curtin.util import human2bytes
from curtin import storage_config
from probert.storage import StorageInfo
log = logging.getLogger('subiquity.models.filesystem')
@ -70,6 +72,9 @@ def _remove_backlinks(obj):
setattr(vv, backlink, None)
_type_to_cls = {}
def fsobj(typ):
def wrapper(c):
c.__attrs_post_init__ = _set_backlinks
@ -77,6 +82,7 @@ def fsobj(typ):
c.id = attributes.idfield(typ)
c._m = attr.ib(repr=None, default=None)
c = attr.s(cmp=False)(c)
_type_to_cls[typ] = c
return c
return wrapper
@ -308,6 +314,7 @@ def asdict(inst):
class DeviceAction(enum.Enum):
INFO = _("Info")
EDIT = _("Edit")
REFORMAT = _("Reformat")
PARTITION = _("Add Partition")
CREATE_LV = _("Create Logical Volume")
FORMAT = _("Format")
@ -332,6 +339,12 @@ def _generic_can_REMOVE(obj):
cd = obj.constructed_device()
if cd is None:
return False
if cd.preserve:
return _("Cannot remove selflabel from pre-exsting {cdtype} "
"{cdlabel}").format(
selflabel=obj.label,
cdtype=cd.desc(),
cdlabel=cd.label)
if isinstance(cd, Raid):
if obj in cd.spare_devices:
return True
@ -379,10 +392,17 @@ class _Formattable(ABC):
@property
def annotations(self):
return []
preserve = getattr(self, 'preserve', None)
if preserve is None:
return []
elif preserve:
return [_("existing")]
else:
return [_("new")]
# Filesystem
_fs = attributes.backlink()
_original_fs = attributes.backlink()
# Raid or LVM_VolGroup for now, but one day ZPool, BCache...
_constructed_device = attributes.backlink()
@ -397,13 +417,24 @@ class _Formattable(ABC):
]
fs = self.fs()
if fs is not None:
r = [_("formatted as {fstype}").format(fstype=fs.fstype)]
if fs.preserve:
format_desc = _("already formatted as {fstype}")
elif self.original_fs() is not None:
format_desc = _("to be reformatted as {fstype}")
else:
format_desc = _("to be formatted as {fstype}")
r = [format_desc.format(fstype=fs.fstype)]
if self._m.is_mounted_filesystem(fs.fstype):
m = fs.mount()
if m:
r.append(_("mounted at {path}").format(path=m.path))
else:
r.append(_("not mounted"))
elif fs.preserve:
if fs.mount() is None:
r.append(_("unused"))
else:
r.append(_("used"))
return r
else:
return [_("unused")]
@ -414,6 +445,9 @@ class _Formattable(ABC):
def fs(self):
return self._fs
def original_fs(self):
return self._original_fs
def constructed_device(self, skip_dm_crypt=True):
cd = self._constructed_device
if cd is None:
@ -499,7 +533,8 @@ class _Device(_Formattable, ABC):
if self._fs is not None:
return self._fs._available()
if self.free_for_partitions > 0:
return True
if not self._has_preexisting_partition():
return True
for p in self._partitions:
if p.available():
return True
@ -511,6 +546,13 @@ class _Device(_Formattable, ABC):
return True
return False
def _has_preexisting_partition(self):
for p in self._partitions:
if p.preserve:
return True
else:
return False
@property
def _can_DELETE(self):
mounted_partitions = 0
@ -547,21 +589,13 @@ class Disk(_Device):
serial = attr.ib(default=None)
path = attr.ib(default=None)
model = attr.ib(default=None)
wipe = attr.ib(default='superblock')
wipe = attr.ib(default=None)
preserve = attr.ib(default=False)
name = attr.ib(default="")
grub_device = attr.ib(default=False)
_info = attr.ib(default=None)
@classmethod
def from_info(self, model, info):
d = Disk(m=model, info=info)
d.serial = info.serial
d.path = info.name
d.model = info.model
return d
def info_for_display(self):
bus = self._info.raw.get('ID_BUS', None)
major = self._info.raw.get('MAJOR', None)
@ -602,6 +636,10 @@ class Disk(_Device):
def size(self):
return align_down(self._info.size)
@property
def annotations(self):
return []
def desc(self):
return _("local disk")
@ -611,10 +649,38 @@ class Disk(_Device):
return self.serial
return self.path
def _potential_boot_partition(self):
if self._m.bootloader == Bootloader.NONE:
return None
if not self._partitions:
return None
if self._m.bootloader == Bootloader.BIOS:
if self._partitions[0].flag == "bios_grub":
return self._partitions[0]
else:
return None
flag = {
Bootloader.UEFI: "boot",
Bootloader.PREP: "prep",
}[self._m.bootloader]
for p in self._partitions:
# XXX should check not extended in the UEFI case too (until we fix
# that bug)
if p.flag == flag:
return p
return None
def _can_be_boot_disk(self):
if self._m.bootloader == Bootloader.BIOS and self.ptable == "msdos":
return True
else:
return self._potential_boot_partition() is not None
@property
def supported_actions(self):
actions = [
DeviceAction.INFO,
DeviceAction.REFORMAT,
DeviceAction.PARTITION,
DeviceAction.FORMAT,
DeviceAction.REMOVE,
@ -624,7 +690,19 @@ class Disk(_Device):
return actions
_can_INFO = True
_can_PARTITION = property(lambda self: self.free_for_partitions > 0)
@property
def _can_REFORMAT(self):
if len(self._partitions) == 0:
return False
for p in self._partitions:
if p._constructed_device is not None:
return False
return True
_can_PARTITION = property(
lambda self: not self._has_preexisting_partition() and
self.free_for_partitions > 0)
_can_FORMAT = property(
lambda self: len(self._partitions) == 0 and
self._constructed_device is None)
@ -644,11 +722,16 @@ class Disk(_Device):
install_dev = self._m.grub_install_device
if install_dev is not None and install_dev.device is self:
return False
return self._fs is None and self._constructed_device is None
if self._has_preexisting_partition():
return self._can_be_boot_disk()
else:
return self._fs is None and self._constructed_device is None
@property
def ok_for_raid(self):
if self._fs is not None:
if self._fs.preserve:
return self._fs._mount is None
return False
if self._constructed_device is not None:
return False
@ -715,10 +798,14 @@ class Partition(_Formattable):
]
_can_EDIT = property(_generic_can_EDIT)
_can_REMOVE = property(_generic_can_REMOVE)
@property
def _can_DELETE(self):
if self.device._has_preexisting_partition():
return _("Cannot delete a single partition from a device that "
"already has partitions.")
if self.flag in ('boot', 'bios_grub', 'prep'):
return _("Cannot delete required bootloader partition")
return _generic_can_DELETE(self)
@ -728,6 +815,8 @@ class Partition(_Formattable):
if self.flag in ('boot', 'bios_grub', 'prep'):
return False
if self._fs is not None:
if self._fs.preserve:
return self._fs._mount is None
return False
if self._constructed_device is not None:
return False
@ -774,7 +863,9 @@ class Raid(_Device):
@property
def _can_EDIT(self):
if len(self._partitions) > 0:
if self.preserve:
return _("Cannot edit pre-existing RAIDs.")
elif len(self._partitions) > 0:
return _(
"Cannot edit {selflabel} because it has partitions.").format(
selflabel=self.label)
@ -790,6 +881,8 @@ class Raid(_Device):
@property
def ok_for_raid(self):
if self._fs is not None:
if self._fs.preserve:
return self._fs._mount is None
return False
if self._constructed_device is not None:
return False
@ -841,7 +934,9 @@ class LVM_VolGroup(_Device):
@property
def _can_EDIT(self):
if len(self._partitions) > 0:
if self.preserve:
return _("Cannot edit pre-existing volume groups.")
elif len(self._partitions) > 0:
return _(
"Cannot edit {selflabel} because it has logical "
"volumes.").format(
@ -849,7 +944,8 @@ class LVM_VolGroup(_Device):
else:
return _generic_can_EDIT(self)
_can_CREATE_LV = Disk._can_PARTITION
_can_CREATE_LV = property(
lambda self: not self.preserve and self.free_for_partitions > 0)
ok_for_raid = False
ok_for_lvm_vg = False
@ -895,7 +991,13 @@ class LVM_LogicalVolume(_Formattable):
]
_can_EDIT = True
_can_DELETE = True
@property
def _can_DELETE(self):
if self.volgroup._has_preexisting_partition():
return _("Cannot delete a single logical volume from a volume "
"group that already has logical volumes.")
return True
ok_for_raid = False
ok_for_lvm_vg = False
@ -939,7 +1041,10 @@ class Filesystem:
def _available(self):
# False if mounted or if fs does not require a mount, True otherwise.
if self._mount is None:
return FilesystemModel.is_mounted_filesystem(self.fstype)
if self.preserve:
return True
else:
return FilesystemModel.is_mounted_filesystem(self.fstype)
else:
return False
@ -1003,14 +1108,100 @@ class FilesystemModel(object):
def __init__(self):
self.bootloader = self._probe_bootloader()
self._disk_info = []
self._probe_data = None
self.reset()
def reset(self):
self._actions = [
Disk.from_info(self, info) for info in self._disk_info]
if self._probe_data is not None:
config = storage_config.extract_storage_config(self._probe_data)
self._actions = self._actions_from_config(
config["storage"]["config"],
self._probe_data['blockdev'])
else:
self._actions = []
self.grub_install_device = None
def _actions_from_config(self, config, blockdevs):
"""Convert curtin storage config into action instances.
curtin represents storage "actions" as defined in
https://curtin.readthedocs.io/en/latest/topics/storage.html. We
convert each action (that we know about) into an instance of
Disk, Partition, RAID, etc (unknown actions, e.g. bcache, are
just ignored).
We also filter out anything that can be reached from a currently
mounted device. The motivation here is only to exclude the media
subiquity is mounted from, so this might be a bit excessive but
hey it works.
Perhaps surprisingly the order of the returned actions matters.
The devices are presented in the filesystem view in the reverse
of the order they appear in _actions, which means that e.g. a
RAID appears higher up the list than the disks is is composed
of. This is quite important as it makes "unpeeling" existing
compound structures easy, you just delete the top device until
you only have disks left.
"""
byid = {}
objs = []
exclusions = set()
for action in config:
if action['type'] == 'mount':
exclusions.add(byid[action['device']])
continue
c = _type_to_cls.get(action['type'], None)
if c is None:
# Ignore any action we do not know how to process yet
# (e.g. bcache)
continue
kw = {}
for f in attr.fields(c):
n = f.name
if n not in action:
continue
v = action[n]
try:
if f.metadata.get('ref', False):
kw[n] = byid[v]
elif f.metadata.get('reflist', False):
kw[n] = [byid[id] for id in v]
else:
kw[n] = v
except KeyError:
# If a dependency of the current action has been
# ignored, we need to ignore the current action too
# (e.g. a bcache's filesystem).
continue
if kw['type'] == 'disk':
path = kw['path']
kw['info'] = StorageInfo({path: blockdevs[path]})
kw['preserve'] = True
obj = byid[action['id']] = c(m=self, **kw)
if action['type'] == "format":
obj.volume._original_fs = obj
objs.append(obj)
while True:
log.debug("exclusions %s", {e.id for e in exclusions})
next_exclusions = exclusions.copy()
for e in exclusions:
next_exclusions.update(itertools.chain(
dependencies(e), reverse_dependencies(e)))
if len(exclusions) == len(next_exclusions):
break
exclusions = next_exclusions
objs = [o for o in objs if o not in exclusions]
for o in objs:
if o.type == "partition" and o.flag == "swap" and o._fs is None:
fs = Filesystem(m=self, fstype="swap", volume=o, preserve=True)
o._original_fs = fs
objs.append(fs)
return objs
def _render_actions(self):
# The curtin storage config has the constraint that an action must be
# preceded by all the things that it depends on. We handle this by
@ -1023,8 +1214,6 @@ class FilesystemModel(object):
emitted_ids = set()
def emit(obj):
if obj.type == 'disk' and not obj.used:
return
r.append(asdict(obj))
emitted_ids.add(obj.id)
@ -1086,45 +1275,9 @@ class FilesystemModel(object):
}
return config
def _get_system_mounted_disks(self):
# This assumes a fairly vanilla setup. It won't list as
# mounted a disk that is only mounted via lvm, for example.
mounted_devs = []
with open('/proc/mounts', encoding=sys.getfilesystemencoding()) as pm:
for line in pm:
if line.startswith('/dev/'):
mounted_devs.append(line.split()[0][5:])
mounted_disks = set()
for dev in mounted_devs:
if os.path.exists('/sys/block/{}'.format(dev)):
mounted_disks.add('/dev/' + dev)
else:
paths = glob.glob('/sys/block/*/{}/partition'.format(dev))
if len(paths) == 1:
mounted_disks.add('/dev/' + paths[0].split('/')[3])
return mounted_disks
def load_probe_data(self, storage):
currently_mounted = self._get_system_mounted_disks()
for path, info in storage.items():
log.debug("fs probe %s", path)
if path in currently_mounted:
continue
if info.type == 'disk':
if info.is_virtual:
continue
if info.raw["MAJOR"] in ("2", "11"): # serial and cd devices
continue
if info.raw['attrs'].get('ro') == "1":
continue
if "ID_CDROM" in info.raw:
continue
# log.debug('disk={}\n{}'.format(
# path, json.dumps(data, indent=4, sort_keys=True)))
if info.size < self.lower_size_limit:
continue
self._disk_info.append(info)
self._actions.append(Disk.from_info(self, info))
def load_probe_data(self, probe_data):
self._probe_data = probe_data
self.reset()
def disk_by_path(self, path):
for a in self._actions:
@ -1252,6 +1405,10 @@ class FilesystemModel(object):
self._actions.append(fs)
return fs
def re_add_filesystem(self, fs):
_set_backlinks(fs)
self._actions.append(fs)
def remove_filesystem(self, fs):
if fs._mount:
raise Exception("can only remove unmounted filesystem")

View File

@ -117,12 +117,14 @@ def make_model(bootloader=None):
return model
def make_disk(model):
def make_disk(model, **kw):
serial = 'serial%s' % len(model._actions)
model._actions.append(Disk(
m=model, serial=serial,
info=FakeStorageInfo(size=100*(2**30))))
return model._actions[-1]
info=FakeStorageInfo(size=100*(2**30)),
**kw))
disk = model._actions[-1]
return disk
def make_model_and_disk(bootloader=None):
@ -130,12 +132,12 @@ def make_model_and_disk(bootloader=None):
return model, make_disk(model)
def make_partition(model, device=None, *, size=None, flag=""):
def make_partition(model, device=None, *, size=None, **kw):
if device is None:
device = make_disk(model)
if size is None:
size = device.free_for_partitions//2
partition = Partition(m=model, device=device, size=size, flag=flag)
partition = Partition(m=model, device=device, size=size, **kw)
model._actions.append(partition)
return partition
@ -180,16 +182,38 @@ def make_model_and_lv(bootloader=None):
class TestFilesystemModel(unittest.TestCase):
def test_disk_annotations(self):
# disks never have annotations
model, disk = make_model_and_disk()
self.assertEqual(disk.annotations, [])
disk.preserve = True
self.assertEqual(disk.annotations, [])
def test_partition_annotations(self):
model, disk = make_model_and_disk()
part = model.add_partition(disk, size=disk.free_for_partitions)
self.assertEqual(part.annotations, ['new'])
part.preserve = True
self.assertEqual(part.annotations, ['existing'])
part.flag = "boot"
self.assertEqual(part.annotations, ['existing', 'ESP'])
part.flag = "prep"
self.assertEqual(part.annotations, ['existing', 'PReP'])
part.flag = "bios_grub"
self.assertEqual(part.annotations, ['existing', 'bios_grub'])
def test_vg_default_annotations(self):
model, disk = make_model_and_disk()
vg = model.add_volgroup('vg-0', {disk})
self.assertEqual(vg.annotations, [])
self.assertEqual(vg.annotations, ['new'])
vg.preserve = True
self.assertEqual(vg.annotations, ['existing'])
def test_vg_encrypted_annotations(self):
model, disk = make_model_and_disk()
dm_crypt = model.add_dm_crypt(disk, key='passw0rd')
vg = model.add_volgroup('vg-0', {dm_crypt})
self.assertEqual(vg.annotations, ['encrypted'])
self.assertEqual(vg.annotations, ['new', 'encrypted'])
def _test_ok_for_xxx(self, model, make_new_device, attr,
test_partitions=True):
@ -205,6 +229,20 @@ class TestFilesystemModel(unittest.TestCase):
dev3 = make_new_device()
make_partition(model, dev3)
self.assertFalse(getattr(dev3, attr))
# Empty existing devices are ok
dev4 = make_new_device()
dev4.preserve = True
self.assertTrue(getattr(dev4, attr))
# A dev with an existing filesystem is ok (there is no
# way to remove the format)
dev5 = make_new_device()
dev5.preserve = True
fs = model.add_filesystem(dev5, 'ext4')
fs.preserve = True
self.assertTrue(dev5.ok_for_raid)
# But a existing, *mounted* filesystem is not.
model.add_mount(fs, '/')
self.assertFalse(dev5.ok_for_raid)
def test_disk_ok_for_xxx(self):
model = make_model()
@ -251,10 +289,23 @@ class TestFilesystemModel(unittest.TestCase):
self.assertEqual(partition.usage_labels(), ["unused"])
fs = model.add_filesystem(partition, 'ext4')
self.assertEqual(
partition.usage_labels(), ["formatted as ext4", "not mounted"])
model.add_mount(fs, '/')
partition.usage_labels(),
["to be formatted as ext4", "not mounted"])
partition._original_fs = fs
fs.preserve = True
partition.preserve = True
self.assertEqual(
partition.usage_labels(), ["formatted as ext4", "mounted at /"])
partition.usage_labels(),
["already formatted as ext4", "not mounted"])
model.remove_filesystem(fs)
fs2 = model.add_filesystem(partition, 'ext4')
self.assertEqual(
partition.usage_labels(),
["to be reformatted as ext4", "not mounted"])
model.add_mount(fs2, '/')
self.assertEqual(
partition.usage_labels(),
["to be reformatted as ext4", "mounted at /"])
def assertActionNotSupported(self, obj, action):
self.assertNotIn(action, obj.supported_actions)
@ -272,12 +323,24 @@ class TestFilesystemModel(unittest.TestCase):
vg = model.add_volgroup('vg1', {objects[0], objects[1]})
self.assertActionPossible(objects[0], DeviceAction.REMOVE)
# Cannot remove a device from a preexisting VG
vg.preserve = True
self.assertActionNotPossible(objects[0], DeviceAction.REMOVE)
vg.preserve = False
# Probably this removal should be a model method?
vg.devices.remove(objects[1])
objects[1]._constructed_device = None
self.assertActionNotPossible(objects[0], DeviceAction.REMOVE)
raid = model.add_raid('md0', 'raid1', set(objects[2:]), set())
self.assertActionPossible(objects[2], DeviceAction.REMOVE)
# Cannot remove a device from a preexisting RAID
raid.preserve = True
self.assertActionNotPossible(objects[2], DeviceAction.REMOVE)
raid.preserve = False
# Probably this removal should be a model method?
raid.devices.remove(objects[4])
objects[4]._constructed_device = None
@ -291,6 +354,31 @@ class TestFilesystemModel(unittest.TestCase):
model, disk = make_model_and_disk()
self.assertActionNotSupported(disk, DeviceAction.EDIT)
def test_disk_action_REFORMAT(self):
model = make_model()
disk1 = make_disk(model, preserve=False)
self.assertActionNotPossible(disk1, DeviceAction.REFORMAT)
disk1p1 = make_partition(model, disk1, preserve=False)
self.assertActionPossible(disk1, DeviceAction.REFORMAT)
model.add_volgroup('vg0', {disk1p1})
self.assertActionNotPossible(disk1, DeviceAction.REFORMAT)
disk2 = make_disk(model, preserve=True)
self.assertActionNotPossible(disk2, DeviceAction.REFORMAT)
disk2p1 = make_partition(model, disk2, preserve=True)
self.assertActionPossible(disk2, DeviceAction.REFORMAT)
model.add_volgroup('vg1', {disk2p1})
self.assertActionNotPossible(disk2, DeviceAction.REFORMAT)
disk3 = make_disk(model, preserve=False)
model.add_volgroup('vg2', {disk3})
self.assertActionNotPossible(disk3, DeviceAction.REFORMAT)
disk4 = make_disk(model, preserve=True)
model.add_volgroup('vg2', {disk4})
self.assertActionNotPossible(disk4, DeviceAction.REFORMAT)
def test_disk_action_PARTITION(self):
model, disk = make_model_and_disk()
self.assertActionPossible(disk, DeviceAction.PARTITION)
@ -299,6 +387,14 @@ class TestFilesystemModel(unittest.TestCase):
make_partition(model, disk, size=disk.free_for_partitions)
self.assertActionNotPossible(disk, DeviceAction.PARTITION)
# Can partition a disk with .preserve=True
disk2 = make_disk(model)
disk2.preserve = True
self.assertActionPossible(disk2, DeviceAction.PARTITION)
# But not if it has a partition.
make_partition(model, disk2, preserve=True)
self.assertActionNotPossible(disk2, DeviceAction.PARTITION)
def test_disk_action_CREATE_LV(self):
model, disk = make_model_and_disk()
self.assertActionNotSupported(disk, DeviceAction.CREATE_LV)
@ -321,13 +417,81 @@ class TestFilesystemModel(unittest.TestCase):
model, disk = make_model_and_disk()
self.assertActionNotSupported(disk, DeviceAction.DELETE)
def test_disk_action_MAKE_BOOT(self):
for bl in Bootloader:
model, disk = make_model_and_disk(bl)
if bl == Bootloader.NONE:
self.assertActionNotSupported(disk, DeviceAction.MAKE_BOOT)
else:
self.assertActionPossible(disk, DeviceAction.MAKE_BOOT)
def test_disk_action_MAKE_BOOT_NONE(self):
model, disk = make_model_and_disk(Bootloader.NONE)
self.assertActionNotSupported(disk, DeviceAction.MAKE_BOOT)
def test_disk_action_MAKE_BOOT_BIOS(self):
model = make_model(Bootloader.BIOS)
# Disks with msdos partition tables can always be the BIOS boot disk.
dos_disk = make_disk(model, ptable='msdos', preserve=True)
self.assertActionPossible(dos_disk, DeviceAction.MAKE_BOOT)
# Even if they have existing partitions
make_partition(
model, dos_disk, size=dos_disk.free_for_partitions, preserve=True)
self.assertActionPossible(dos_disk, DeviceAction.MAKE_BOOT)
# (we never create dos partition tables so no need to test
# preserve=False case).
# GPT disks with new partition tables can always be the BIOS boot disk
gpt_disk = make_disk(model, ptable='gpt', preserve=False)
self.assertActionPossible(gpt_disk, DeviceAction.MAKE_BOOT)
# Even if they are filled with partitions (we resize partitions to fit)
make_partition(model, gpt_disk, size=dos_disk.free_for_partitions)
self.assertActionPossible(gpt_disk, DeviceAction.MAKE_BOOT)
# GPT disks with existing partition tables but no partitions can be the
# BIOS boot disk (in general we ignore existing empty partition tables)
gpt_disk2 = make_disk(model, ptable='gpt', preserve=True)
self.assertActionPossible(gpt_disk2, DeviceAction.MAKE_BOOT)
# If there is an existing *partition* though, it cannot be the boot
# disk
make_partition(model, gpt_disk2, preserve=True)
self.assertActionNotPossible(gpt_disk2, DeviceAction.MAKE_BOOT)
# Unless there is already a bios_grub partition we can reuse
gpt_disk3 = make_disk(model, ptable='gpt', preserve=True)
make_partition(
model, gpt_disk3, flag="bios_grub", preserve=True)
make_partition(
model, gpt_disk3, preserve=True)
self.assertActionPossible(gpt_disk3, DeviceAction.MAKE_BOOT)
# Edge case city: the bios_grub partition has to be first
gpt_disk4 = make_disk(model, ptable='gpt', preserve=True)
make_partition(
model, gpt_disk4, preserve=True)
make_partition(
model, gpt_disk4, flag="bios_grub", preserve=True)
self.assertActionNotPossible(gpt_disk4, DeviceAction.MAKE_BOOT)
def _test_MAKE_BOOT_boot_partition(self, bl, flag):
# The logic for when MAKE_BOOT is enabled for both UEFI and PREP
# bootloaders turns out to be the same, modulo the special flag that
# has to be present on a partition.
model = make_model(bl)
# A disk with a new partition table can always be the UEFI/PREP boot
# disk.
new_disk = make_disk(model, preserve=False)
self.assertActionPossible(new_disk, DeviceAction.MAKE_BOOT)
# Even if they are filled with partitions (we resize partitions to fit)
make_partition(model, new_disk, size=new_disk.free_for_partitions)
self.assertActionPossible(new_disk, DeviceAction.MAKE_BOOT)
# A disk with an existing but empty partitions can also be the
# UEFI/PREP boot disk.
old_disk = make_disk(model, preserve=True)
self.assertActionPossible(old_disk, DeviceAction.MAKE_BOOT)
# If there is an existing partition though, it cannot.
make_partition(model, old_disk, preserve=True)
self.assertActionNotPossible(old_disk, DeviceAction.MAKE_BOOT)
# If there is an existing ESP/PReP partition though, fine!
make_partition(model, old_disk, flag=flag, preserve=True)
self.assertActionPossible(old_disk, DeviceAction.MAKE_BOOT)
def test_disk_action_MAKE_BOOT_UEFI(self):
self._test_MAKE_BOOT_boot_partition(Bootloader.UEFI, "boot")
def test_disk_action_MAKE_BOOT_PREP(self):
self._test_MAKE_BOOT_boot_partition(Bootloader.PREP, "prep")
def test_partition_action_INFO(self):
model, part = make_model_and_partition()
@ -339,6 +503,10 @@ class TestFilesystemModel(unittest.TestCase):
model.add_volgroup('vg1', {part})
self.assertActionNotPossible(part, DeviceAction.EDIT)
def test_partition_action_REFORMAT(self):
model, part = make_model_and_partition()
self.assertActionNotSupported(part, DeviceAction.REFORMAT)
def test_partition_action_PARTITION(self):
model, part = make_model_and_partition()
self.assertActionNotSupported(part, DeviceAction.PARTITION)
@ -378,6 +546,12 @@ class TestFilesystemModel(unittest.TestCase):
part = make_partition(model, flag=flag)
self.assertActionNotPossible(part, DeviceAction.DELETE)
# You cannot delete a partition from a disk that has
# pre-existing partitions (only reformat)
disk2 = make_disk(model, preserve=True)
disk2p1 = make_partition(model, disk2, preserve=True)
self.assertActionNotPossible(disk2p1, DeviceAction.DELETE)
def test_partition_action_MAKE_BOOT(self):
model, part = make_model_and_partition()
self.assertActionNotSupported(part, DeviceAction.MAKE_BOOT)
@ -396,6 +570,14 @@ class TestFilesystemModel(unittest.TestCase):
make_partition(model, raid2)
self.assertActionNotPossible(raid2, DeviceAction.EDIT)
raid3 = make_raid(model)
raid3.preserve = True
self.assertActionNotPossible(raid3, DeviceAction.EDIT)
def test_raid_action_REFORMAT(self):
model, raid = make_model_and_raid()
self.assertActionNotSupported(raid, DeviceAction.REFORMAT)
def test_raid_action_PARTITION(self):
model, raid = make_model_and_raid()
self.assertActionPossible(raid, DeviceAction.PARTITION)
@ -404,6 +586,14 @@ class TestFilesystemModel(unittest.TestCase):
make_partition(model, raid, size=raid.free_for_partitions)
self.assertActionNotPossible(raid, DeviceAction.PARTITION)
# Can partition a raid with .preserve=True
raid2 = make_raid(model)
raid2.preserve = True
self.assertActionPossible(raid2, DeviceAction.PARTITION)
# But not if it has a partition.
make_partition(model, raid2, preserve=True)
self.assertActionNotPossible(raid2, DeviceAction.PARTITION)
def test_raid_action_CREATE_LV(self):
model, raid = make_model_and_raid()
self.assertActionNotSupported(raid, DeviceAction.CREATE_LV)
@ -459,6 +649,14 @@ class TestFilesystemModel(unittest.TestCase):
model.add_logical_volume(vg, 'lv1', size=vg.free_for_partitions//2)
self.assertActionNotPossible(vg, DeviceAction.EDIT)
vg2 = make_vg(model)
vg2.preserve = True
self.assertActionNotPossible(vg2, DeviceAction.EDIT)
def test_vg_action_REFORMAT(self):
model, vg = make_model_and_vg()
self.assertActionNotSupported(vg, DeviceAction.REFORMAT)
def test_vg_action_PARTITION(self):
model, vg = make_model_and_vg()
self.assertActionNotSupported(vg, DeviceAction.PARTITION)
@ -470,6 +668,9 @@ class TestFilesystemModel(unittest.TestCase):
self.assertActionPossible(vg, DeviceAction.CREATE_LV)
model.add_logical_volume(vg, 'lv2', size=vg.free_for_partitions)
self.assertActionNotPossible(vg, DeviceAction.CREATE_LV)
vg2 = make_vg(model)
vg2.preserve = True
self.assertActionNotPossible(vg2, DeviceAction.CREATE_LV)
def test_vg_action_FORMAT(self):
model, vg = make_model_and_vg()
@ -503,6 +704,10 @@ class TestFilesystemModel(unittest.TestCase):
model, lv = make_model_and_lv()
self.assertActionPossible(lv, DeviceAction.EDIT)
def test_lv_action_REFORMAT(self):
model, lv = make_model_and_lv()
self.assertActionNotSupported(lv, DeviceAction.REFORMAT)
def test_lv_action_PARTITION(self):
model, lv = make_model_and_lv()
self.assertActionNotSupported(lv, DeviceAction.PARTITION)
@ -527,6 +732,10 @@ class TestFilesystemModel(unittest.TestCase):
model.add_mount(fs, '/')
self.assertActionPossible(lv, DeviceAction.DELETE)
lv2 = make_lv(model)
lv2.preserve = lv2.volgroup.preserve = True
self.assertActionNotPossible(lv2, DeviceAction.DELETE)
def test_lv_action_MAKE_BOOT(self):
model, lv = make_model_and_lv()
self.assertActionNotSupported(lv, DeviceAction.MAKE_BOOT)

View File

@ -58,9 +58,9 @@ class MountSelector(WidgetWrap):
opts.append((mnt, False))
if first_opt is None:
first_opt = len(opts)
opts.append((_('other'), True, OTHER))
opts.append((_('Other'), True, OTHER))
opts.append(('---', False)),
opts.append((_('leave unmounted'), True, LEAVE_UNMOUNTED))
opts.append((_('Leave unmounted'), True, LEAVE_UNMOUNTED))
self._selector = Selector(opts, first_opt)
connect_signal(self._selector, 'select', self._select_mount)
self._other = _MountEditor()

View File

@ -96,3 +96,66 @@ class ConfirmDeleteStretchy(Stretchy):
def cancel(self, sender=None):
self.parent.remove_overlay()
class ConfirmReformatStretchy(Stretchy):
def __init__(self, parent, obj):
self.parent = parent
self.obj = obj
fs = obj.fs()
if fs is not None:
title = _("Remove filesystem from {}").format(obj.desc())
lines = [
_(
"Do you really want to remove the existing filesystem "
"from {}?"
).format(obj.label),
"",
]
m = fs.mount()
if m is not None:
lines.append(_(
"It is formatted as {fstype} and mounted at "
"{path}").format(
fstype=fs.fstype,
path=m.path))
else:
lines.append(_(
"It is formatted as {fstype} and not mounted.").format(
fstype=fs.fstype))
else:
if obj.type == "lvm_volgroup":
things = _("logical volumes")
else:
things = _("partitions")
title = _("Remove all {things} from {obj}").format(
things=things, obj=obj.desc())
lines = [
_(
"Do you really want to remove all {things} from "
"{obj}?").format(
things=things, obj=obj.label),
"",
]
# XXX summarize partitions here?
delete_btn = danger_btn(label=_("Reformat"), on_press=self.confirm)
widgets = [
Text("\n".join(lines)),
Text(""),
button_pile([
delete_btn,
other_btn(label=_("Cancel"), on_press=self.cancel),
]),
]
super().__init__(title, widgets, 0, 2)
def confirm(self, sender=None):
self.parent.controller.reformat(self.obj)
self.parent.refresh_model_inputs()
self.parent.remove_overlay()
def cancel(self, sender=None):
self.parent.remove_overlay()

View File

@ -67,7 +67,7 @@ from subiquity.models.filesystem import (
humanize_size,
)
from .delete import ConfirmDeleteStretchy
from .delete import ConfirmDeleteStretchy, ConfirmReformatStretchy
from .disk_info import DiskInfoStretchy
from .helpers import summarize_device
from .lvm import VolGroupStretchy
@ -289,6 +289,7 @@ class DeviceList(WidgetWrap):
super().__init__(self.table)
_disk_INFO = _stretchy_shower(DiskInfoStretchy)
_disk_REFORMAT = _stretchy_shower(ConfirmReformatStretchy)
_disk_PARTITION = _stretchy_shower(PartitionStretchy)
_disk_FORMAT = _stretchy_shower(FormatEntireStretchy)
@ -353,7 +354,8 @@ class DeviceList(WidgetWrap):
else:
meth_name = '_{}_{}'.format(device.type, action.name)
meth = getattr(self, meth_name)
if not whynot and action == DeviceAction.DELETE:
if not whynot and action in [DeviceAction.DELETE,
DeviceAction.REFORMAT]:
label = Color.danger_button(ActionMenuOpenButton(label))
device_actions.append(Action(
label=label,

View File

@ -126,7 +126,7 @@ class GuidedDiskSelectionView(BaseView):
if disk.size >= dehumanize_size("6G"):
arrow = ClickableIcon(arrow)
connect_signal(
arrow, 'click', self.choose_disk, disk.path)
arrow, 'click', self.choose_disk, disk)
wrap = _wrap_button_row
else:
start, arrow, end = '', '', ''
@ -152,9 +152,8 @@ class GuidedDiskSelectionView(BaseView):
def cancel(self, btn=None):
self.controller.default()
def choose_disk(self, btn, disk_path):
self.model.reset()
disk = self.model.disk_by_path(disk_path)
def choose_disk(self, btn, disk):
self.controller.reformat(disk)
if self.method == "direct":
result = {
"size": disk.free_for_partitions,

View File

@ -25,6 +25,7 @@ import re
from urwid import connect_signal, Text
from subiquitycore.ui.form import (
BooleanField,
Form,
FormField,
simple_field,
@ -57,14 +58,23 @@ class FSTypeField(FormField):
# This will need to do something different for editing an
# existing partition that is already formatted.
options = [
('ext4', True),
('xfs', True),
('btrfs', True),
('---', False),
('swap', True),
('---', False),
('leave unformatted', True, None),
('ext4', True),
('xfs', True),
('btrfs', True),
('---', False),
('swap', True),
]
if form.existing_fs_type is None:
options = options + [
('---', False),
(_('Leave unformatted'), True, None),
]
else:
label = _('Leave formatted as {}').format(form.existing_fs_type)
options = [
(label, True, None),
('---', False),
] + options
sel = Selector(opts=options)
sel.value = None
return sel
@ -128,8 +138,14 @@ LVNameField = simple_field(LVNameEditor)
class PartitionForm(Form):
def __init__(self, model, max_size, initial, lvm_names):
def __init__(self, model, max_size, initial, lvm_names, device):
self.model = model
self.device = device
self.existing_fs_type = None
if device:
existing_fs = device.original_fs()
if existing_fs:
self.existing_fs_type = existing_fs.fstype
initial_path = initial.get('mount')
self.mountpoints = {
m.path: m.device.volume for m in self.model.all_mounts()
@ -143,15 +159,31 @@ class PartitionForm(Form):
if max_size is None:
self.remove_field('size')
connect_signal(self.fstype.widget, 'select', self.select_fstype)
self.form_pile = None
self.select_fstype(None, self.fstype.widget.value)
def select_fstype(self, sender, fstype):
self.mount.enabled = self.model.is_mounted_filesystem(fstype)
show_use = False
if fstype is None:
if self.existing_fs_type == "swap":
show_use = True
fstype = self.existing_fs_type
if self.form_pile is not None:
for i, (w, o) in enumerate(self.form_pile.contents):
if w is self.mount._table and show_use:
self.form_pile.contents[i] = (self.use_swap._table, o)
elif w is self.use_swap._table and not show_use:
self.form_pile.contents[i] = (self.mount._table, o)
if getattr(self.device, 'flag', None) != "boot":
self.mount.enabled = self.model.is_mounted_filesystem(fstype)
name = LVNameField(_("Name: "))
size = SizeField()
fstype = FSTypeField(_("Format:"))
mount = MountField(_("Mount:"))
use_swap = BooleanField(
_("Use as swap"),
help=_("Use this swap partition in the installed system."))
def clean_size(self, val):
if not val:
@ -202,6 +234,16 @@ class PartitionForm(Form):
return _("{} is already mounted at {}.").format(
dev.label.title(), mount)
def as_rows(self):
r = super().as_rows()
if self.existing_fs_type == "swap":
exclude = self.mount._table
else:
exclude = self.use_swap._table
i = r.index(exclude)
del r[i-1:i+1]
return r
bios_grub_partition_description = _(
"Required bootloader partition\n"
@ -217,8 +259,14 @@ boot_partition_description = _(
"Required bootloader partition\n"
"\n"
'This is the ESP / "EFI system partition" required by UEFI. Grub will be '
'installed onto this partition, which must be formatted as fat32. The '
'only aspect of this partition that can be edited is the size.')
'installed onto this partition, which must be formatted as fat32.')
boot_partition_description_size = _(
' The only aspect of this partition that can be edited is the size.')
boot_partition_description_reformat = _(
' You can choose whether to use the existing filesystem on this '
'partition or reformat it.')
prep_partition_description = _(
"Required bootloader partition\n"
@ -253,7 +301,11 @@ class PartitionStretchy(Stretchy):
max_size += self.partition.size
fs = self.partition.fs()
if fs is not None:
if partition.flag != "boot":
if fs.preserve:
initial['fstype'] = None
if fs.fstype == "swap":
initial['use_swap'] = fs.mount() is not None
elif partition.flag != "boot":
initial['fstype'] = fs.fstype
if self.model.is_mounted_filesystem(fs.fstype):
mount = fs.mount()
@ -275,7 +327,8 @@ class PartitionStretchy(Stretchy):
x += 1
initial['name'] = name
self.form = PartitionForm(self.model, max_size, initial, lvm_names)
self.form = PartitionForm(
self.model, max_size, initial, lvm_names, partition)
if not isinstance(disk, LVM_VolGroup):
self.form.remove_field('name')
@ -288,15 +341,39 @@ class PartitionStretchy(Stretchy):
if partition is not None:
if partition.flag == "boot":
opts = [Option(("fat32", True))]
self.form.fstype.widget.options = opts
self.form.fstype.widget.index = 0
self.form.mount.enabled = False
self.form.fstype.enabled = False
if partition.original_fs():
opts = [
Option((
_("Use existing fat32 filesystem"),
True,
None
)),
Option(("---", False)),
Option((
_("Reformat as fresh fat32 filesystem"),
True,
"fat32"
)),
]
self.form.fstype.widget.options = opts
if partition.fs().preserve:
self.form.fstype.widget.index = 0
else:
self.form.fstype.widget.index = 2
self.form.mount.enabled = False
else:
opts = [Option(("fat32", True))]
self.form.fstype.widget.options = opts
self.form.fstype.widget.index = 0
self.form.mount.enabled = False
self.form.fstype.enabled = False
elif partition.flag in ["bios_grub", "prep"]:
self.form.mount.enabled = False
self.form.fstype.enabled = False
self.form.size.enabled = False
if partition.preserve:
self.form.name.enabled = False
self.form.size.enabled = False
connect_signal(self.form, 'submit', self.done)
connect_signal(self.form, 'cancel', self.cancel)
@ -305,8 +382,13 @@ class PartitionStretchy(Stretchy):
focus_index = 0
if partition is not None:
if self.partition.flag == "boot":
desc = boot_partition_description
if self.partition.preserve:
desc += boot_partition_description_reformat
else:
desc += boot_partition_description_size
rows.extend([
Text(_(boot_partition_description)),
Text(_(desc)),
Text(""),
])
elif self.partition.flag == "bios_grub":
@ -322,8 +404,9 @@ class PartitionStretchy(Stretchy):
])
focus_index = 2
rows.extend(self.form.as_rows())
self.form.form_pile = Pile(rows)
widgets = [
Pile(rows),
self.form.form_pile,
Text(""),
self.form.buttons,
]
@ -373,6 +456,12 @@ class FormatEntireStretchy(Stretchy):
initial = {}
fs = device.fs()
if fs is not None:
if fs.preserve:
initial['fstype'] = None
if fs.fstype == "swap":
initial['use_swap'] = fs.mount() is not None
else:
initial['fstype'] = fs.fstype
initial['fstype'] = fs.fstype
if self.model.is_mounted_filesystem(fs.fstype):
mount = fs.mount()
@ -380,7 +469,7 @@ class FormatEntireStretchy(Stretchy):
initial['mount'] = mount.path
elif not isinstance(device, Disk):
initial['fstype'] = 'ext4'
self.form = PartitionForm(self.model, 0, initial, None)
self.form = PartitionForm(self.model, 0, initial, None, device)
self.form.remove_field('size')
self.form.remove_field('name')
@ -395,8 +484,9 @@ class FormatEntireStretchy(Stretchy):
Text(""),
]
rows.extend(self.form.as_rows())
self.form.form_pile = Pile(rows)
widgets = [
Pile(rows),
self.form.form_pile,
Text(""),
self.form.buttons,
]

View File

@ -1,6 +1,5 @@
import unittest
from unittest import mock
from collections import namedtuple
import urwid
@ -12,14 +11,12 @@ from subiquity.models.filesystem import (
Disk,
FilesystemModel,
)
from subiquity.models.tests.test_filesystem import (
FakeStorageInfo,
)
from subiquity.ui.views.filesystem.filesystem import FilesystemView
FakeStorageInfo = namedtuple(
'FakeStorageInfo', ['name', 'size', 'free', 'serial', 'model'])
FakeStorageInfo.__new__.__defaults__ = (None,) * len(FakeStorageInfo._fields)
class FilesystemViewTests(unittest.TestCase):
def make_view(self, model, devices=[]):
@ -35,9 +32,9 @@ class FilesystemViewTests(unittest.TestCase):
def test_one_disk(self):
model = mock.create_autospec(spec=FilesystemModel)
disk = Disk.from_info(model, FakeStorageInfo(
name='disk-name', size=100*(2**20), free=50*(2**20),
serial="DISK-SERIAL"))
disk = Disk(
m=model, serial="DISK-SERIAL",
info=FakeStorageInfo(size=100*(2**20), free=50*(2**20)))
view = self.make_view(model, [disk])
w = view_helpers.find_with_pred(
view,

View File

@ -50,6 +50,7 @@ class PartitionViewTests(unittest.TestCase):
view_helpers.click(stretchy.form.done_btn.base_widget)
valid_data['mount'] = '/'
valid_data['size'] = dehumanize_size(valid_data['size'])
valid_data['use_swap'] = False
view.controller.partition_disk_handler.assert_called_once_with(
stretchy.disk, None, valid_data)
@ -69,6 +70,28 @@ class PartitionViewTests(unittest.TestCase):
'size': dehumanize_size(form_data['size']),
'fstype': 'xfs',
'mount': None,
'use_swap': False,
}
view.controller.partition_disk_handler.assert_called_once_with(
stretchy.disk, stretchy.partition, expected_data)
def test_edit_existing_partition(self):
form_data = {
'fstype': "xfs",
}
model, disk = make_model_and_disk()
partition = model.add_partition(disk, 512*(2**20))
partition.preserve = True
model.add_filesystem(partition, "ext4")
view, stretchy = make_view(model, disk, partition)
self.assertFalse(stretchy.form.size.enabled)
self.assertTrue(stretchy.form.done_btn.enabled)
view_helpers.enter_data(stretchy.form, form_data)
view_helpers.click(stretchy.form.done_btn.base_widget)
expected_data = {
'fstype': 'xfs',
'mount': None,
'use_swap': False,
}
view.controller.partition_disk_handler.assert_called_once_with(
stretchy.disk, stretchy.partition, expected_data)
@ -88,6 +111,7 @@ class PartitionViewTests(unittest.TestCase):
'size': dehumanize_size(form_data['size']),
'fstype': "fat32",
'mount': '/boot/efi',
'use_swap': False,
}
view.controller.partition_disk_handler.assert_called_once_with(
stretchy.disk, stretchy.partition, expected_data)

View File

@ -82,4 +82,6 @@ def get_focus_path(w):
def enter_data(form, data):
for k, v in data.items():
getattr(form, k).value = v
bf = getattr(form, k)
assert bf.enabled, "%s is not enabled" % (k,)
bf.value = v