yast/yast-storage-ng

View on GitHub
src/lib/y2storage/fake_device_factory.rb

Summary

Maintainability
A
0 mins
Test Coverage
# Copyright (c) [2015-2017] SUSE LLC
#
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of version 2 of the GNU General Public License as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, contact SUSE LLC.
#
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.

require "yast"
require "y2storage"
require "y2storage/abstract_device_factory"

module Y2Storage
  #
  # Factory class to generate faked devices in a device graph.
  # This is typically used with a YAML file.
  # Use the inherited load_yaml_file() to start the process.
  #
  # rubocop:disable Metrics/ClassLength
  #
  class FakeDeviceFactory < AbstractDeviceFactory
    # Valid toplevel products of this factory
    VALID_TOPLEVEL  = ["dasd", "disk", "md", "lvm_vg"]

    # Valid hierarchy within the products of this factory.
    # This indicates the permitted children types for each parent.
    VALID_HIERARCHY =
      {
        "dasd"       => ["partition_table", "partitions", "file_system", "encryption"],
        "disk"       => ["partition_table", "partitions", "file_system", "encryption"],
        "md"         => ["md_devices", "partition_table", "partitions", "file_system", "encryption"],
        "md_devices" => ["md_device"],
        "md_device"  => [],
        "partitions" => ["partition", "free"],
        "partition"  => ["file_system", "encryption", "btrfs"],
        "encryption" => ["file_system"],
        "lvm_vg"     => ["lvm_lvs", "lvm_pvs"],
        "lvm_lvs"    => ["lvm_lv"],
        "lvm_lv"     => ["file_system", "encryption", "btrfs"],
        "lvm_pvs"    => ["lvm_pv"],
        "lvm_pv"     => [],
        "btrfs"      => ["subvolumes"],
        "subvolumes" => ["subvolume"]
      }

    # Valid parameters for file_system
    FILE_SYSTEM_PARAM = [
      "mount_point", "label", "uuid", "fstab_options", "btrfs", "mount_by", "mkfs_options"
    ]

    # Valid parameters for each product of this factory.
    # Sub-products are not listed here.
    VALID_PARAM =
      {
        "dasd"            => [
          "name", "size", "block_size", "io_size", "min_grain", "align_ofs", "type", "format"
        ].concat(FILE_SYSTEM_PARAM),
        "disk"            => [
          "name", "size", "block_size", "io_size", "min_grain", "align_ofs", "mbr_gap"
        ].concat(FILE_SYSTEM_PARAM),
        "md"              => [
          "name", "md_level", "md_parity", "chunk_size", "md_uuid", "in_etc_mdadm", "metadata"
        ].concat(FILE_SYSTEM_PARAM),
        "md_device"       => ["blk_device"],
        "partition_table" => [],
        "partitions"      => [],
        "partition"       => [
          "size", "start", "align", "name", "type", "id"
        ].concat(FILE_SYSTEM_PARAM),
        "file_system"     => [],
        "encryption"      => ["name", "type", "password"],
        "free"            => ["size", "start"],
        "lvm_vg"          => ["vg_name", "extent_size"],
        "lvm_lv"          => [
          "lv_name", "size", "stripes", "stripe_size"
        ].concat(FILE_SYSTEM_PARAM),
        "lvm_pv"          => ["blk_device"],
        "btrfs"           => ["default_subvolume"],
        "subvolumes"      => [],
        "subvolume"       => ["path", "nocow"]
      }

    # Dependencies between products on the same hierarchy level.
    DEPENDENCIES =
      {
        # file_system depends on encryption because any encryption needs to be
        # created first (and then the file system on the encryption layer).
        #
        # file_system depends on partition_table so a partition table is
        # created before any file_system directly on a disk so an error can be
        # reported if both are specified: It's either a partiton table or a
        # file system, not both.
        #
        # file_system depends on md_devices because the devices must be added to
        # the MD before formatting it.
        "file_system"     => ["encryption", "partition_table", "md_devices"],

        # partition_table depends on "md_devices" because the devices must be added
        # to the MD before creating partitions on it.
        "partition_table" => ["md_devices"]
      }

    class << self
      #
      # Read a YAML file and build a fake device tree from it.
      #
      # This is a singleton method for convenience. It creates a
      # FakeDeviceFactory internally for one-time usage. If you use this more
      # often (for example, in a loop), it is recommended to use create a
      # FakeDeviceFactory and use its load_yaml_file() method repeatedly.
      #
      # @param devicegraph [Devicegraph] where to build the tree
      # @param input_file [String] name of the YAML file
      #
      def load_yaml_file(devicegraph, input_file)
        factory = FakeDeviceFactory.new(devicegraph)
        factory.load_yaml_file(input_file)
      end
    end

    def initialize(devicegraph)
      super(devicegraph)
      @disks = Set.new
      @file_system_data = {}
    end

    protected

    # Return a hash for the valid hierarchy of the products of this factory:
    # Each hash key returns an array (that might be empty) for the child
    # types that are valid below that key.
    #
    # @return [Hash<String, Array<String>>]
    #
    def valid_hierarchy
      VALID_HIERARCHY
    end

    # Return an array for valid toplevel products of this factory.
    #
    # @return [Array<String>] valid toplevel products
    #
    def valid_toplevel
      VALID_TOPLEVEL
    end

    # Return an hash of valid parameters for each product type of this
    # factory. This does not include sub-products, only the parameters that
    # are passed directly to each individual product.
    #
    # @return [Hash<String, Array<String> >]
    #
    def valid_param
      VALID_PARAM
    end

    # Fix up parameters to the create_xy() methods.  In this instance,
    # this is used to convert parameters representing a DiskSize to a
    # DiskSize object that can be used directly.
    #
    # This method is optional. The base class checks with respond_to? if it
    # is implemented before it is called.
    #
    # @param name [String] factory product name
    # @param param [Hash] create_xy() parameters
    #
    # @return [Hash or Scalar] changed parameters
    #
    def fixup_param(name, param)
      log.info("Fixing up #{param} for #{name}")
      ["size", "start", "block_size", "io_size", "min_grain", "align_ofs",
       "mbr_gap", "extent_size", "stripe_size", "chunk_size"].each do |key|
        param[key] = DiskSize.new(param[key]) if param.key?(key)
      end
      param
    end

    # Return a hash describing dependencies from one sub-product (on the same
    # hierarchy level) to another so they can be produced in the correct order.
    #
    # For example, if there is an encryption layer and a file system in a
    # partition, the encryption layer needs to be created first so the file
    # system can be created inside that encryption layer.
    #
    def dependencies
      DEPENDENCIES
    end

    # Factory methods
    #
    # The AbstractDeviceFactory base class will collect all methods starting
    # with "create_" via Ruby introspection (methods()) and use them for
    # creating factory products.
    #

    # Factory method to create a DASD disk.
    #
    # @param _parent [nil] (disks are toplevel)
    # @param args [Hash] disk parameters:
    #   "name"       device name ("/dev/sda" etc.)
    #   "size"       disk size
    #   "block_size" block size
    #   "io_size"    optimal io size
    #   "min_grain"  minimal grain
    #   "align_ofs"  alignment offset
    #   "type"       DASD type ("eckd", "fba")
    #   "format"     DASD format ("ldl", "cdl")
    #
    # @return [String] device name of the new DASD disk ("/dev/sda" etc.)
    def create_dasd(_parent, args)
      dasd_args = add_defaults_for_dasd(args)
      dasd = new_partitionable(Dasd, dasd_args)
      type = fetch(DasdType, dasd_args["type"], "dasd type", dasd_args["name"])
      format = fetch(DasdFormat, dasd_args["format"], "dasd format", dasd_args["name"])
      dasd.type = type unless type.is?(:unknown)
      dasd.format = format unless format.is?(:none)
      dasd.name
    end

    def add_defaults_for_dasd(args)
      dasd_args = args.dup
      dasd_args["name"] ||= "/dev/dasda"
      dasd_args["type"] ||= "unknown"
      dasd_args["format"] ||= "none"
      if dasd_args["type"] == "eckd"
        dasd_args["block_size"] ||= DiskSize.KiB(4)
        dasd_args["min_grain"] ||= DiskSize.KiB(4)
      end
      dasd_args
    end

    # Factory method to create a disk.
    #
    # @param _parent [nil] (disks are toplevel)
    # @param args [Hash] disk parameters:
    #   "name"       device name ("/dev/sda" etc.)
    #   "size"       disk size
    #   "range"      max number of partitions
    #   "block_size" block size
    #   "io_size"    optimal io size
    #   "min_grain"  minimal grain
    #   "align_ofs"  alignment offset
    #   "mbr_gap"    mbr gap (for msdos partition table)
    #
    # @return [String] device name of the new disk ("/dev/sda" etc.)
    def create_disk(_parent, args)
      new_partitionable(Disk, args).name
    end

    # Factory method to create a Software RAID (Md)
    #
    # @param _parent [nil] (Mds are toplevel)
    # @param args [Hash] RAID parameters:
    #   * :name [String] RAID name (e.g., /dev/md0)
    #   * :md_level [String] "raid0", "raid1", etc
    #   * :chunk_size [DiskSize]
    #   * :md_uuid [String]
    #   * :in_etc_mdadm [Boolean]
    #   * :metadata [String]
    #
    # @return [String] name of the new Software RAID (e.g., /dev/md0)
    def create_md(_parent, args)
      name = args["name"] || Md.find_free_numeric_name(devicegraph)
      md = Md.create(devicegraph, name)
      add_md_attributes(md, args)

      save_filesystem_attributes(md, args)
      md.name
    end

    # Sets attributes values to the recently created Software RAID
    #
    # @param md [Y2Stroage::Md]
    # @param args [Hash] RAID parameters:
    def add_md_attributes(md, args)
      add_md_level(md, args["md_level"])
      add_md_parity(md, args["md_parity"])
      add_md_chunk_size(md, args["chunk_size"])
      add_md_uuid(md, args["md_uuid"])
      add_md_in_etc_mdamd(md, args["in_etc_mdadm"])
      add_md_metadata(md, args["metadata"])
    end

    # Sets the MD RAID level (RAID0 by defaul)
    #
    # @param md [Y2Stroage::Md]
    # @param level [String, nil]
    def add_md_level(md, level)
      level = Y2Storage::MdLevel.find(level) unless level.nil?
      level ||= Y2Storage::MdLevel::RAID0

      md.md_level = level
    end

    # Sets the MD RAID parity algorithm
    #
    # @param md [Y2Stroage::Md]
    # @param parity [String, nil]
    def add_md_parity(md, parity)
      return if parity.nil?

      parity = Y2Storage::MdParity.find(parity)
      md.md_parity = parity
    end

    # Sets the MD RAID chunk size
    #
    # @param md [Y2Stroage::Md]
    # @param chunk_size [Y2Storage::DiskSize, nil]
    def add_md_chunk_size(md, chunk_size)
      return if chunk_size.nil?

      md.chunk_size = chunk_size
    end

    # Sets the MD RAID uuid
    #
    # @note Public Md API (wrapper) does not allow to set this value because
    #   this value should only be probed.
    #
    # @param md [Y2Stroage::Md]
    # @param uuid [String, nil]
    def add_md_uuid(md, uuid)
      return if uuid.nil?

      md.to_storage_value.uuid = uuid
    end

    # Sets the MD RAID "in_etc_mdadm" value
    #
    # @param md [Y2Stroage::Md]
    # @param in_etc_mdadm [Boolean, nil]
    def add_md_in_etc_mdamd(md, in_etc_mdadm)
      return if in_etc_mdadm.nil?

      md.to_storage_value.in_etc_mdadm = in_etc_mdadm
    end

    # Sets the MD RAID metadata
    #
    # @note Public Md API (wrapper) does not allow to set this value because
    #   this value should only be probed.
    #
    # @param md [Y2Stroage::Md]
    # @param metadata [String, nil]
    def add_md_metadata(md, metadata)
      return if metadata.nil?

      md.to_storage_value.metadata = metadata
    end

    # Factory method to add a block device to a Software RAID
    #
    # @param parent [Md] Software RAID
    # @param args [Hash] block device parameters:
    #   * :blk_device [String] block device used by the Software RAID
    def create_md_device(parent, args)
      md = Md.find_by_name(devicegraph, parent)
      md_device = BlkDevice.find_by_name(devicegraph, args["blk_device"])

      md.add_device(md_device)
    end

    # Method to create a partitionable.
    # @see #create_dasd
    # @see #create_disk
    #
    # @param partitionable_class [Dasd, Disk]
    # @param args [Hash<String, String>]
    #
    # @return [Y2Storage::Partitionable] device
    #
    # FIXME: this method is too complex. It offends three different cops
    # related to complexity.
    # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize
    def new_partitionable(partitionable_class, args)
      @volumes = Set.new
      @free_blob      = nil
      @free_regions   = []
      @mbr_gap        = nil

      log.info("#{__method__}( #{args} )")
      name = args["name"] || "/dev/sda"
      size = args["size"]
      raise ArgumentError, "\"size\" missing for disk #{name}" if size.nil?
      raise ArgumentError, "Duplicate disk name #{name}" if @disks.include?(name)

      @disks << name
      block_size = args["block_size"] if args["block_size"]
      @mbr_gap = args["mbr_gap"] if args["mbr_gap"]
      if block_size && block_size.size > 0
        r = Region.create(0, size.to_i / block_size.to_i, block_size)
        disk = partitionable_class.create(@devicegraph, name, r)
      else
        disk = partitionable_class.create(@devicegraph, name)
        disk.size = size
      end
      set_topology_attributes!(disk, args)
      # range (number of partitions that the kernel can handle) used to be
      # 16 for scsi and 64 for ide. Now it's 256 for most of them.
      disk.range = args["range"] || 256

      save_filesystem_attributes(disk, args)
      disk
    end
    # rubocop:enable all

    # Saves all attributes related to the filesystem when the device is directly formatted
    #
    # @param device [BlkDevice]
    # @param args [Hash] device and filesystem parameters
    def save_filesystem_attributes(device, args)
      return unless args.keys.any? { |x| FILE_SYSTEM_PARAM.include?(x) }

      # No use trying to check for disk.has_partition_table here and throwing
      # an error in that case: The AbstractDeviceFactory base class will
      # already have caused a Storage::WrongNumberOfChildren exception and
      # convert that into a better readable HierarchyError. When we get here,
      # that error already happened.
      log.info("Creating filesystem directly on device #{args}")
      file_system_data_picker(device.name, args)
    end

    # Modifies topology settings of the disk according to factory arguments
    #
    # @param disk [Disk]
    # @param args [Hash] disk parameters. See {#create_disk}
    def set_topology_attributes!(disk, args)
      io_size = args["io_size"]
      min_grain = args["min_grain"]
      align_ofs = args["align_ofs"]
      disk.topology.optimal_io_size = io_size.size if io_size && io_size.size > 0
      disk.topology.alignment_offset = align_ofs.size if align_ofs
      disk.topology.minimal_grain = min_grain.size if min_grain && min_grain.size > 0
    end

    # Factory method to create a partition table.
    #
    # @param parent [String] disk name ("/dev/sda" etc.)
    # @param args [String] disk label type: "gpt", "ms-dos"
    #
    # @return [String] device name of the disk ("/dev/sda" etc.)
    #
    def create_partition_table(parent, args)
      log.info("#{__method__}( #{parent}, #{args} )")
      disk_name = parent
      ptable_type = str_to_ptable_type(args)
      disk = Partitionable.find_by_name(@devicegraph, disk_name)
      ptable = disk.create_partition_table(ptable_type)
      ptable.minimal_mbr_gap = @mbr_gap if ptable.respond_to?(:minimal_mbr_gap=) && @mbr_gap
      disk_name
    end

    # Partition table type represented by a string
    #
    # @param string [String] usually from a YAML file
    # @return [PartitionTables::Type]
    def str_to_ptable_type(string)
      # Allow different spelling
      string = "msdos" if string.casecmp("ms-dos").zero?
      fetch(PartitionTables::Type, string, "partition table type", "disk_name")
    end

    # Factory method to create a partition.
    #
    # Some of the parameters ("mount_point", "label"...) really belong to the
    # file system which is a separate factory product, but it is more natural
    # to specify this for the partition, so those data are kept
    # in @file_system_data to be picked up in create_file_system when needed.
    #
    # @param parent [String] disk name ("/dev/sda" etc.)
    #
    # @param args [Hash] partition table parameters:
    #   "size"  partition size (unlimited if missing)
    #   "start" partition start (optional)
    #   "align" partition align policy (optional)
    #   "name"  device name ("/dev/sdb3" etc.)
    #   "type"  "primary", "extended", "logical"
    #   "id"
    #   "mount_point"   mount point for the associated file system
    #   "label"         file system label
    #   "uuid"          file system UUID
    #   "fstab_options" /etc/fstab options for the file system
    #
    # @return [String] device name of the disk ("/dev/sda" etc.)
    #
    # FIXME: this method is too complex. It offends four different cops
    # related to complexity.
    # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
    # rubocop:disable  Metrics/MethodLength, Metrics/AbcSize
    def create_partition(parent, args)
      log.info("#{__method__}( #{parent}, #{args} )")
      disk_name = parent
      size      = args["size"] || DiskSize.unlimited
      start     = args["start"]
      part_name = args["name"]
      type      = args["type"] || "primary"
      id        = args["id"] || "linux"
      align     = args["align"]

      raise ArgumentError, "\"name\" missing for partition #{args} on #{disk_name}" unless part_name
      raise ArgumentError, "Duplicate partition #{part_name}" if @volumes.include?(part_name)

      @volumes << part_name
      file_system_data_picker(part_name, args)

      id = id.to_i(16) if id.is_a?(::String) && id.start_with?("0x")
      id   = fetch(PartitionId,   id,   "partition ID",   part_name) unless id.is_a?(Integer)
      type = fetch(PartitionType, type, "partition type", part_name)
      align = fetch(AlignPolicy,  align, "align policy",  part_name) if align

      disk = Partitionable.find_by_name(devicegraph, disk_name)
      ptable = disk.partition_table
      slots = ptable.unused_partition_slots

      # partitions are created in order, so first suitable slot should be fine
      # note: skip areas we marked as empty
      slot = slots.find { |s| s.possible?(type) && !@free_regions.member?(s.region.start) }
      raise ArgumentError, "No suitable slot for partition #{part_name}" if !slot

      region = slot.region

      # region = slots.first.region
      # if no start has been specified, take free region into account
      if !start && @free_blob
        @free_regions.push(region.start)
        start_block = region.start + (@free_blob.to_i / region.block_size.to_i)
      end
      @free_blob = nil

      # if start has been specified, use it
      start_block = start.to_i / region.block_size.to_i if start

      # adjust start block, if necessary
      if start_block
        if start_block > region.start && start_block <= region.end
          region.adjust_length(region.start - start_block)
        end
        region.start = start_block
      end

      # if no size has been specified, use whole region
      region.length = size.to_i / region.block_size.to_i if !size.unlimited?

      # align partition if specified
      region = disk.topology.align(region, align) if align

      raise Error, "Trying to create a zero-size partition" if region.empty?

      partition = ptable.create_partition(part_name, region, type)
      partition.id = id

      part_name
    end
    # rubocop:enable all

    # Factory method to create a file system.
    #
    # This fetches some parameters from @file_system_data:
    # "mount_point", "label", "uuid", "encryption"
    #
    # @param parent [String] parent (partition or disk) device name ("/dev/sdc2")
    # @param args   [String] file system type ("xfs", "btrfs", ...)
    #
    # @return [String] partition device name ("/dev/sdc2" etc.)
    #
    def create_file_system(parent, args)
      log.info("#{__method__}( #{parent}, #{args} )")
      fs_type = fetch(Filesystems::Type, args, "file system type", parent)

      # Fetch file system related parameters stored by create_partition()
      fs_param = @file_system_data[parent] || {}
      encryption = fs_param["encryption"]

      if !encryption.nil?
        log.info("file system is on encrypted device #{encryption}")
        parent = encryption
      end
      blk_device = BlkDevice.find_by_name(@devicegraph, parent)
      file_system = blk_device.create_blk_filesystem(fs_type)
      assign_file_system_params(file_system, fs_param)
      parent
    end

    def assign_file_system_params(file_system, fs_param)
      ["label", "uuid", "mkfs_options"].each do |param|
        value = fs_param[param]
        file_system.public_send(:"#{param}=", value) if value
      end

      add_mount_point(file_system, fs_param)
    end

    def add_mount_point(filesystem, fs_param)
      mount_path = fs_param["mount_point"]
      return if mount_path.nil? || mount_path.empty?

      mount_point = filesystem.create_mount_point(mount_path)

      mount_options = fs_param["fstab_options"]
      mount_point.mount_options = mount_options unless mount_options.nil?

      if fs_param["mount_by"]
        mount_point.mount_by = fetch(
          Filesystems::MountByType, fs_param["mount_by"], "mount by name schema", mount_point
        )
      end

      nil
    end

    # Picks some parameters that are really file system related from args
    # and places them in @file_system_data to be picked up later by
    # create_file_system.
    #
    # @param [String] name of blk_device file system is on
    #
    # @param args [Hash] hash with data from yaml file
    #
    def file_system_data_picker(name, args)
      fs_param = FILE_SYSTEM_PARAM << "encryption"
      @file_system_data[name] = args.select { |k, _v| fs_param.include?(k) }
    end

    # Factory method to create a slot of free space.
    #
    # We just remember the value and take it into account when we create the next partition.
    #
    # @param parent [String] disk name ("/dev/sda" etc.)
    #
    # @param args [Hash] free space parameters:
    #   "size"  free space size
    #   "start" (ignored)
    #
    # @return [String] device name of the disk ("/dev/sda" etc.)
    #
    def create_free(parent, args)
      log.info("#{__method__}( #{parent}, #{args} )")
      disk_name = parent
      size = args["size"]
      @free_blob = size if size && size.to_i > 0
      disk_name
    end

    ENCRYPTION_METHOD_ALIASES = {
      "luks" => "luks1"
    }.freeze
    private_constant :ENCRYPTION_METHOD_ALIASES
    # Factory method to create an encryption layer.
    #
    # @param parent [String] parent device name ("/dev/sda1" etc.)
    #
    # @param args [Hash] encryption layer parameters:
    #   "name"     name encryption layer ("/dev/mapper/cr_Something")
    #   "type"     encryption type; default: "luks"
    #   "password" encryption password (optional)
    #
    # @return [Object] new encryption object
    #
    def create_encryption(parent, args)
      log.info("#{__method__}( #{parent}, #{args} )")
      name = encryption_name(args["name"], parent)
      password = args["password"]
      type_name = args["type"] || "luks"
      type_name = ENCRYPTION_METHOD_ALIASES[type_name] if ENCRYPTION_METHOD_ALIASES.key?(type_name)
      # We only support creating LUKS so far
      method = EncryptionMethod.find(type_name)
      raise ArgumentError, "Unsupported encryption type #{type_name}" unless method

      blk_parent = BlkDevice.find_by_name(@devicegraph, parent)
      encryption = blk_parent.encrypt(dm_name: name, password: password, method: method)
      if @file_system_data.key?(parent)
        # Notify create_file_system that this partition is encrypted
        @file_system_data[parent]["encryption"] = encryption.name
      end
      encryption
    end

    def encryption_name(name, parent)
      return nil if name.nil? || name.empty?

      if name.include?("/")
        processed_encryption_name(name, parent)
      else
        name
      end
    end

    # DeviceMapper name for a given DeviceMapper full path
    def processed_encryption_name(name, parent)
      valid_name = false
      result = nil

      if name.start_with?("/dev/mapper/")
        result = name.split("/").last
        valid_name = !result.nil? && !result.empty?
      end

      if !valid_name
        raise ArgumentError, "Unexpected \"name\" value for encryption on #{parent}: #{name}"
      end

      result
    end

    # Factory method to create a lvm volume group.
    #
    # @param _parent [nil] (volume groups are toplevel)
    # @param args [Hash] volume group parameters:
    #   "vg_name"     volume group name
    #   "extent_size" extent size
    #
    # @return [Object] new volume group object
    #
    def create_lvm_vg(_parent, args)
      log.info("#{__method__}( #{args} )")
      @volumes = Set.new # contains both partitions and logical volumes

      vg_name = args["vg_name"]
      lvm_vg = LvmVg.create(@devicegraph, vg_name)

      extent_size = args["extent_size"] || DiskSize.zero
      lvm_vg.extent_size = extent_size if extent_size.to_i > 0

      lvm_vg
    end

    # Factory method to create a lvm logical volume.
    #
    # Some of the parameters ("mount_point", "label"...) really belong to the
    # file system which is a separate factory product, but it is more natural
    # to specify this for the logical volume, so those data are kept
    # in @file_system_data to be picked up in create_file_system when needed.
    #
    # @param parent [Object] volume group object
    #
    # @param args [Hash] lvm logical volume parameters:
    #   "lv_name"     logical volume name
    #   "size"        partition size
    #   "stripes"     number of stripes
    #   "stripe_size" stripe size
    #   "mount_point"   mount point for the associated file system
    #   "label"         file system label
    #   "uuid"          file system UUID
    #   "fstab_options" /etc/fstab options for the file system
    #
    # @return [String] device name of new logical volume
    #
    def create_lvm_lv(parent, args)
      log.info("#{__method__}( #{parent}, #{args} )")

      lv_name = args["lv_name"]
      raise ArgumentError, "\"lv_name\" missing for lvm_lv #{args} on #{vg_name}" unless lv_name
      raise ArgumentError, "Duplicate lvm_lv #{lv_name}" if @volumes.include?(lv_name)

      @volumes << lv_name

      size = args["size"] || DiskSize.zero
      raise ArgumentError, "\"size\" missing for lvm_lv #{lv_name}" unless args.key?("size")

      lvm_lv = parent.create_lvm_lv(lv_name, size)
      create_lvm_lv_stripe_parameters(lvm_lv, args)

      file_system_data_picker(lvm_lv.name, args)

      lvm_lv.name
    end

    # Helper class for create_lvm_lv handling the stripes related parameters.
    #
    def create_lvm_lv_stripe_parameters(lvm_lv, args)
      stripes = args["stripes"] || 0
      lvm_lv.stripes = stripes if stripes > 0

      stripe_size = args["stripe_size"] || DiskSize.zero
      lvm_lv.stripe_size = stripe_size if stripe_size.to_i > 0
    end

    # Factory method to create a lvm physical volume.
    #
    # @param parent [Object] volume group object
    #
    # @param args [Hash] lvm physical volume parameters:
    #   "blk_device" block device used by physical volume
    #
    # @return [Object] new physical volume object
    #
    def create_lvm_pv(parent, args)
      log.info("#{__method__}( #{parent}, #{args} )")

      blk_device_name = args["blk_device"]
      blk_device = BlkDevice.find_by_name(devicegraph, blk_device_name)

      parent.add_lvm_pv(blk_device)
    end

    # Factory method for a btrfs pseudo object to create subvolumes.
    #
    # @param parent [String] Name of the partition or LVM LV
    #
    # @param args [Hash] btrfs parameters:
    #   "default_subvolume"
    #
    # @return [String] Name of the partition or LVM LV
    #
    def create_btrfs(parent, args)
      log.info("#{__method__}( #{parent}, #{args} )")
      default_subvolume = args["default_subvolume"]
      if default_subvolume && !default_subvolume.empty?
        blk_device = BlkDevice.find_by_name(devicegraph, parent)
        filesystem = blk_device.filesystem
        raise HierarchyError, "No btrfs on #{parent}" if !filesystem || !filesystem.type.is?(:btrfs)

        toplevel = filesystem.top_level_btrfs_subvolume
        subvolume = toplevel.create_btrfs_subvolume(default_subvolume)
        subvolume.set_default_btrfs_subvolume
      end
      parent
    end

    # Factory method for a btrfs subvolume
    #
    # @param parent [String] Name of the partition or LVM LV
    #
    # @param args [Hash] subvolume parameters:
    #   "path"  subvolume path without leading "@" or "/"
    #   "nocow" "no copy on write" attribute (default: false)
    #
    # @return [String] Name of the partition or LVM LV
    #
    def create_subvolume(parent, args)
      log.info("#{__method__}( #{parent}, #{args} )")
      path  = args["path"]
      nocow = args.fetch("nocow", false)
      raise ArgumentError, "No path for subvolume" unless path

      blk_device = BlkDevice.find_by_name(@devicegraph, parent)
      blk_device.filesystem.create_btrfs_subvolume(path, nocow)
    end

    private

    # Fetch an enum value
    # @raise [ArgumentError] if such value is not defined
    #
    # @param klass  [Class] class used to represent the enum
    # @param name   [String] name of the enum value
    # @param type   [String] type (description) of 'key'
    # @param object [String] name of the object that was being processed
    #
    def fetch(klass, name, type, object)
      value = klass.find(name)
      if !value
        available = klass.all.map(&:to_s)
        raise ArgumentError, "Invalid #{type} \"#{name}\" for #{object} - use one of #{available}"
      end
      value
    end
  end
end