forked from Seltcana/rootfs-wip
252 lines
5.5 KiB
Crystal
252 lines
5.5 KiB
Crystal
require "colorize"
|
||
require "option_parser"
|
||
require "file_utils"
|
||
|
||
require "./quadruplet.cr"
|
||
require "./backends.cr"
|
||
|
||
# Not currently used. Waiting for API updates.
|
||
#require "baguette-crystal-base"
|
||
|
||
# Prototyping for the next generation of baguette-crystal-base.
|
||
|
||
class Baguette::Configuration
|
||
property application_name : String
|
||
|
||
def initialize(@application_name)
|
||
end
|
||
end
|
||
|
||
module Baguette::Log
|
||
def info(text)
|
||
STDOUT << (":: ".colorize :blue) << (text.colorize :white) << "\n"
|
||
end
|
||
|
||
def warn(text)
|
||
STDERR << (":: ".colorize :yellow) << (text.colorize :yellow) << "\n"
|
||
end
|
||
|
||
def error(text)
|
||
STDERR << (":: ".colorize :red) << (text.colorize :red) << "\n"
|
||
end
|
||
end
|
||
|
||
module Baguette::Base
|
||
getter configuration : Configuration
|
||
|
||
include Baguette::Log
|
||
end
|
||
|
||
class OptionParser
|
||
def to_s(io : IO) : Nil
|
||
if banner = @banner
|
||
io << banner << '\n'
|
||
end
|
||
|
||
@flags.select(&.starts_with? " -").join io, '\n'
|
||
|
||
io << "\ncommands:\n"
|
||
@flags.select{ |x| !x.starts_with? " -" }.join io, '\n'
|
||
end
|
||
end
|
||
|
||
Colorize.on_tty_only!
|
||
|
||
# CAUTION
|
||
# backends: storage, ???
|
||
# rootfs templates
|
||
|
||
class RootFS::Context
|
||
include Baguette::Base
|
||
|
||
def initialize
|
||
@configuration = Baguette::Configuration.new "rootfs"
|
||
end
|
||
|
||
def run
|
||
options_parser = uninitialized OptionParser
|
||
|
||
commands = {
|
||
:new => ->(args : Array(String)){
|
||
RootFS.new args[1], args[0]
|
||
},
|
||
:save => ->(args : Array(String)){
|
||
RootFS.load(args[0]).save(args[1])
|
||
},
|
||
:restore => ->(args : Array(String)){
|
||
RootFS.load(args[0]).restore(args[1])
|
||
},
|
||
:mount => ->(args : Array(String)){
|
||
RootFS.load(args[0]).mount(args[1]?)
|
||
},
|
||
:unmount => ->(args : Array(String)){
|
||
RootFS.load(args[0]).unmount(args[1]?)
|
||
}
|
||
}
|
||
|
||
command = nil
|
||
arguments = uninitialized Array(String)
|
||
|
||
options_parser = OptionParser.parse do |parser|
|
||
parser.banner = "usage: #{@configuration.application_name} <command> [options]"
|
||
|
||
parser.on "new", "Creates a new rootfs." do
|
||
parser.banner = "usage: #{@configuration.application_name} new <template> <rootfs-directory>"
|
||
|
||
command = :new
|
||
end
|
||
|
||
parser.on "save", "Creates a snapshot of a rootfs." do
|
||
parser.banner = "usage: #{@configuration.application_name} save <rootfs-directory> <snapshot-directory>"
|
||
command = :save
|
||
end
|
||
|
||
parser.on "restore", "Restores a rootfs from a snapshot." do
|
||
parser.banner = "usage: #{@configuration.application_name} restore <rootfs-directory> <snapshot-directory>"
|
||
command = :restore
|
||
end
|
||
|
||
parser.on "mount", "Bind-mount a directory in a rootfs." do
|
||
parser.banner = "usage: #{@configuration.application_name} mount <rootfs-directory> [mount-point]"
|
||
command = :mount
|
||
end
|
||
|
||
parser.on "unmount", "Unmount a directory from a rootfs." do
|
||
parser.banner = "usage: #{@configuration.application_name} unmount <rootfs-directory> [mount-point]"
|
||
command = :unmount
|
||
end
|
||
|
||
parser.on "-c", "--color", "Forces the use of colors." do
|
||
Colorize.enabled = true
|
||
end
|
||
|
||
parser.on "-h", "--help", "Displays this help message." do
|
||
puts parser
|
||
exit 0
|
||
end
|
||
|
||
parser.unknown_args do |args|
|
||
if command.nil?
|
||
raise "no command provided" if args.size < 1
|
||
end
|
||
|
||
arguments = args
|
||
end
|
||
end
|
||
|
||
if command.nil?
|
||
puts options_parser
|
||
exit 1
|
||
end
|
||
|
||
if proc = commands[command]?
|
||
proc.call(arguments)
|
||
else
|
||
# FIXME: This is an internal error. It shouldn’t trigger.
|
||
raise "unrecognized command: #{command}"
|
||
end
|
||
rescue e
|
||
if message = e.message
|
||
error message
|
||
else
|
||
error e.to_s
|
||
end
|
||
end
|
||
end
|
||
|
||
class RootFS::RootFS
|
||
include Baguette::Log
|
||
|
||
getter directory : String
|
||
|
||
def initialize(@directory)
|
||
end
|
||
|
||
def self.new(directory, template : String)
|
||
instance = RootFS.allocate
|
||
instance.initialize directory
|
||
instance.create! template
|
||
instance
|
||
end
|
||
|
||
protected def execute(command : String)
|
||
info "Executing: #{command}"
|
||
r = Process.new(
|
||
"sh", ["-c", command],
|
||
input: Process::Redirect::Inherit,
|
||
output: Process::Redirect::Inherit,
|
||
error: Process::Redirect::Inherit
|
||
).wait
|
||
|
||
raise "Command returned #{r.exit_status}." unless r.success?
|
||
end
|
||
|
||
protected def create!(template : String)
|
||
quadruplet = Quadruplet.new template
|
||
|
||
is_btrfs = true # FIXME: do the actual detection
|
||
|
||
unless is_btrfs
|
||
raise "can only create rootfs in a btrfs filesystem"
|
||
end
|
||
|
||
unless backend = Backend.all.find &.name.==(quadruplet.name)
|
||
error "unrecognized quadruplet name: #{quadruplet.name}"
|
||
exit 2
|
||
end
|
||
|
||
execute "btrfs subvolume create '#{@directory}'"
|
||
|
||
# FIXME: We should homogeneize quadruplet components.
|
||
backend.new(@directory, self).create! quadruplet.architecture, quadruplet.version, quadruplet.variant
|
||
|
||
File.write template, "#{@directory}/rootfs"
|
||
end
|
||
|
||
def self.load(directory)
|
||
new directory
|
||
end
|
||
|
||
def delete
|
||
show!
|
||
puts "UNIMPLEMENTED: removing rootfs directory here"
|
||
end
|
||
|
||
def save(snapshot_directory)
|
||
execute "btrfs subvolume snapshot '#{@directory}' '#{snapshot_directory}'"
|
||
end
|
||
|
||
def restore(snapshot_directory)
|
||
unmount
|
||
execute "btrfs subvolume delete '#{@directory}'"
|
||
execute "btrfs subvolume snapshot '#{snapshot_directory}' '#{@directory}'"
|
||
end
|
||
|
||
def mount(directory = nil)
|
||
if directory
|
||
execute "mount --bind '#{directory}' '#{@directory}/#{directory}'"
|
||
return
|
||
end
|
||
|
||
mount "/proc"
|
||
mount "/sys"
|
||
end
|
||
|
||
def unmount(directory = nil)
|
||
if directory
|
||
execute "mountpoint -q '#{@directory}/#{directory}' || exit 0 && umount '#{@directory}/#{directory}'"
|
||
return
|
||
end
|
||
|
||
unmount "/proc"
|
||
unmount "/sys"
|
||
end
|
||
|
||
def show!
|
||
puts "#{@directory}"
|
||
end
|
||
end
|
||
|
||
RootFS::Context.new.run
|
||
|