Class: RightScale::Platform::VolumeManager

Inherits:
Object
  • Object
show all
Defined in:
lib/right_agent/platform/linux.rb,
lib/right_agent/platform/darwin.rb,
lib/right_agent/platform/windows.rb

Overview

Provides utilities for managing volumes (disks).

Defined Under Namespace

Classes: ParserError, VolumeError

Instance Method Summary collapse

Constructor Details

#initializeVolumeManager

Returns a new instance of VolumeManager.



208
209
210
# File 'lib/right_agent/platform/linux.rb', line 208

def initialize

end

Instance Method Details

#assign_device(volume_device_or_index, device) ⇒ Object

Assigns the given device name to the volume given by index and clears the readonly attribute, if necessary. The device must not currently be in use.

Parameters

volume_device_or_index(int)

old device or zero-based volume index (from volumes list, etc.) to select for assignment.

device(String)

device specified for the volume to create

Return

always true

Raise

ArgumentError

on invalid parameters

VolumeError

on failure to assign device name

ParserError

on failure to parse volume list

Raises:

  • (ArgumentError)


543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
# File 'lib/right_agent/platform/windows.rb', line 543

def assign_device(volume_device_or_index, device)
  volume_selector_match = volume_device_or_index.to_s.match(/^([D-Zd-z]|\d+):?$/)
  raise ArgumentError.new("Invalid volume_device_or_index = #{volume_device_or_index}") unless volume_selector_match
  volume_selector = volume_selector_match[1]
  raise ArgumentError.new("Invalid device = #{device}") unless is_attachable_volume_path?(device)
  new_letter = device[0,1]
  script = <<EOF
rescan
list volume
select volume #{volume_selector}
attribute volume clear readonly noerr
assign letter=#{new_letter}
EOF
  exit_code, output_text = run_script(script)
  raise VolumeError.new("Failed to assign device \"#{device}\" for volume \"#{volume_device_or_index}\": exit code = #{exit_code}\n#{script}\n#{output_text}") if exit_code != 0
  true
end

#blocking_popen(command) ⇒ Object

Runs the specified command synchronously using IO.popen

Parameters

command(String)

system command to be executed

Return

result(Array)

tuple of [exit_code, output_text]



332
333
334
335
336
337
338
# File 'lib/right_agent/platform/linux.rb', line 332

def blocking_popen(command)
  output_text = ""
  IO.popen(command) do |io|
    output_text = io.read
  end
  return $?.exitstatus, output_text
end

#disks(conditions = nil) ⇒ Object

Gets a list of physical or virtual disks in the form:

[{:index, :status, :total_size, :free_size, :dynamic, :gpt}*]

where

:index >= 0
:status = 'Online' | 'Offline'
:total_size = bytes used by partitions
:free_size = bytes not used by partitions
:dynamic = true | false
:gpt = true | false

GPT = GUID partition table

Parameters

conditions{Hash)

hash of conditions to match or nil (default)

Return

volumes(Array)

array of hashes detailing visible volumes.

Raise

VolumeError

on failure to list disks

ParserError

on failure to parse disks from output

Raises:



363
364
365
366
367
368
369
370
371
# File 'lib/right_agent/platform/windows.rb', line 363

def disks(conditions = nil)
  script = <<EOF
rescan
list disk
EOF
  exit_code, output_text = run_script(script)
  raise VolumeError.new("Failed to list disks: exit code = #{exit_code}\n#{script}\n#{output_text}") if exit_code != 0
  return parse_disks(output_text, conditions)
end

#format_disk(disk_index, device) ⇒ Object

Formats a disk given by disk index and the device (e.g. “D:”) for the volume on the primary NTFS partition which will be created.

Parameters

disk_index(int): zero-based disk index (from disks list, etc.)

device(String)

device specified for the volume to create

Return

always true

Raise

ArgumentError

on invalid parameters

VolumeError

on failure to format

Raises:

  • (ArgumentError)


459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
# File 'lib/right_agent/platform/windows.rb', line 459

def format_disk(disk_index, device)
  # note that creating the primary partition automatically creates and
  # selects a new volume, which can be assigned a letter before the
  # partition has actually been formatted.
  raise ArgumentError.new("Invalid index = #{disk_index}") unless disk_index >= 0
  raise ArgumentError.new("Invalid device = #{device}") unless is_attachable_volume_path?(device)
  letter = device[0,1]
  online_command = if @os_info.major < 6; "online noerr"; else; "online disk noerr"; end
  clear_readonly_command = if @os_info.major < 6; ""; else; "attribute disk clear readonly noerr"; end

  # note that Windows 2003 server version of diskpart doesn't support
  # format so that has to be done separately.
  format_command = if @os_info.major < 6; ""; else; "format FS=NTFS quick"; end
  script = <<EOF
rescan
list disk
select disk #{disk_index}
#{clear_readonly_command}
#{online_command}
clean
create partition primary
assign letter=#{letter}
#{format_command}
EOF
  exit_code, output_text = run_script(script)
  raise VolumeError.new("Failed to format disk #{disk_index} for device #{device}: exit code = #{exit_code}\n#{script}\n#{output_text}") if exit_code != 0

  # must format using command shell's FORMAT command before 2008 server.
  if @os_info.major < 6
    command = "echo Y | format #{letter}: /Q /V: /FS:NTFS"
    output_text = `#{command}`
    exit_code = $?.exitstatus
    raise VolumeError.new("Failed to format disk #{disk_index} for device #{device}: exit code = #{exit_code}\n#{output_text}") if exit_code != 0
  end
  true
end

#is_attachable_volume_path?(path) ⇒ Boolean

Determines if the given path is valid for a Windows volume attachemnt (excluding the reserved A: B: C: drives).

Return

result(Boolean)

true if path is a valid volume root

Returns:

  • (Boolean)


337
338
339
# File 'lib/right_agent/platform/windows.rb', line 337

def is_attachable_volume_path?(path)
   return nil != (path =~ /^[D-Zd-z]:[\/\\]?$/)
end

#mount_volume(volume, mountpoint) ⇒ Object

Mounts a volume (returned by VolumeManager.volumes) to the mountpoint specified.

Parameters

volume(Hash)

the volume hash returned by VolumeManager.volumes

mountpoint(String)

the exact path where the device will be mounted ex: /mnt

Return

always true

Raise

ArgumentError

on invalid parameters

VolumeError

on a failure to mount the device

Raises:

  • (ArgumentError)


299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/right_agent/platform/linux.rb', line 299

def mount_volume(volume, mountpoint)
  raise ArgumentError.new("Invalid volume = #{volume.inspect}") unless volume.is_a?(Hash) && volume[:device]
  exit_code, mount_list_output = blocking_popen('mount')
  raise VolumeError.new("Failed interrogation of current mounts; Exit Status: #{exit_code}\nError: #{mount_list_output}") unless exit_code == 0

  device_match = /^#{volume[:device]} on (.+?)\s/.match(mount_list_output)
  mountpoint_from_device_match = device_match ? device_match[1] : mountpoint
  unless (mountpoint_from_device_match && mountpoint_from_device_match == mountpoint)
    raise VolumeError.new("Attempted to mount volume \"#{volume[:device]}\" at \"#{mountpoint}\" but it was already mounted at #{mountpoint_from_device_match}")
  end

  mountpoint_match = /^(.+?) on #{mountpoint}/.match(mount_list_output)
  device_from_mountpoint_match = mountpoint_match ? mountpoint_match[1] : volume[:device]
  unless (device_from_mountpoint_match && device_from_mountpoint_match == volume[:device])
    raise VolumeError.new("Attempted to mount volume \"#{volume[:device]}\" at \"#{mountpoint}\" but \"#{device_from_mountpoint_match}\" was already mounted there.")
  end

  # The volume is already mounted at the correct mountpoint
  return true if /^#{volume[:device]} on #{mountpoint}/.match(mount_list_output)

  # TODO: Maybe validate that the mountpoint is valid *nix path?
  exit_code, mount_output = blocking_popen("mount -t #{volume[:filesystem].strip} #{volume[:device]} #{mountpoint}")
  raise VolumeError.new("Failed to mount volume to \"#{mountpoint}\" with device \"#{volume[:device]}\"; Exit Status: #{exit_code}\nError: #{mount_output}") unless exit_code == 0
  return true
end

#online_disk(disk_index) ⇒ Object

Brings the disk given by index online and clears the readonly attribute, if necessary. The latter is required for some kinds of disks to online successfully and SAN volumes may be readonly when initially attached. As this change may bring additional volumes online the updated volumes list is returned.

Parameters

disk_index(int)

zero-based disk index

Return

always true

Raise

ArgumentError

on invalid parameters

VolumeError

on failure to online disk

ParserError

on failure to parse volume list

Raises:

  • (ArgumentError)


512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
# File 'lib/right_agent/platform/windows.rb', line 512

def online_disk(disk_index)
  raise ArgumentError.new("Invalid disk_index = #{disk_index}") unless disk_index >= 0
  clear_readonly_command = if @os_info.major < 6; ""; else; "attribute disk clear readonly noerr"; end
  online_command = if @os_info.major < 6; "online"; else; "online disk noerr"; end
  script = <<EOF
rescan
list disk
select disk #{disk_index}
#{clear_readonly_command}
#{online_command}
EOF
  exit_code, output_text = run_script(script)
  raise VolumeError.new("Failed to online disk #{disk_index}: exit code = #{exit_code}\n#{script}\n#{output_text}") if exit_code != 0
  true
end

#partitions(disk_index, conditions = nil) ⇒ Object

Gets a list of partitions for the disk given by index in the form:

{:index, :type, :size, :offset}

where

:index >= 0
:type = 'OEM' | 'Primary' | <undocumented>
:size = size in bytes used by partition on disk
:offset = offset of partition in bytes from head of disk

Parameters

disk_index(int)

disk index to query

conditions{Hash)

hash of conditions to match or nil (default)

Return

result(Array)

list of partitions or empty

Raise

VolumeError

on failure to list partitions

ParserError

on failure to parse partitions from output

Raises:



435
436
437
438
439
440
441
442
443
444
# File 'lib/right_agent/platform/windows.rb', line 435

def partitions(disk_index, conditions = nil)
   script = <<EOF
rescan
select disk #{disk_index}
list partition
EOF
  exit_code, output_text = run_script(script)
  raise VolumeError.new("Failed to list partitions exit code = #{exit_code}\n#{script}\n#{output_text}") if exit_code != 0
  return parse_partitions(output_text, conditions)
end

#volumes(conditions = nil) ⇒ Object

Gets a list of currently visible volumes in the form:

[{:index, :device, :label, :filesystem, :type, :total_size, :status, :info}*]

where

:index >= 0
:device = "[A-Z]:"
:label = up to 11 characters
:filesystem = nil | 'NTFS' | <undocumented>
:type = 'NTFS' | <undocumented>
:total_size = size in bytes
:status = 'Healthy' | <undocumented>
:info = 'System' | empty | <undocumented>

note that a strange aspect of diskpart is that it won’t correlate disks to volumes in any list even though partition lists are always in the context of a selected disk.

volume order can change as volumes are created/destroyed between diskpart sessions so volume 0 can represent C: in one session and then be represented as volume 1 in the next call to diskpart.

volume labels are truncated to 11 characters by diskpart even though NTFS allows up to 32 characters.

Parameters

conditions{Hash)

hash of conditions to match or nil (default)

Return

volumes(Array)

array of hashes detailing visible volumes.

Raise

VolumeError

on failure to list volumes

ParserError

on failure to parse volumes from output

Raises:



224
225
226
227
228
# File 'lib/right_agent/platform/linux.rb', line 224

def volumes(conditions = nil)
  exit_code, blkid_resp = blocking_popen('blkid')
  raise VolumeError.new("Failed to list volumes exit code = #{exit_code}\nblkid\n#{blkid_resp}") unless exit_code == 0
  return parse_volumes(blkid_resp, conditions)
end