crypt_reboot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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