class LinuxAdmin::Disk

Constants

PARTED_FIELDS

Attributes

model[RW]
path[RW]

Public Class Methods

local() click to toggle source

Collect local disk information via the lsblk command. Only disks with a size greater than zero are returned.

# File lib/linux_admin/disk.rb, line 14
def self.local
  result = Common.run!(Common.cmd("lsblk"), :params => {:b => nil, :d => nil, :n => nil, :p => nil, :o => "NAME,SIZE,TYPE,MODEL"})
  result.output.split("\n").collect do |string|
    path, size, type, *model = string.split
    if type.casecmp?('disk') && size.to_i > 0
      self.new(:path => path, :size => size.to_i, :model => model.join(' '))
    end
  end.compact
end
new(args = {}) click to toggle source
# File lib/linux_admin/disk.rb, line 24
def initialize(args = {})
  @path  = args[:path]
  @model = args[:model] || "unknown"
  @size  = args[:size]
end

Public Instance Methods

clear!() click to toggle source
# File lib/linux_admin/disk.rb, line 101
def clear!
  @partitions = []

  # clear partition table
  Common.run!(Common.cmd(:dd),
      :params => { 'if=' => '/dev/zero', 'of=' => @path,
                   'bs=' => 512, 'count=' => 1})

  self
end
create_partition(partition_type, *args) click to toggle source
# File lib/linux_admin/disk.rb, line 61
def create_partition(partition_type, *args)
  create_partition_table unless has_partition_table?

  start = finish = size = nil
  case args.length
  when 1 then
    start  = partitions.empty? ? 0 : partitions.last.end_sector
    size   = args.first
    finish = start + size

  when 2 then
    start  = args[0]
    finish = args[1]

  else
    raise ArgumentError, "must specify start/finish or size"
  end

  id = partitions.empty? ? 1 : (partitions.last.id + 1)
  options = parted_options_array('mkpart', '-a', 'opt', partition_type, start, finish)
  Common.run!(Common.cmd(:parted), :params => {nil => options})

  partition = Partition.new(:disk           => self,
                            :id             => id,
                            :start_sector   => start,
                            :end_sector     => finish,
                            :size           => size,
                            :partition_type => partition_type)
  partitions << partition
  partition
end
create_partition_table(type = "msdos") click to toggle source
# File lib/linux_admin/disk.rb, line 51
def create_partition_table(type = "msdos")
  Common.run!(Common.cmd(:parted), :params => {nil => parted_options_array("mklabel", type)})
end
create_partitions(partition_type, *args) click to toggle source
# File lib/linux_admin/disk.rb, line 93
def create_partitions(partition_type, *args)
  check_if_partitions_overlap(args)

  args.each { |arg|
    self.create_partition(partition_type, arg[:start], arg[:end])
  }
end
has_partition_table?() click to toggle source
# File lib/linux_admin/disk.rb, line 55
def has_partition_table?
  result = Common.run(Common.cmd(:parted), :params => {nil => parted_options_array("print")})

  result_indicates_partition_table?(result)
end
partition_path(id) click to toggle source
# File lib/linux_admin/disk.rb, line 112
def partition_path(id)
  case model
  when "nvme"
    "#{path}p#{id}"
  else
    "#{path}#{id}"
  end
end
partitions() click to toggle source
# File lib/linux_admin/disk.rb, line 44
def partitions
  @partitions ||=
    parted_output.collect { |disk|
      partition_from_parted(disk)
    }
end
size() click to toggle source
# File lib/linux_admin/disk.rb, line 30
def size
  @size ||= begin
    size = nil
    out = Common.run!(Common.cmd(:fdisk), :params => {"-l" => nil}).output
    out.each_line do |l|
      /Disk #{path}: .*B, (\d+) bytes/.match(l) do |m|
        size = m[1].to_i
        break
      end
    end
    size
  end
end

Private Instance Methods

check_if_partitions_overlap(partitions) click to toggle source
# File lib/linux_admin/disk.rb, line 147
def check_if_partitions_overlap(partitions)
  ranges =
    partitions.collect do |partition|
      start  = partition[:start]
      finish = partition[:end]
      start.delete('%')
      finish.delete('%')
      start.to_f..finish.to_f
    end

  if overlapping_ranges?(ranges)
    raise ArgumentError, "overlapping partitions"
  end
end
overlapping_ranges?(ranges) click to toggle source
# File lib/linux_admin/disk.rb, line 134
def overlapping_ranges?(ranges)
  ranges.find do |range1|
    ranges.any? do |range2|
      range1 != range2 &&
        ranges_overlap?(range1, range2)
    end
  end
end
parse_model(parted_line) click to toggle source
# File lib/linux_admin/disk.rb, line 179
def parse_model(parted_line)
  matches = parted_line.match(/^Model:.*\((?<model>\w+)\)$/)
  @model = matches[:model] if matches
end
parted_default_options() click to toggle source
# File lib/linux_admin/disk.rb, line 209
def parted_default_options
  @parted_default_options ||= ['--script', path].freeze
end
parted_options_array(*args) click to toggle source
# File lib/linux_admin/disk.rb, line 204
def parted_options_array(*args)
  args = args.first if args.first.kind_of?(Array)
  parted_default_options + args
end
parted_output() click to toggle source
# File lib/linux_admin/disk.rb, line 162
def parted_output
  # TODO: Should this really catch non-zero RC, set output to the default "" and silently return [] ?
  #   If so, should other calls to parted also do the same?
  # requires sudo
  out = Common.run(Common.cmd(:parted),
            :params => { nil => parted_options_array('print') }).output
  split = []
  out.each_line do |l|
    if l =~ /^ [0-9].*/
      split << l.split
    elsif l =~ /^Model:.*/
      parse_model(l)
    end
  end
  split
end
partition_from_parted(output_disk) click to toggle source
# File lib/linux_admin/disk.rb, line 184
def partition_from_parted(output_disk)
  args = {:disk => self}
  PARTED_FIELDS.each_index do |i|
    val = output_disk[i]
    case PARTED_FIELDS[i]
    when :start_sector, :end_sector, :size
      if val =~ /([0-9\.]*)([kKMG])B/
        val = str_to_bytes($1, $2)
      end

    when :id
      val = val.to_i

    end
    args[PARTED_FIELDS[i]] = val
  end

  Partition.new(args)
end
ranges_overlap?(range1, range2) click to toggle source
# File lib/linux_admin/disk.rb, line 143
def ranges_overlap?(range1, range2) # copied from activesupport Range#overlaps?
  range1.cover?(range2.first) || range2.cover?(range1.first)
end
result_indicates_partition_table?(result) click to toggle source
# File lib/linux_admin/disk.rb, line 213
def result_indicates_partition_table?(result)
  # parted exits with 1 but writes this oddly spelled error to stdout.
  missing = (result.exit_status == 1 && result.output.include?("unrecognised disk label"))
  !missing
end
str_to_bytes(val, unit) click to toggle source
# File lib/linux_admin/disk.rb, line 123
def str_to_bytes(val, unit)
  case unit
  when 'K', 'k' then
    val.to_f * 1_024 # 1.kilobytes
  when 'M' then
    val.to_f * 1_048_576 # 1.megabyte
  when 'G' then
    val.to_f * 1_073_741_824 # 1.gigabytes
  end
end