コード例 #1
0
ファイル: storage_layouts.py プロジェクト: ocni-dtu/maas
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
コード例 #2
0
ファイル: storage_layouts.py プロジェクト: ocni-dtu/maas
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()
コード例 #3
0
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()
コード例 #4
0
ファイル: storage_layouts.py プロジェクト: ocni-dtu/maas
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
コード例 #5
0
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"