class BcacheStorageLayoutBase(StorageLayoutBase): """Base that provides the logic for bcache layout types. This class is shared by `BcacheStorageLayout` and `BcacheLVMStorageLayout`. """ DEFAULT_CACHE_MODE = CACHE_MODE_TYPE.WRITETHROUGH cache_mode = forms.ChoiceField( choices=CACHE_MODE_TYPE_CHOICES, required=False ) cache_size = BytesOrPercentageField(required=False) cache_no_part = forms.BooleanField(required=False) def setup_cache_device_field(self): """Setup the possible cache devices.""" if self.boot_disk is None: return choices = [ (block_device.id, block_device.id) for block_device in self.block_devices if block_device != self.boot_disk ] invalid_choice_message = compose_invalid_choice_text( "cache_device", choices ) self.fields["cache_device"] = forms.ChoiceField( choices=choices, required=False, error_messages={"invalid_choice": invalid_choice_message}, ) def _find_best_cache_device(self): """Return the best possible cache device on the node.""" if self.boot_disk is None: return None block_devices = self.node.physicalblockdevice_set.exclude( id__in=[self.boot_disk.id] ).order_by("size") for block_device in block_devices: if "ssd" in block_device.tags: return block_device return None def get_cache_device(self): """Return the device to use for cache.""" # Return the requested cache device. if self.cleaned_data.get("cache_device"): for block_device in self.block_devices: if block_device.id == self.cleaned_data["cache_device"]: return block_device # Return the best bcache device. return self._find_best_cache_device() def get_cache_mode(self): """Get the cache mode. Return of None means to expand the entire cache device. """ if self.cleaned_data.get("cache_mode"): return self.cleaned_data["cache_mode"] else: return self.DEFAULT_CACHE_MODE def get_cache_size(self): """Get the size of the cache partition. Return of None means to expand the entire cache device. """ if self.cleaned_data.get("cache_size"): return self.cleaned_data["cache_size"] else: return None def get_cache_no_part(self): """Return true if use full cache device without partition.""" return self.cleaned_data["cache_no_part"] def create_cache_set(self): """Create the cache set based on the provided options.""" # Circular imports. from maasserver.models.partitiontable import PartitionTable cache_block_device = self.get_cache_device() cache_no_part = self.get_cache_no_part() if cache_no_part: return CacheSet.objects.get_or_create_cache_set_for_block_device( cache_block_device ) else: cache_partition_table = PartitionTable.objects.create( block_device=cache_block_device ) cache_partition = cache_partition_table.add_partition( size=self.get_cache_size() ) return CacheSet.objects.get_or_create_cache_set_for_partition( cache_partition ) def clean(self): # Circular imports. from maasserver.models.blockdevice import MIN_BLOCK_DEVICE_SIZE cleaned_data = super(BcacheStorageLayoutBase, self).clean() cache_device = self.get_cache_device() cache_size = self.get_cache_size() cache_no_part = self.get_cache_no_part() if cache_size is not None and cache_no_part: error_msg = ( "Cannot use cache_size and cache_no_part at the same time." ) set_form_error(self, "cache_size", error_msg) set_form_error(self, "cache_no_part", error_msg) elif cache_device is not None and cache_size is not None: if is_percentage(cache_size): cache_size = calculate_size_from_percentage( cache_device.size, cache_size ) if cache_size < MIN_BLOCK_DEVICE_SIZE: set_form_error( self, "cache_size", "Size is too small. Minimum size is %s." % MIN_BLOCK_DEVICE_SIZE, ) if cache_size > cache_device.size: set_form_error( self, "cache_size", "Size is too large. Maximum size is %s." % (cache_device.size), ) cleaned_data["cache_size"] = cache_size return cleaned_data
class StorageLayoutBase(Form): """Base class all storage layouts extend from.""" boot_size = BytesOrPercentageField(required=False) root_size = BytesOrPercentageField(required=False) def __init__(self, node, params: dict = None): super(StorageLayoutBase, self).__init__( data=({} if params is None else params) ) self.node = node self.block_devices = self._load_physical_block_devices() self.boot_disk = node.get_boot_disk() self.setup_root_device_field() def _load_physical_block_devices(self): """Load all the `PhysicalBlockDevice`'s for node.""" # The websocket prefetches node.blockdevice_set, creating a queryset # on node.physicalblockdevice_set adds addtional queries. physical_bds = [] for bd in self.node.blockdevice_set.all(): try: physical_bds.append(bd.physicalblockdevice) except Exception: pass return sorted(physical_bds, key=lambda bd: bd.id) def setup_root_device_field(self): """Setup the possible root devices.""" choices = [ (block_device.id, block_device.id) for block_device in self.block_devices ] invalid_choice_message = compose_invalid_choice_text( "root_device", choices ) self.fields["root_device"] = forms.ChoiceField( choices=choices, required=False, error_messages={"invalid_choice": invalid_choice_message}, ) def _clean_size(self, field, min_size=None, max_size=None): """Clean a size field.""" size = self.cleaned_data[field] if size is None: return None if is_percentage(size): # Calculate the percentage not counting the EFI partition. size = calculate_size_from_percentage( self.boot_disk.size - EFI_PARTITION_SIZE, size ) if min_size is not None and size < min_size: raise ValidationError( "Size is too small. Minimum size is %s." % min_size ) if max_size is not None and size > max_size: raise ValidationError( "Size is too large. Maximum size is %s." % max_size ) return size def clean_boot_size(self): """Clean the boot_size field.""" if self.boot_disk is not None: return self._clean_size( "boot_size", MIN_BOOT_PARTITION_SIZE, ( self.boot_disk.size - EFI_PARTITION_SIZE - MIN_ROOT_PARTITION_SIZE ), ) else: return None def clean_root_size(self): """Clean the root_size field.""" if self.boot_disk is not None: return self._clean_size( "root_size", MIN_ROOT_PARTITION_SIZE, ( self.boot_disk.size - EFI_PARTITION_SIZE - MIN_BOOT_PARTITION_SIZE ), ) else: return None def clean(self): """Validate the data.""" cleaned_data = super(StorageLayoutBase, self).clean() if len(self.block_devices) == 0: raise StorageLayoutMissingBootDiskError( "Node doesn't have any storage devices to configure." ) disk_size = self.boot_disk.size total_size = EFI_PARTITION_SIZE + self.get_boot_size() root_size = self.get_root_size() if root_size is not None and total_size + root_size > disk_size: raise ValidationError( "Size of the boot partition and root partition are larger " "than the available space on the boot disk." ) return cleaned_data def get_root_device(self): """Get the device that should be the root partition. Return the boot_disk if no root_device was defined. """ if self.cleaned_data.get("root_device"): root_id = self.cleaned_data["root_device"] return self.node.physicalblockdevice_set.get(id=root_id) else: # User didn't specify a root disk so use the currently defined # boot disk. return self.boot_disk def get_boot_size(self): """Get the size of the boot partition.""" if self.cleaned_data.get("boot_size"): return self.cleaned_data["boot_size"] else: return 0 def get_root_size(self): """Get the size of the root partition. Return of None means to expand the remaining of the disk. """ if self.cleaned_data.get("root_size"): return self.cleaned_data["root_size"] else: return None def create_basic_layout(self, boot_size=None): """Create the basic layout that is similar for all layout types. :return: The created root partition. """ # Circular imports. from maasserver.models.filesystem import Filesystem from maasserver.models.partitiontable import PartitionTable boot_partition_table = PartitionTable.objects.create( block_device=self.boot_disk ) bios_boot_method = self.node.get_bios_boot_method() node_arch, _ = self.node.split_arch() if ( boot_partition_table.table_type == PARTITION_TABLE_TYPE.GPT and bios_boot_method == "uefi" and node_arch != "ppc64el" ): # Add EFI partition only if booting UEFI and not a ppc64el # architecture. efi_partition = boot_partition_table.add_partition( size=EFI_PARTITION_SIZE, bootable=True ) Filesystem.objects.create( partition=efi_partition, fstype=FILESYSTEM_TYPE.FAT32, label="efi", mount_point="/boot/efi", ) elif ( bios_boot_method != "uefi" and node_arch == "arm64" and boot_size is None ): # Add boot partition only if booting an arm64 architecture and # not UEFI and boot_size is None. boot_partition = boot_partition_table.add_partition( size=MIN_BOOT_PARTITION_SIZE, bootable=True ) Filesystem.objects.create( partition=boot_partition, fstype=FILESYSTEM_TYPE.EXT4, label="boot", mount_point="/boot", ) if boot_size is None: boot_size = self.get_boot_size() if boot_size > 0: boot_partition = boot_partition_table.add_partition( size=boot_size, bootable=True ) Filesystem.objects.create( partition=boot_partition, fstype=FILESYSTEM_TYPE.EXT4, label="boot", mount_point="/boot", ) root_device = self.get_root_device() root_size = self.get_root_size() if root_device == self.boot_disk: partition_table = boot_partition_table root_device = self.boot_disk else: partition_table = PartitionTable.objects.create( block_device=root_device ) # Fix the maximum root_size for MBR. max_mbr_size = get_max_mbr_partition_size() if ( partition_table.table_type == PARTITION_TABLE_TYPE.MBR and root_size is not None and root_size > max_mbr_size ): root_size = max_mbr_size root_partition = partition_table.add_partition(size=root_size) return root_partition, boot_partition_table def configure(self, allow_fallback=True): """Configure the storage for the node.""" if not self.is_valid(): raise StorageLayoutFieldsError(self.errors) self.node._clear_full_storage_configuration() return self.configure_storage(allow_fallback) def configure_storage(self, allow_fallback): """Configure the storage of the node. Sub-classes should override this method not `configure`. """ raise NotImplementedError() def is_uefi_partition(self, partition): """Returns whether or not the given partition is a UEFI partition.""" if partition.partition_table.table_type != PARTITION_TABLE_TYPE.GPT: return False if partition.size != EFI_PARTITION_SIZE: return False if not partition.bootable: return False fs = partition.get_effective_filesystem() if fs is None: return False if fs.fstype != FILESYSTEM_TYPE.FAT32: return False if fs.label != "efi": return False if fs.mount_point != "/boot/efi": return False return True def is_boot_partition(self, partition): """Returns whether or not the given partition is a boot partition.""" if not partition.bootable: return False fs = partition.get_effective_filesystem() if fs is None: return False if fs.fstype != FILESYSTEM_TYPE.EXT4: return False if fs.label != "boot": return False if fs.mount_point != "/boot": return False return True def is_layout(self): """Returns the block device the layout was applied on.""" raise NotImplementedError()
class StorageLayoutBase(Form): """Base class all storage layouts extend from.""" boot_size = BytesOrPercentageField(required=False) root_size = BytesOrPercentageField(required=False) def __init__(self, node, params: dict=None): super(StorageLayoutBase, self).__init__( data=({} if params is None else params)) self.node = node self.block_devices = self._load_physical_block_devices() self.boot_disk = node.get_boot_disk() self.setup_root_device_field() def _load_physical_block_devices(self): """Load all the `PhysicalBlockDevice`'s for node.""" return list(self.node.physicalblockdevice_set.order_by('id').all()) def setup_root_device_field(self): """Setup the possible root devices.""" choices = [ (block_device.id, block_device.id) for block_device in self.block_devices ] invalid_choice_message = compose_invalid_choice_text( 'root_device', choices) self.fields['root_device'] = forms.ChoiceField( choices=choices, required=False, error_messages={'invalid_choice': invalid_choice_message}) def _clean_size(self, field, min_size=None, max_size=None): """Clean a size field.""" size = self.cleaned_data[field] if size is None: return None if is_percentage(size): # Calculate the percentage not counting the EFI partition. size = calculate_size_from_percentage( self.boot_disk.size - EFI_PARTITION_SIZE, size) if min_size is not None and size < min_size: raise ValidationError( "Size is too small. Minimum size is %s." % min_size) if max_size is not None and size > max_size: raise ValidationError( "Size is too large. Maximum size is %s." % max_size) return size def clean_boot_size(self): """Clean the boot_size field.""" if self.boot_disk is not None: return self._clean_size( 'boot_size', MIN_BOOT_PARTITION_SIZE, ( self.boot_disk.size - EFI_PARTITION_SIZE - MIN_ROOT_PARTITION_SIZE)) else: return None def clean_root_size(self): """Clean the root_size field.""" if self.boot_disk is not None: return self._clean_size( 'root_size', MIN_ROOT_PARTITION_SIZE, ( self.boot_disk.size - EFI_PARTITION_SIZE - MIN_BOOT_PARTITION_SIZE)) else: return None def clean(self): """Validate the data.""" cleaned_data = super(StorageLayoutBase, self).clean() if len(self.block_devices) == 0: raise StorageLayoutMissingBootDiskError( "Node doesn't have any storage devices to configure.") disk_size = self.boot_disk.size total_size = ( EFI_PARTITION_SIZE + self.get_boot_size()) root_size = self.get_root_size() if root_size is not None and total_size + root_size > disk_size: raise ValidationError( "Size of the boot partition and root partition are larger " "than the available space on the boot disk.") return cleaned_data def get_root_device(self): """Get the device that should be the root partition. Return of None means to use the boot disk. """ if self.cleaned_data.get('root_device'): root_id = self.cleaned_data['root_device'] return self.node.physicalblockdevice_set.get(id=root_id) else: return None def get_boot_size(self): """Get the size of the boot partition.""" if self.cleaned_data.get('boot_size'): return self.cleaned_data['boot_size'] else: return 0 def get_root_size(self): """Get the size of the root partition. Return of None means to expand the remaining of the disk. """ if self.cleaned_data.get('root_size'): return self.cleaned_data['root_size'] else: return None def create_basic_layout(self, boot_size=None): """Create the basic layout that is similar for all layout types. :return: The created root partition. """ # Circular imports. from maasserver.models.filesystem import Filesystem from maasserver.models.partitiontable import PartitionTable boot_partition_table = PartitionTable.objects.create( block_device=self.boot_disk) bios_boot_method = self.node.get_bios_boot_method() node_arch, _ = self.node.split_arch() if (boot_partition_table.table_type == PARTITION_TABLE_TYPE.GPT and bios_boot_method == "uefi" and node_arch != "ppc64el"): # Add EFI partition only if booting UEFI and not a ppc64el # architecture. efi_partition = boot_partition_table.add_partition( size=EFI_PARTITION_SIZE, bootable=True) Filesystem.objects.create( partition=efi_partition, fstype=FILESYSTEM_TYPE.FAT32, label="efi", mount_point="/boot/efi") elif (bios_boot_method != "uefi" and node_arch == "arm64" and boot_size is None): # Add boot partition only if booting an arm64 architecture and # not UEFI and boot_size is None. boot_partition = boot_partition_table.add_partition( size=MIN_BOOT_PARTITION_SIZE, bootable=True) Filesystem.objects.create( partition=boot_partition, fstype=FILESYSTEM_TYPE.EXT4, label="boot", mount_point="/boot") if boot_size is None: boot_size = self.get_boot_size() if boot_size > 0: boot_partition = boot_partition_table.add_partition( size=boot_size, bootable=True) Filesystem.objects.create( partition=boot_partition, fstype=FILESYSTEM_TYPE.EXT4, label="boot", mount_point="/boot") root_device = self.get_root_device() root_size = self.get_root_size() if root_device is None or root_device == self.boot_disk: partition_table = boot_partition_table root_device = self.boot_disk else: partition_table = PartitionTable.objects.create( block_device=root_device) # Fix the maximum root_size for MBR. max_mbr_size = get_max_mbr_partition_size() if (partition_table.table_type == PARTITION_TABLE_TYPE.MBR and root_size is not None and root_size > max_mbr_size): root_size = max_mbr_size root_partition = partition_table.add_partition( size=root_size) return root_partition, boot_partition_table def configure(self, allow_fallback=True): """Configure the storage for the node.""" if not self.is_valid(): raise StorageLayoutFieldsError(self.errors) self.node._clear_full_storage_configuration() return self.configure_storage(allow_fallback) def configure_storage(self, allow_fallback): """Configure the storage of the node. Sub-classes should override this method not `configure`. """ raise NotImplementedError()
class LVMStorageLayout(StorageLayoutBase): """LVM layout. NAME SIZE TYPE FSTYPE MOUNTPOINT sda 100G disk sda1 512M part fat32 /boot/efi sda2 99.5G part lvm-pv(vgroot) vgroot 99.5G lvm lvroot 99.5G lvm ext4 / """ DEFAULT_VG_NAME = "vgroot" DEFAULT_LV_NAME = "lvroot" vg_name = forms.CharField(required=False) lv_name = forms.CharField(required=False) lv_size = BytesOrPercentageField(required=False) def get_vg_name(self): """Get the name of the volume group.""" if self.cleaned_data.get("vg_name"): return self.cleaned_data["vg_name"] else: return self.DEFAULT_VG_NAME def get_lv_name(self): """Get the name of the logical volume.""" if self.cleaned_data.get("lv_name"): return self.cleaned_data["lv_name"] else: return self.DEFAULT_LV_NAME def get_lv_size(self): """Get the size of the logical volume. Return of None means to expand the entire volume group. """ if self.cleaned_data.get("lv_size"): return self.cleaned_data["lv_size"] else: return None def get_calculated_lv_size(self, volume_group): """Return the size of the logical volume based on `lv_size` or the available size in the `volume_group`.""" lv_size = self.get_lv_size() if lv_size is None: lv_size = volume_group.get_size() return lv_size def clean(self): """Validate the lv_size.""" cleaned_data = super(LVMStorageLayout, self).clean() lv_size = self.get_lv_size() if lv_size is not None: root_size = self.get_root_size() if root_size is None: root_size = ( self.boot_disk.size - EFI_PARTITION_SIZE - self.get_boot_size() ) if is_percentage(lv_size): lv_size = calculate_size_from_percentage(root_size, lv_size) if lv_size < MIN_ROOT_PARTITION_SIZE: set_form_error( self, "lv_size", "Size is too small. Minimum size is %s." % MIN_ROOT_PARTITION_SIZE, ) if lv_size > root_size: set_form_error( self, "lv_size", "Size is too large. Maximum size is %s." % root_size, ) cleaned_data["lv_size"] = lv_size return cleaned_data def configure_storage(self, allow_fallback): """Create the LVM configuration.""" # Circular imports. from maasserver.models.filesystem import Filesystem from maasserver.models.filesystemgroup import VolumeGroup root_partition, root_partition_table = self.create_basic_layout() # Add extra partitions if MBR and extra space. partitions = [root_partition] if root_partition_table.table_type == PARTITION_TABLE_TYPE.MBR: available_size = root_partition_table.get_available_size() while available_size > MIN_PARTITION_SIZE: part = root_partition_table.add_partition() partitions.append(part) available_size -= part.size # Create the volume group and logical volume. volume_group = VolumeGroup.objects.create_volume_group( self.get_vg_name(), block_devices=[], partitions=partitions ) logical_volume = volume_group.create_logical_volume( self.get_lv_name(), self.get_calculated_lv_size(volume_group) ) Filesystem.objects.create( block_device=logical_volume, fstype=FILESYSTEM_TYPE.EXT4, label="root", mount_point="/", ) return "lvm" def is_layout(self): """Checks if the node is using an LVM layout.""" for bd in self.block_devices: pt = bd.get_partitiontable() if pt is None: continue for partition in pt.partitions.all(): # On UEFI systems the first partition is for the bootloader. If # found check the next partition. if partition.get_partition_number() == 1 and self.is_uefi_partition( partition ): continue # Most layouts allow you to define a boot partition, skip it # if its defined. if self.is_boot_partition(partition): continue # Check if the partition is an LVM PV. fs = partition.get_effective_filesystem() if fs is None: break if fs.fstype != FILESYSTEM_TYPE.LVM_PV: break fsg = fs.filesystem_group if fsg is None: break # Don't use querysets here incase the given data has already # been cached. if len(fsg.virtual_devices.all()) == 0: break # self.configure() always puts the LV as the first device. vbd = fsg.virtual_devices.all()[0] vfs = vbd.get_effective_filesystem() if vfs is None: break if vfs.fstype != FILESYSTEM_TYPE.EXT4: break if vfs.label != "root": break if vfs.mount_point != "/": break return bd return None
class LVMStorageLayout(StorageLayoutBase): """LVM layout. NAME SIZE TYPE FSTYPE MOUNTPOINT sda 100G disk sda1 512M part fat32 /boot/efi sda2 99.5G part lvm-pv(vgroot) vgroot 99.5G lvm lvroot 99.5G lvm ext4 / """ DEFAULT_VG_NAME = "vgroot" DEFAULT_LV_NAME = "lvroot" vg_name = forms.CharField(required=False) lv_name = forms.CharField(required=False) lv_size = BytesOrPercentageField(required=False) def get_vg_name(self): """Get the name of the volume group.""" if self.cleaned_data.get('vg_name'): return self.cleaned_data['vg_name'] else: return self.DEFAULT_VG_NAME def get_lv_name(self): """Get the name of the logical volume.""" if self.cleaned_data.get('lv_name'): return self.cleaned_data['lv_name'] else: return self.DEFAULT_LV_NAME def get_lv_size(self): """Get the size of the logical volume. Return of None means to expand the entire volume group. """ if self.cleaned_data.get('lv_size'): return self.cleaned_data['lv_size'] else: return None def get_calculated_lv_size(self, volume_group): """Return the size of the logical volume based on `lv_size` or the available size in the `volume_group`.""" lv_size = self.get_lv_size() if lv_size is None: lv_size = volume_group.get_size() return lv_size def clean(self): """Validate the lv_size.""" cleaned_data = super(LVMStorageLayout, self).clean() lv_size = self.get_lv_size() if lv_size is not None: root_size = self.get_root_size() if root_size is None: root_size = ( self.boot_disk.size - EFI_PARTITION_SIZE - self.get_boot_size()) if is_percentage(lv_size): lv_size = calculate_size_from_percentage( root_size, lv_size) if lv_size < MIN_ROOT_PARTITION_SIZE: set_form_error( self, "lv_size", "Size is too small. Minimum size is %s." % ( MIN_ROOT_PARTITION_SIZE)) if lv_size > root_size: set_form_error( self, "lv_size", "Size is too large. Maximum size is %s." % root_size) cleaned_data['lv_size'] = lv_size return cleaned_data def configure_storage(self, allow_fallback): """Create the LVM configuration.""" # Circular imports. from maasserver.models.filesystem import Filesystem from maasserver.models.filesystemgroup import VolumeGroup root_partition, root_partition_table = self.create_basic_layout() # Add extra partitions if MBR and extra space. partitions = [root_partition] if root_partition_table.table_type == PARTITION_TABLE_TYPE.MBR: available_size = root_partition_table.get_available_size() while available_size > MIN_PARTITION_SIZE: part = root_partition_table.add_partition() partitions.append(part) available_size -= part.size # Create the volume group and logical volume. volume_group = VolumeGroup.objects.create_volume_group( self.get_vg_name(), block_devices=[], partitions=partitions) logical_volume = volume_group.create_logical_volume( self.get_lv_name(), self.get_calculated_lv_size(volume_group)) Filesystem.objects.create( block_device=logical_volume, fstype=FILESYSTEM_TYPE.EXT4, label="root", mount_point="/") return "lvm"