Skip to content

Instantly share code, notes, and snippets.

@hx
Last active July 10, 2017 23:25
Show Gist options
  • Save hx/d739387a1b5535bf81429ce142e0411d to your computer and use it in GitHub Desktop.
Save hx/d739387a1b5535bf81429ce142e0411d to your computer and use it in GitHub Desktop.
Create a software RAID set on macOS
#!/usr/bin/env ruby
# coding: utf-8
abort "This script needs Ruby 2 or later. You have #{RUBY_VERSION}." if RUBY_VERSION < '2.0.0'
require 'io/console'
require 'pathname'
require 'shellwords'
# Imitating ActiveSupport a bit here:
class String
def blank?
strip == ''
end
end
class Object
def blank?
!!self
end
def present?
!blank?
end
end
STDIN.reopen('/dev/tty') unless STDIN.isatty # So this script can be piped into Ruby
class Disk < Hash
attr_accessor :selected
attr_reader :device_name
def initialize(device_name)
@device_name = device_name
merge! `diskutil info #{device_name}`.scan(/^\s*(.+?):\s+(.+?)\s*$/).to_h
end
def raidable?
!(virtual? || internal?)
end
def virtual?
self['Virtual'] == 'Yes'
end
def internal?
self['Device Location'] == 'Internal'
end
def description
@name ||= (
n = self['Volume Name']
n = self['Device / Media Name'] if n.start_with? 'Not applicable'
n
)
end
# def byte_size
# @byte_size ||= self['Disk Size'][/\((\d+) Bytes\)/, 1].to_i
# end
def human_size
@human_size ||= (self['Total Size'] || self['Disk Size'])[/^(.+?)\s*\(/, 1]
end
def toggle!
self.selected = !selected
end
class << self
attr_accessor :cursor, :chosen
def all
@all ||= Dir['/dev/disk?'].map { |n| Disk.new n[/\w+$/] }.select(&:raidable?)
end
def show!
puts "\e[A" * (all.length + 5) if @shown
all.map.with_index do |disk, index|
puts [
index == cursor ? '➤ ' : ' ',
disk.selected ? '✓' : ' ',
disk.human_size,
disk.description
].join ' '
end
puts ''
puts 'UP / DOWN to move cursor'
puts 'SPACE to toggle'
puts 'ENTER to continue'
@shown = true
end
def move(offset)
self.cursor = (cursor + offset) % all.length
show!
end
def toggle!
all[cursor].toggle!
show!
end
def choose!
self.chosen = all.select(&:selected)
end
end
self.cursor = 0
end
# Borrowed from https://gist.github.com/acook/4190379
def read_char
STDIN.echo = false
STDIN.raw!
input = STDIN.getc.chr
if input == "\e"
input << STDIN.read_nonblock(3) rescue nil
input << STDIN.read_nonblock(2) rescue nil
end
ensure
STDIN.echo = true
STDIN.cooked!
return input
end
# Borrowed from Captain
def prompt(str, silent = false, default: nil)
print str
result = silent ? STDIN.noecho(&:gets) : STDIN.gets
puts '' if silent || result.nil? || !result.end_with?($/)
result = (result || '').chomp
result = default if result.blank? && default.present?
result = yield(result) if block_given?
result
end
# Borrowed from Captain
def choose(text, letters)
chars = letters.chars.map(&:downcase).uniq
default = letters.chars.find { |c| c.upcase == c }
default = default.downcase if default
print "#{text} [#{letters}]: "
result = nil
while result.nil?
result = STDIN.getch.downcase
print result = default if result == "\r" || result == "\n"
result = nil unless chars.include? result
end
puts ''
result
end
puts "Create a new AppleRAID volume (PID #{$$})"
puts ''
puts 'Choose two or more volumes to continue;'
puts ''
Disk.show!
until Disk.chosen do
case read_char
when "\e[A"
Disk.move -1
when "\e[B"
Disk.move 1
when ' '
Disk.toggle!
when "\r"
Disk.choose!
when "\e", "\x03" # Escape or CTRL+C
exit 1
else
print "\x07" # Alarm
end
end
abort 'You need to choose at least two disks.' unless Disk.chosen.length >= 2
puts ''
scheme = {
s: 'stripe',
m: 'mirror',
c: 'concat'
}[choose('Do you want Striped, Mirrored, or Concatenated?', 'smc').to_sym]
name = prompt('Enter a name for your RAID volume: ', default: 'New RAID Volume', &:strip)
puts ''
cmd = ['diskutil', 'appleRAID', 'create', scheme, name, 'JHFS+', *Disk.chosen.map(&:device_name)]
puts 'This is the command you need to run:'
puts ''
puts cmd.map(&:shellescape).join ' '
puts
system *cmd if choose('Do you want me to run it for you?', 'Yn') == 'y'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment