Last active
July 10, 2017 23:25
-
-
Save hx/d739387a1b5535bf81429ce142e0411d to your computer and use it in GitHub Desktop.
Create a software RAID set on macOS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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