Use attribute metadata to drive maintenance of backlinks

By backlinks I mean for example the link a filesystem has to a mount
that uses it, or the link from a partition to the RAID it is a member
of.

This might be a little magical, but something like this is really nice
when interpreting config objects from curtin.
This commit is contained in:
Michael Hudson-Doyle 2019-04-01 15:11:42 +13:00
parent 67884836db
commit 18374cef06
1 changed files with 67 additions and 30 deletions

View File

@ -27,7 +27,48 @@ import platform
log = logging.getLogger('subiquity.models.filesystem')
def _set_backlinks(obj):
for field in attr.fields(type(obj)):
backlink = field.metadata.get('backlink')
if backlink is None:
continue
v = getattr(obj, field.name)
if v is None:
continue
if not isinstance(v, (list, set)):
v = [v]
for vv in v:
b = getattr(vv, backlink, None)
if isinstance(b, list):
b.append(obj)
elif isinstance(b, set):
b.add(obj)
else:
setattr(vv, backlink, obj)
def _remove_backlinks(obj):
for field in attr.fields(type(obj)):
backlink = field.metadata.get('backlink')
if backlink is None:
continue
v = getattr(obj, field.name)
if v is None:
continue
if not isinstance(v, (list, set)):
v = [v]
for vv in v:
b = getattr(vv, backlink, None)
if isinstance(b, list):
b.remove(obj)
elif isinstance(b, set):
b.remove(obj)
else:
setattr(vv, backlink, None)
def fsobj(c):
c.__attrs_post_init__ = _set_backlinks
return attr.s(cmp=False)(c)
@ -161,12 +202,18 @@ def idfield(base):
return attr.ib(default=attr.Factory(factory))
def ref():
return attr.ib(default=None, metadata={'ref': True})
def ref(*, backlink=None):
metadata = {'ref': True}
if backlink:
metadata['backlink'] = backlink
return attr.ib(default=None, metadata=metadata)
def reflist():
return attr.ib(default=attr.Factory(set), metadata={'reflist': True})
def reflist(*, backlink=None):
metadata = {'reflist': True}
if backlink:
metadata['backlink'] = backlink
return attr.ib(default=attr.Factory(set), metadata=metadata)
def const(value):
@ -478,7 +525,7 @@ class Partition(_Formattable):
id = idfield("part")
type = const("partition")
device = ref() # Disk
device = ref(backlink="_partitions") # Disk
size = attr.ib(default=None)
wipe = attr.ib(default=None)
flag = attr.ib(default=None)
@ -543,8 +590,8 @@ class Raid(_Device):
preserve = attr.ib(default=False)
name = attr.ib(default=None)
raidlevel = attr.ib(default=None) # 0, 1, 5, 6, 10
devices = reflist() # set([_Formattable])
spare_devices = reflist() # set([_Formattable])
devices = reflist(backlink="_constructed_device") # set([_Formattable])
spare_devices = reflist(backlink="_constructed_device") # ditto
ptable = attr.ib(default=None)
@property
@ -623,7 +670,7 @@ class LVM_VolGroup(_Device):
type = const("lvm_volgroup")
preserve = attr.ib(default=False)
name = attr.ib(default=None)
devices = reflist() # set([_Formattable])
devices = reflist(backlink="_constructed_device") # set([_Formattable])
_passphrase = attr.ib(default=None, repr=False)
def serialize_devices(self):
@ -691,7 +738,7 @@ class LVM_LogicalVolume(_Formattable):
id = idfield("lv")
type = const("lvm_partition")
name = attr.ib(default=None)
volgroup = ref() # LVM_VolGroup
volgroup = ref(backlink="_partitions") # LVM_VolGroup
size = attr.ib(default=None)
preserve = attr.ib(default=False)
@ -744,7 +791,7 @@ class Filesystem:
id = idfield("fs")
type = const("format")
fstype = attr.ib(default=None)
volume = ref() # _Formattable
volume = ref(backlink="_fs") # _Formattable
label = attr.ib(default=None)
uuid = attr.ib(default=None)
preserve = attr.ib(default=False)
@ -767,7 +814,7 @@ class Filesystem:
class Mount:
id = idfield("mount")
type = const("mount")
device = ref() # Filesystem
device = ref(backlink="_mount") # Filesystem
path = attr.ib(default=None)
def can_delete(self):
@ -983,9 +1030,7 @@ class FilesystemModel(object):
raise Exception("%s is already formatted" % (disk.label,))
p = Partition(device=disk, size=real_size, flag=flag, wipe=wipe)
if flag in ("boot", "bios_grub", "prep"):
disk._partitions.insert(0, p)
else:
disk._partitions.append(p)
disk._partitions.insert(0, disk._partitions.pop())
disk.ptable = 'gpt'
self._partitions.append(p)
return p
@ -993,7 +1038,7 @@ class FilesystemModel(object):
def remove_partition(self, part):
if part._fs or part._constructed_device:
raise Exception("can only remove empty partition")
part.device._partitions.remove(part)
_remove_backlinks(part)
self._partitions.remove(part)
if len(part.device._partitions) == 0:
part.device.ptable = None
@ -1007,43 +1052,35 @@ class FilesystemModel(object):
for d in devices | spare_devices:
if isinstance(d, Disk):
self._use_disk(d)
d._constructed_device = r
self._raids.append(r)
return r
def remove_raid(self, raid):
if raid._fs or raid._constructed_device or len(raid.partitions()):
raise Exception("can only remove empty RAID")
for d in raid.devices:
d._constructed_device = None
_remove_backlinks(raid)
self._raids.remove(raid)
def add_volgroup(self, name, devices, passphrase):
vg = LVM_VolGroup(name=name, devices=devices, passphrase=passphrase)
for d in devices:
if isinstance(d, Disk):
self._use_disk(d)
d._constructed_device = vg
self._vgs.append(vg)
return vg
def remove_volgroup(self, vg):
if len(vg._partitions):
raise Exception("can only remove empty VG")
for d in vg.devices:
d._constructed_device = None
_remove_backlinks(vg)
self._vgs.remove(vg)
def add_logical_volume(self, vg, name, size):
lv = LVM_LogicalVolume(volgroup=vg, name=name, size=size)
vg._partitions.append(lv)
self._lvs.append(lv)
return lv
def remove_logical_volume(self, lv):
if lv._fs:
raise Exception("can only remove empty LV")
lv.volgroup._partitions.remove(lv)
_remove_backlinks(lv)
self._lvs.remove(lv)
def add_filesystem(self, volume, fstype):
@ -1057,25 +1094,25 @@ class FilesystemModel(object):
self._use_disk(volume)
if volume._fs is not None:
raise Exception("%s is already formatted")
volume._fs = fs = Filesystem(volume=volume, fstype=fstype)
fs = Filesystem(volume=volume, fstype=fstype)
self._filesystems.append(fs)
return fs
def remove_filesystem(self, fs):
if fs._mount:
raise Exception("can only remove unmounted filesystem")
fs.volume._fs = None
_remove_backlinks(fs)
self._filesystems.remove(fs)
def add_mount(self, fs, path):
if fs._mount is not None:
raise Exception("%s is already mounted")
fs._mount = m = Mount(device=fs, path=path)
m = Mount(device=fs, path=path)
self._mounts.append(m)
return m
def remove_mount(self, mount):
mount.device._mount = None
_remove_backlinks(mount)
self._mounts.remove(mount)
def any_configuration_done(self):