crypt_reboot 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +169 -0
  5. data/exe/cryptreboot +9 -0
  6. data/lib/basic_loader.rb +60 -0
  7. data/lib/crypt_reboot/boot_config.rb +24 -0
  8. data/lib/crypt_reboot/cli/exiter.rb +25 -0
  9. data/lib/crypt_reboot/cli/happy_exiter.rb +14 -0
  10. data/lib/crypt_reboot/cli/params/definition.rb +125 -0
  11. data/lib/crypt_reboot/cli/params/flattener.rb +24 -0
  12. data/lib/crypt_reboot/cli/params/help_generator.rb +22 -0
  13. data/lib/crypt_reboot/cli/params/parser.rb +29 -0
  14. data/lib/crypt_reboot/cli/params_parsing_executor.rb +73 -0
  15. data/lib/crypt_reboot/cli/sad_exiter.rb +14 -0
  16. data/lib/crypt_reboot/cli.rb +8 -0
  17. data/lib/crypt_reboot/concatenator.rb +24 -0
  18. data/lib/crypt_reboot/config.rb +20 -0
  19. data/lib/crypt_reboot/crypt_tab/deserializer.rb +28 -0
  20. data/lib/crypt_reboot/crypt_tab/entry.rb +39 -0
  21. data/lib/crypt_reboot/crypt_tab/entry_deserializer.rb +51 -0
  22. data/lib/crypt_reboot/crypt_tab/entry_serializer.rb +21 -0
  23. data/lib/crypt_reboot/crypt_tab/keyfile_locator.rb +21 -0
  24. data/lib/crypt_reboot/crypt_tab/luks_to_plain_converter.rb +39 -0
  25. data/lib/crypt_reboot/crypt_tab/serializer.rb +29 -0
  26. data/lib/crypt_reboot/files_generator.rb +44 -0
  27. data/lib/crypt_reboot/files_writer.rb +17 -0
  28. data/lib/crypt_reboot/gziper.rb +22 -0
  29. data/lib/crypt_reboot/initramfs/archiver.rb +35 -0
  30. data/lib/crypt_reboot/initramfs/decompressor/intolerant_decompressor.rb +65 -0
  31. data/lib/crypt_reboot/initramfs/decompressor/tolerant_decompressor.rb +28 -0
  32. data/lib/crypt_reboot/initramfs/decompressor.rb +12 -0
  33. data/lib/crypt_reboot/initramfs/extractor.rb +36 -0
  34. data/lib/crypt_reboot/initramfs/patcher.rb +47 -0
  35. data/lib/crypt_reboot/initramfs_patch_squeezer.rb +28 -0
  36. data/lib/crypt_reboot/instantiable_config.rb +52 -0
  37. data/lib/crypt_reboot/kexec/loader.rb +30 -0
  38. data/lib/crypt_reboot/kexec_patching_loader.rb +28 -0
  39. data/lib/crypt_reboot/lazy_config.rb +22 -0
  40. data/lib/crypt_reboot/luks/checker.rb +27 -0
  41. data/lib/crypt_reboot/luks/data.rb +37 -0
  42. data/lib/crypt_reboot/luks/data_fetcher.rb +30 -0
  43. data/lib/crypt_reboot/luks/dumper/luks_v1_parser.rb +64 -0
  44. data/lib/crypt_reboot/luks/dumper/luks_v2_parser.rb +62 -0
  45. data/lib/crypt_reboot/luks/dumper.rb +30 -0
  46. data/lib/crypt_reboot/luks/key_fetcher.rb +50 -0
  47. data/lib/crypt_reboot/luks/version_detector.rb +32 -0
  48. data/lib/crypt_reboot/passphrase_asker.rb +15 -0
  49. data/lib/crypt_reboot/patched_initramfs_generator.rb +21 -0
  50. data/lib/crypt_reboot/rebooter.rb +20 -0
  51. data/lib/crypt_reboot/runner/binary.rb +12 -0
  52. data/lib/crypt_reboot/runner/boolean.rb +18 -0
  53. data/lib/crypt_reboot/runner/generic.rb +46 -0
  54. data/lib/crypt_reboot/runner/lines.rb +13 -0
  55. data/lib/crypt_reboot/runner/no_result.rb +13 -0
  56. data/lib/crypt_reboot/runner/text.rb +12 -0
  57. data/lib/crypt_reboot/runner.rb +8 -0
  58. data/lib/crypt_reboot/safe_temp/directory.rb +28 -0
  59. data/lib/crypt_reboot/safe_temp/file_name.rb +23 -0
  60. data/lib/crypt_reboot/safe_temp/mounter.rb +33 -0
  61. data/lib/crypt_reboot/single_assign_restricted_map.rb +26 -0
  62. data/lib/crypt_reboot/version.rb +5 -0
  63. data/lib/crypt_reboot.rb +17 -0
  64. metadata +138 -0
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ # Patch initramfs and load it along with kernel using kexec,
5
+ # so it is ready to be executed.
6
+ class KexecPatchingLoader
7
+ def call(boot_config = BootConfig.new(
8
+ kernel: Config.kernel,
9
+ initramfs: Config.initramfs,
10
+ cmdline: Config.cmdline
11
+ ))
12
+ generator.call(boot_config.initramfs) do |patched_initramfs|
13
+ patched_boot_config = boot_config.with_initramfs(patched_initramfs)
14
+ loader.call(patched_boot_config)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :generator, :loader
21
+
22
+ def initialize(generator: PatchedInitramfsGenerator.new,
23
+ loader: Kexec::Loader.new)
24
+ @generator = generator
25
+ @loader = loader
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ # Return getter lambdas instead of configuration settings directly
5
+ class LazyConfig
6
+ class << self
7
+ def method_missing(method_name, *args, **kwargs, &block)
8
+ return super unless instance.respond_to?(method_name)
9
+
10
+ -> { instance.send(method_name, *args, **kwargs, &block) }
11
+ end
12
+
13
+ def respond_to_missing?(name, *_, **_)
14
+ instance.respond_to?(name)
15
+ end
16
+
17
+ def instance
18
+ Config.instance
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ module Luks
5
+ # Return true in case given device is LUKS (of given version is provided), false otherwise
6
+ class Checker
7
+ def call(headevice, version = :any)
8
+ args = version == :any ? [] : ['--type', version]
9
+ runner.call(binary, 'isLuks', 'none', '--header', headevice, *args)
10
+ end
11
+
12
+ private
13
+
14
+ def binary
15
+ lazy_binary.call
16
+ end
17
+
18
+ attr_reader :lazy_binary, :runner
19
+
20
+ def initialize(lazy_binary: LazyConfig.cryptsetup_path,
21
+ runner: Runner::Boolean.new)
22
+ @lazy_binary = lazy_binary
23
+ @runner = runner
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ module Luks
5
+ # Value-object with encryption parameters
6
+ class Data
7
+ attr_reader :cipher, :offset, :sector_size, :key
8
+
9
+ def ==(other)
10
+ cipher == other.cipher && offset == other.offset &&
11
+ sector_size == other.sector_size && key == other.key
12
+ end
13
+
14
+ def key_bits
15
+ key.bytesize * 8
16
+ end
17
+
18
+ def with_key(new_key)
19
+ self.class.new(
20
+ cipher: cipher,
21
+ offset: offset,
22
+ sector_size: sector_size,
23
+ key: new_key
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ def initialize(cipher:, offset:, sector_size:, key: '')
30
+ @cipher = cipher
31
+ @offset = offset
32
+ @sector_size = sector_size
33
+ @key = key
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ module Luks
5
+ # Fetch LUKS data including key (user will be asked for passphrase)
6
+ class DataFetcher
7
+ def call(headevice)
8
+ version = detector.call(headevice)
9
+ data = dumper.call(headevice, version)
10
+ pass = asker.call("Enter passphrase to unlock #{headevice}: ")
11
+ key = key_fetcher.call(headevice, pass)
12
+ data.with_key(key)
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :detector, :dumper, :asker, :key_fetcher
18
+
19
+ def initialize(detector: VersionDetector.new,
20
+ dumper: Dumper.new,
21
+ asker: PassphraseAsker.new,
22
+ key_fetcher: KeyFetcher.new)
23
+ @detector = detector
24
+ @dumper = dumper
25
+ @asker = asker
26
+ @key_fetcher = key_fetcher
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ module Luks
5
+ class Dumper
6
+ # Parse LUKS1
7
+ class LuksV1Parser
8
+ ParsingError = Class.new StandardError
9
+
10
+ def call(lines)
11
+ data = parse_lines(lines)
12
+ instantiate(data.to_h)
13
+ end
14
+
15
+ private
16
+
17
+ SECTOR_SIZE = 512 # LUKS1 support only this sector size
18
+ private_constant :SECTOR_SIZE
19
+
20
+ def instantiate(raw)
21
+ data_class.new(
22
+ cipher: [raw.fetch(:cipher_name), raw.fetch(:cipher_mode)].join('-'),
23
+ offset: raw.fetch(:offset),
24
+ sector_size: SECTOR_SIZE
25
+ )
26
+ rescue KeyError => e
27
+ raise ParsingError, 'Parsing failed because of missing data', cause: e
28
+ end
29
+
30
+ def parse_lines(lines)
31
+ map_generator.call.tap do |result|
32
+ lines.each do |line|
33
+ update_result!(result, line: line)
34
+ end
35
+ end
36
+ end
37
+
38
+ def update_result!(result, line:)
39
+ case line
40
+ when /^Cipher name:\s+([\w-]+)$/
41
+ result[:cipher_name] = Regexp.last_match(1)
42
+ when /^Cipher mode:\s+([\w-]+)$/
43
+ result[:cipher_mode] = Regexp.last_match(1)
44
+ when /^Payload offset:\s+(\d+)$/
45
+ # LUKS1 provides offset in sectors
46
+ result[:offset] = Regexp.last_match(1).to_i * SECTOR_SIZE
47
+ end
48
+ rescue duplicate_exception => e
49
+ raise ParsingError, "Parsing failed on: `#{line}`", cause: e
50
+ end
51
+
52
+ attr_reader :data_class, :map_generator, :duplicate_exception
53
+
54
+ def initialize(data_class: Data,
55
+ map_generator: -> { SingleAssignRestrictedMap.new },
56
+ duplicate_exception: SingleAssignRestrictedMap::AlreadyAssigned)
57
+ @data_class = data_class
58
+ @map_generator = map_generator
59
+ @duplicate_exception = duplicate_exception
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ module Luks
5
+ class Dumper
6
+ # Parse LUKS2
7
+ class LuksV2Parser
8
+ ParsingError = Class.new StandardError
9
+
10
+ def call(lines)
11
+ data = parse_lines(lines)
12
+ instantiate(data.to_h)
13
+ end
14
+
15
+ private
16
+
17
+ def instantiate(args)
18
+ data_class.new(**args)
19
+ rescue ArgumentError => e
20
+ raise ParsingError, 'Parsing failed because of missing data', cause: e
21
+ end
22
+
23
+ def parse_lines(lines)
24
+ map_generator.call.tap do |result|
25
+ section_found = false
26
+ lines.each do |line|
27
+ if section_found
28
+ break if line =~ /^\w/
29
+
30
+ update_result!(result, line: line)
31
+ end
32
+ line =~ /^Data segments:/ && section_found = true
33
+ end
34
+ end
35
+ end
36
+
37
+ def update_result!(result, line:)
38
+ case line
39
+ when /offset:\s+(\d+) \[bytes\]/
40
+ result[:offset] = Regexp.last_match(1).to_i
41
+ when /cipher:\s+([\w-]+)$/
42
+ result[:cipher] = Regexp.last_match(1)
43
+ when /sector:\s+(\d+) \[bytes\]/
44
+ result[:sector_size] = Regexp.last_match(1).to_i
45
+ end
46
+ rescue duplicate_exception => e
47
+ raise ParsingError, "Parsing failed on: `#{line}`", cause: e
48
+ end
49
+
50
+ attr_reader :data_class, :map_generator, :duplicate_exception
51
+
52
+ def initialize(data_class: Data,
53
+ map_generator: -> { SingleAssignRestrictedMap.new },
54
+ duplicate_exception: SingleAssignRestrictedMap::AlreadyAssigned)
55
+ @data_class = data_class
56
+ @map_generator = map_generator
57
+ @duplicate_exception = duplicate_exception
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ module Luks
5
+ # Depending on LUKS version, delegates parsing to different parser
6
+ class Dumper
7
+ def call(headevice, version)
8
+ dump = runner.call(binary, 'luksDump', 'none', '--header', headevice)
9
+ parser = parsers.fetch(version)
10
+ parser.call(dump)
11
+ end
12
+
13
+ private
14
+
15
+ def binary
16
+ lazy_binary.call
17
+ end
18
+
19
+ attr_reader :lazy_binary, :runner, :parsers
20
+
21
+ def initialize(lazy_binary: LazyConfig.cryptsetup_path,
22
+ runner: Runner::Lines.new,
23
+ parsers: { 'LUKS2' => LuksV2Parser.new, 'LUKS1' => LuksV1Parser.new })
24
+ @lazy_binary = lazy_binary
25
+ @runner = runner
26
+ @parsers = parsers
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ module Luks
5
+ # Fetch LUKS key
6
+ class KeyFetcher
7
+ InvalidPassphrase = Class.new StandardError
8
+
9
+ def call(headevice, passphrase)
10
+ temp_provider.call do |key_file|
11
+ luks_dump(headevice, key_file, passphrase)
12
+ file_reader.call(key_file)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def luks_dump(headevice, master_key_file, passphrase)
19
+ runner.call(
20
+ binary, 'luksDump', 'none', '--header', headevice,
21
+ '--dump-master-key', '--master-key-file', master_key_file,
22
+ '--key-file', '-',
23
+ input: passphrase
24
+ )
25
+ rescue run_exception
26
+ # For simplicity sake let's assume it's invalid passphrase.
27
+ # Other errors such as invalid device/header were handled by previous validation.
28
+ raise InvalidPassphrase
29
+ end
30
+
31
+ def binary
32
+ lazy_binary.call
33
+ end
34
+
35
+ attr_reader :lazy_binary, :runner, :run_exception, :file_reader, :temp_provider
36
+
37
+ def initialize(lazy_binary: LazyConfig.cryptsetup_path,
38
+ runner: Runner::Lines.new,
39
+ run_exception: Runner::ExitError,
40
+ file_reader: File.method(:read),
41
+ temp_provider: SafeTemp::FileName.new)
42
+ @lazy_binary = lazy_binary
43
+ @runner = runner
44
+ @run_exception = run_exception
45
+ @file_reader = file_reader
46
+ @temp_provider = temp_provider
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ module Luks
5
+ # Return LUKS version or raise the exception if given file doesn't represent a valid LUKS device
6
+ class VersionDetector
7
+ Error = Class.new StandardError
8
+ NotLuks = Class.new Error
9
+ UnsupportedVersion = Class.new Error
10
+
11
+ def call(headevice)
12
+ version = supported_versions.find do |tested_version|
13
+ checker.call(headevice, tested_version)
14
+ end
15
+ return version if version
16
+ raise UnsupportedVersion if checker.call(headevice)
17
+
18
+ raise NotLuks
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :checker, :supported_versions
24
+
25
+ def initialize(checker: Checker.new,
26
+ supported_versions: %w[LUKS2 LUKS1])
27
+ @checker = checker
28
+ @supported_versions = supported_versions
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+
5
+ module CryptReboot
6
+ # Ask user for passphrase and return it
7
+ class PassphraseAsker
8
+ def call(prompt)
9
+ print prompt
10
+ $stdin.noecho(&:gets).chomp
11
+ ensure
12
+ puts
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ # Yield path to patched version of provided initramfs
5
+ class PatchedInitramfsGenerator
6
+ def call(initramfs_path, &block)
7
+ patch = squeezer.call(initramfs_path)
8
+ patcher.call(initramfs_path, patch, &block)
9
+ end
10
+
11
+ private
12
+
13
+ attr_reader :squeezer, :patcher
14
+
15
+ def initialize(squeezer: InitramfsPatchSqueezer.new,
16
+ patcher: Initramfs::Patcher.new)
17
+ @squeezer = squeezer
18
+ @patcher = patcher
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ # Perform the reboot or exit (doesn't return)
5
+ class Rebooter
6
+ def call(act = !Config.prepare_only)
7
+ act ? runner.call : exiter.call
8
+ end
9
+
10
+ private
11
+
12
+ attr_reader :runner, :exiter
13
+
14
+ def initialize(runner: -> { Process.exec Config.reboot_path },
15
+ exiter: -> { exit 0 })
16
+ @runner = runner
17
+ @exiter = exiter
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ module Runner
5
+ # Return stdout as string
6
+ class Binary < Generic
7
+ def call(*args, **opts)
8
+ super(*args, **opts.merge(binary: true)).out
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ module Runner
5
+ # Return true or false, depending if command succeeded or failed
6
+ class Boolean < Generic
7
+ def call(...)
8
+ super(...).success?
9
+ end
10
+
11
+ private
12
+
13
+ def initialize(**opts)
14
+ super(**opts.merge(run_method: :run!))
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-command'
4
+
5
+ module CryptReboot
6
+ module Runner
7
+ # Run an external process. Abstract class, use descendants.
8
+ class Generic
9
+ private
10
+
11
+ def call(*args, input: nil, output_file: nil, binary: false)
12
+ options = build_options(input, output_file, binary)
13
+ cmd.send(run_method, *args, **options)
14
+ rescue exceptions[:exit] => e
15
+ raise ExitError, cause: e
16
+ rescue exceptions[:not_found] => e
17
+ raise CommandNotFound, cause: e
18
+ end
19
+
20
+ def build_options(input, output_file, binary)
21
+ {}.tap do |options|
22
+ options[:input] = input if input
23
+ options[:out] = output_file if output_file
24
+ options[:binmode] = true if binary
25
+ end
26
+ end
27
+
28
+ def cmd
29
+ lazy_cmd.call
30
+ end
31
+
32
+ attr_reader :lazy_cmd, :run_method, :exceptions
33
+
34
+ def initialize(lazy_cmd: -> { TTY::Command.new(uuid: false, printer: Config.debug ? :pretty : :null) },
35
+ run_method: :run,
36
+ exceptions: {
37
+ exit: TTY::Command::ExitError,
38
+ not_found: Errno::ENOENT
39
+ })
40
+ @lazy_cmd = lazy_cmd
41
+ @run_method = run_method
42
+ @exceptions = exceptions
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ module Runner
5
+ # Return standard output in form of lines.
6
+ # Newline characters are removed.
7
+ class Lines < Generic
8
+ def call(...)
9
+ super(...).to_a
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ module Runner
5
+ # Return nil
6
+ class NoResult < Generic
7
+ def call(...)
8
+ super(...)
9
+ nil
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ module Runner
5
+ # Return stdout as string
6
+ class Text < Generic
7
+ def call(...)
8
+ super(...).out
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ module Runner
5
+ ExitError = Class.new StandardError
6
+ CommandNotFound = Class.new StandardError
7
+ end
8
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tmpdir'
4
+
5
+ module CryptReboot
6
+ module SafeTemp
7
+ # Create temporary directory, mounts tmpfs and yields tmp dir location.
8
+ # Make sure to cleanup afterwards.
9
+ class Directory
10
+ def call
11
+ tmp_maker.call do |dir|
12
+ mounter.call(dir) do
13
+ yield dir
14
+ end
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :mounter, :tmp_maker
21
+
22
+ def initialize(mounter: Mounter.new, tmp_maker: Dir.method(:mktmpdir))
23
+ @mounter = mounter
24
+ @tmp_maker = tmp_maker
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ module SafeTemp
5
+ # Yield non-existing temporary file name located in a safe dir.
6
+ # Afterwards the directory containing this file is deleted.
7
+ class FileName
8
+ def call(name = 'file')
9
+ dir_provider.call do |dir|
10
+ yield File.join(dir, name)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :dir_provider
17
+
18
+ def initialize(dir_provider: Directory.new)
19
+ @dir_provider = dir_provider
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptReboot
4
+ module SafeTemp
5
+ # Mount tmpfs at the given mount point, yield and unmount
6
+ class Mounter
7
+ def call(dir, &block)
8
+ mounter.call(dir)
9
+ run(dir, &block)
10
+ end
11
+
12
+ private
13
+
14
+ def run(dir, &block)
15
+ block.call
16
+ ensure
17
+ umounter.call(dir)
18
+ end
19
+
20
+ attr_reader :runner, :mounter, :umounter
21
+
22
+ def initialize(runner: Runner::NoResult.new,
23
+ mounter: lambda { |dir|
24
+ runner.call(Config.mount_path, '-t', 'tmpfs', '-o', 'mode=700', 'none', dir)
25
+ },
26
+ umounter: ->(dir) { runner.call(Config.umount_path, dir) })
27
+ @runner = runner
28
+ @mounter = mounter
29
+ @umounter = umounter
30
+ end
31
+ end
32
+ end
33
+ end