2020-11-25 22:01:45 +01:00
|
|
|
|
require "colorize"
|
|
|
|
|
require "option_parser"
|
|
|
|
|
require "file_utils"
|
|
|
|
|
|
2020-12-12 08:55:47 +01:00
|
|
|
|
require "./quadruplet.cr"
|
|
|
|
|
require "./backends.cr"
|
|
|
|
|
|
2020-11-25 22:01:45 +01:00
|
|
|
|
# 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
|
|
|
|
|
|
2020-11-28 21:21:01 +01:00
|
|
|
|
module Baguette::Log
|
2020-11-25 22:01:45 +01:00
|
|
|
|
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)
|
2020-11-28 21:21:01 +01:00
|
|
|
|
STDERR << (":: ".colorize :red) << (text.colorize :red) << "\n"
|
2020-11-25 22:01:45 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2020-11-28 21:21:01 +01:00
|
|
|
|
module Baguette::Base
|
|
|
|
|
getter configuration : Configuration
|
|
|
|
|
|
|
|
|
|
include Baguette::Log
|
|
|
|
|
end
|
|
|
|
|
|
2020-12-15 10:06:13 +01:00
|
|
|
|
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
|
|
|
|
|
|
2020-11-25 22:01:45 +01:00
|
|
|
|
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 = {
|
2020-12-15 10:06:13 +01:00
|
|
|
|
:new => ->(args : Array(String)){
|
2020-11-28 21:21:01 +01:00
|
|
|
|
RootFS.new args[1], args[0]
|
|
|
|
|
},
|
2020-12-15 10:06:13 +01:00
|
|
|
|
:save => ->(args : Array(String)){
|
2020-11-28 21:21:01 +01:00
|
|
|
|
RootFS.load(args[0]).save(args[1])
|
|
|
|
|
},
|
2020-12-15 10:06:13 +01:00
|
|
|
|
:restore => ->(args : Array(String)){
|
2020-11-28 21:21:01 +01:00
|
|
|
|
RootFS.load(args[0]).restore(args[1])
|
|
|
|
|
},
|
2020-12-15 10:06:13 +01:00
|
|
|
|
:mount => ->(args : Array(String)){
|
2020-11-29 00:23:28 +01:00
|
|
|
|
RootFS.load(args[0]).mount(args[1]?)
|
|
|
|
|
},
|
2020-12-15 10:06:13 +01:00
|
|
|
|
:unmount => ->(args : Array(String)){
|
2020-11-29 00:23:28 +01:00
|
|
|
|
RootFS.load(args[0]).unmount(args[1]?)
|
2020-11-25 22:01:45 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-15 10:06:13 +01:00
|
|
|
|
command = nil
|
2020-11-25 22:01:45 +01:00
|
|
|
|
arguments = uninitialized Array(String)
|
|
|
|
|
|
|
|
|
|
options_parser = OptionParser.parse do |parser|
|
|
|
|
|
parser.banner = "usage: #{@configuration.application_name} <command> [options]"
|
|
|
|
|
|
2020-12-15 10:06:13 +01:00
|
|
|
|
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
|
|
|
|
|
|
2020-11-25 22:01:45 +01:00
|
|
|
|
parser.on "-c", "--color", "Forces the use of colors." do
|
|
|
|
|
Colorize.enabled = true
|
|
|
|
|
end
|
|
|
|
|
|
2020-11-28 21:21:01 +01:00
|
|
|
|
parser.on "-h", "--help", "Displays this help message." do
|
2020-12-15 10:06:13 +01:00
|
|
|
|
puts parser
|
2020-11-28 21:21:01 +01:00
|
|
|
|
exit 0
|
|
|
|
|
end
|
|
|
|
|
|
2020-11-25 22:01:45 +01:00
|
|
|
|
parser.unknown_args do |args|
|
2020-12-15 10:06:13 +01:00
|
|
|
|
if command.nil?
|
|
|
|
|
raise "no command provided" if args.size < 1
|
|
|
|
|
end
|
2020-11-25 22:01:45 +01:00
|
|
|
|
|
|
|
|
|
arguments = args
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2020-12-15 10:06:13 +01:00
|
|
|
|
if command.nil?
|
|
|
|
|
puts options_parser
|
|
|
|
|
exit 1
|
|
|
|
|
end
|
|
|
|
|
|
2020-11-25 22:01:45 +01:00
|
|
|
|
if proc = commands[command]?
|
|
|
|
|
proc.call(arguments)
|
|
|
|
|
else
|
2020-12-15 10:06:13 +01:00
|
|
|
|
# FIXME: This is an internal error. It shouldn’t trigger.
|
2020-11-25 22:01:45 +01:00
|
|
|
|
raise "unrecognized command: #{command}"
|
|
|
|
|
end
|
|
|
|
|
rescue e
|
|
|
|
|
if message = e.message
|
|
|
|
|
error message
|
|
|
|
|
else
|
|
|
|
|
error e.to_s
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
class RootFS::RootFS
|
2020-11-28 21:21:01 +01:00
|
|
|
|
include Baguette::Log
|
|
|
|
|
|
2020-11-25 22:01:45 +01:00
|
|
|
|
getter directory : String
|
|
|
|
|
|
|
|
|
|
def initialize(@directory)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def self.new(directory, template : String)
|
|
|
|
|
instance = RootFS.allocate
|
|
|
|
|
instance.initialize directory
|
|
|
|
|
instance.create! template
|
|
|
|
|
instance
|
|
|
|
|
end
|
|
|
|
|
|
2020-11-28 21:21:01 +01:00
|
|
|
|
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
|
|
|
|
|
|
2020-11-25 22:01:45 +01:00
|
|
|
|
protected def create!(template : String)
|
2020-12-12 08:55:47 +01:00
|
|
|
|
quadruplet = Quadruplet.new template
|
2020-11-29 20:48:47 +01:00
|
|
|
|
|
2020-11-28 21:21:01 +01:00
|
|
|
|
is_btrfs = true # FIXME: do the actual detection
|
|
|
|
|
|
|
|
|
|
unless is_btrfs
|
|
|
|
|
raise "can only create rootfs in a btrfs filesystem"
|
|
|
|
|
end
|
|
|
|
|
|
2020-12-12 08:55:47 +01:00
|
|
|
|
unless backend = Backend.all.find &.name.==(quadruplet.name)
|
|
|
|
|
error "unrecognized quadruplet name: #{quadruplet.name}"
|
|
|
|
|
exit 2
|
|
|
|
|
end
|
2020-12-12 05:25:39 +01:00
|
|
|
|
|
2020-12-12 08:55:47 +01:00
|
|
|
|
execute "btrfs subvolume create '#{@directory}'"
|
2020-12-12 05:25:39 +01:00
|
|
|
|
|
2020-12-12 08:55:47 +01:00
|
|
|
|
# FIXME: We should homogeneize quadruplet components.
|
|
|
|
|
backend.new(@directory, self).create! quadruplet.architecture, quadruplet.version, quadruplet.variant
|
2020-12-12 05:25:39 +01:00
|
|
|
|
|
2020-12-12 08:55:47 +01:00
|
|
|
|
File.write template, "#{@directory}/rootfs"
|
2020-11-25 22:01:45 +01:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def self.load(directory)
|
2020-11-28 21:21:01 +01:00
|
|
|
|
new directory
|
2020-11-25 22:01:45 +01:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def delete
|
|
|
|
|
show!
|
|
|
|
|
puts "UNIMPLEMENTED: removing rootfs directory here"
|
|
|
|
|
end
|
|
|
|
|
|
2020-11-28 21:21:01 +01:00
|
|
|
|
def save(snapshot_directory)
|
|
|
|
|
execute "btrfs subvolume snapshot '#{@directory}' '#{snapshot_directory}'"
|
2020-11-25 22:01:45 +01:00
|
|
|
|
end
|
|
|
|
|
|
2020-11-28 21:21:01 +01:00
|
|
|
|
def restore(snapshot_directory)
|
|
|
|
|
unmount
|
|
|
|
|
execute "btrfs subvolume delete '#{@directory}'"
|
|
|
|
|
execute "btrfs subvolume snapshot '#{snapshot_directory}' '#{@directory}'"
|
2020-11-25 22:01:45 +01:00
|
|
|
|
end
|
|
|
|
|
|
2020-11-28 21:21:01 +01:00
|
|
|
|
def mount(directory = nil)
|
2020-11-29 00:23:28 +01:00
|
|
|
|
if directory
|
|
|
|
|
execute "mount --bind '#{directory}' '#{@directory}/#{directory}'"
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
mount "/proc"
|
|
|
|
|
mount "/sys"
|
2020-11-25 22:01:45 +01:00
|
|
|
|
end
|
|
|
|
|
|
2020-11-28 21:21:01 +01:00
|
|
|
|
def unmount(directory = nil)
|
2020-11-29 00:23:28 +01:00
|
|
|
|
if directory
|
|
|
|
|
execute "umount '#{@directory}/#{directory}'"
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
unmount "/proc"
|
|
|
|
|
unmount "/sys"
|
2020-11-25 22:01:45 +01:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def show!
|
2020-11-28 21:21:01 +01:00
|
|
|
|
puts "#{@directory}"
|
2020-11-25 22:01:45 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
RootFS::Context.new.run
|
|
|
|
|
|