Class: OsCtl::Lib::Exporter::Zfs

Inherits:
Exporter::Base
  • Object
show all
Includes:
Utils::Log, Utils::System
Defined in:
lib/libosctl/exporter/zfs.rb

Overview

Handles dumping containers as ZFS streams into tar archives

Usage:

exporter.dump_rootfs do
# Create a snapshot a dump it
exporter.dump_base

# Create another snapshot and dump it as an incremental stream
# from the base snapshot
exporter.dump_incremental
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Utils::System

#find_executable!, #read_process_output, #repeat_on_failure, #syscmd, #syscmd_argv, #write_stdin, #zfs

Methods included from Utils::Log

included

Constructor Details

#initialize(*_) ⇒ Zfs

Returns a new instance of Zfs.



21
22
23
24
25
26
# File 'lib/libosctl/exporter/zfs.rb', line 21

def initialize(*_)
  super

  @datasets = ct.datasets[1..] # skip the root dataset
  @snapshots = []
end

Instance Attribute Details

#base_snapObject (readonly, protected)

Returns the value of attribute base_snap.



79
80
81
# File 'lib/libosctl/exporter/zfs.rb', line 79

def base_snap
  @base_snap
end

#snapshotsObject (readonly, protected)

Returns the value of attribute snapshots.



79
80
81
# File 'lib/libosctl/exporter/zfs.rb', line 79

def snapshots
  @snapshots
end

Instance Method Details

#check_stream_statuses(compression, statuses) ⇒ Object (protected)



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/libosctl/exporter/zfs.rb', line 168

def check_stream_statuses(compression, statuses)
  processes = [['zfs send', statuses.fetch(0)]]
  processes << ['gzip', statuses.fetch(1)] if compression == :gzip

  failures = processes.filter_map do |name, status|
    next if status.success?

    if status.exited?
      "#{name} failed with exit status #{status.exitstatus}"
    else
      "#{name} was terminated by signal #{status.termsig}"
    end
  end

  raise failures.join('; ') unless failures.empty?
end

#dump_baseObject

Dump initial data stream

Should be called from within the block given to #dump_rootfs.



54
55
56
57
58
59
60
# File 'lib/libosctl/exporter/zfs.rb', line 54

def dump_base
  @base_snap = snapshot(ct.dataset, 'base')

  each_dataset_file('base') do |ds, file|
    dump_stream(file, ds, base_snap)
  end
end

#dump_file_name(compression, name) ⇒ Object (protected)



199
200
201
202
203
204
205
206
207
208
209
# File 'lib/libosctl/exporter/zfs.rb', line 199

def dump_file_name(compression, name)
  base = File.join('rootfs', "#{name}.dat")

  case compression
  when :gzip
    "#{base}.gz"

  else
    base
  end
end

#dump_incremental(from_snap: nil) ⇒ Object

Dump incremental data stream from the base stream

Should be called from within the block given to #dump_rootfs.



65
66
67
68
69
70
71
# File 'lib/libosctl/exporter/zfs.rb', line 65

def dump_incremental(from_snap: nil)
  snap = snapshot(ct.dataset, 'incr')

  each_dataset_file('incremental') do |ds, file|
    dump_stream(file, ds, snap, from_snap || base_snap)
  end
end

#dump_rootfs { ... } ⇒ Object

Method used to wrap dumping of base and incremental data streams of rootfs

Yields:



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/libosctl/exporter/zfs.rb', line 31

def dump_rootfs
  tar.mkdir('rootfs', DIR_MODE)

  each_dataset_dir do |_ds, dir|
    tar.mkdir(File.join('rootfs', dir), DIR_MODE)
  end

  yield

  tar.add_file('snapshots.yml', FILE_MODE) do |tf|
    tf.write(ConfigFile.dump_yaml(snapshots.reverse))
  end
ensure
  each_dataset do |ds|
    snapshots.reverse_each do |snap|
      zfs(:destroy, '', "#{ds}@#{snap}")
    end
  end
end

#dump_stream(name, dataset, snap, from_snap = nil) ⇒ Object (protected)



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/libosctl/exporter/zfs.rb', line 122

def dump_stream(name, dataset, snap, from_snap = nil)
  compression = get_compression(dataset)
  statuses = nil

  cmd = if from_snap
          "#{zfs_send} -I @#{from_snap} #{dataset}@#{snap}"
        else
          "#{zfs_send} #{dataset}@#{snap}"
        end

  tar.add_file(dump_file_name(compression, name), FILE_MODE) do |tf|
    statuses = process_stream(compression, cmd, tf)
  end

  check_stream_statuses(compression, statuses)
end

#each_dataset {|ds| ... } ⇒ Object (protected)

Iterate over all datasets

Yield Parameters:



83
84
85
86
# File 'lib/libosctl/exporter/zfs.rb', line 83

def each_dataset(&block)
  block.call(ct.dataset)
  datasets.each(&block)
end

#each_dataset_dir {|ds, dir_name| ... } ⇒ Object (protected)

Iterate over all datasets and yield the dataset along with directory name for the tar archive, where its streams will be stored.

Yield Parameters:

  • ds (Zfs::Dataset)
  • dir_name (String)

    directory name within the archive



93
94
95
96
97
# File 'lib/libosctl/exporter/zfs.rb', line 93

def each_dataset_dir
  each_dataset do |ds|
    yield(ds, ds.relative_name)
  end
end

#each_dataset_file(name) {|ds, fname| ... } ⇒ Object (protected)

Iterate over all datasets and yield the dataset along with file name for the archive.

Parameters:

  • name (String)

    base/incremental

Yield Parameters:

  • ds (Zfs::Dataset)
  • fname (String)

    file name within the archive



105
106
107
108
109
# File 'lib/libosctl/exporter/zfs.rb', line 105

def each_dataset_file(name)
  each_dataset_dir do |ds, dir|
    yield(ds, File.join(dir, name))
  end
end

#formatObject



73
74
75
# File 'lib/libosctl/exporter/zfs.rb', line 73

def format
  :zfs
end

#get_compression(dataset) ⇒ Object (protected)



185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/libosctl/exporter/zfs.rb', line 185

def get_compression(dataset)
  case opts[:compression]
  when :auto
    if !opts[:compressed_send] || zfs(:get, '-H -o value compression', dataset).output.strip == 'off'
      :gzip
    else
      :off
    end

  else
    opts[:compression].to_sym
  end
end

#gzip_commandObject (protected)



160
161
162
# File 'lib/libosctl/exporter/zfs.rb', line 160

def gzip_command
  ['gzip', '-c']
end

#process_stream(compression, command, tf) ⇒ Object (protected)



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/libosctl/exporter/zfs.rb', line 139

def process_stream(compression, command, tf)
  pipeline =
    case compression
    when :gzip
      ["exec #{command}", resolve_command(gzip_command)]
    when :off
      ["exec #{command}"]
    else
      raise "unexpected compression type '#{compression}'"
    end

  statuses = nil

  Open3.pipeline_r(*pipeline) do |stream, wait_threads|
    IO.copy_stream(stream, tf)
    statuses = wait_threads.map(&:value)
  end

  statuses
end

#resolve_command(command) ⇒ Object (protected)



164
165
166
# File 'lib/libosctl/exporter/zfs.rb', line 164

def resolve_command(command)
  [find_executable!(command.fetch(0)), *command.drop(1)]
end

#snapshot(dataset, type) ⇒ Object (protected)



115
116
117
118
119
120
# File 'lib/libosctl/exporter/zfs.rb', line 115

def snapshot(dataset, type)
  snap = snapshot_name(type)
  zfs(:snapshot, '-r', "#{dataset}@#{snap}")
  snapshots << snap
  snap
end

#snapshot_name(type) ⇒ Object (protected)



111
112
113
# File 'lib/libosctl/exporter/zfs.rb', line 111

def snapshot_name(type)
  "osctl-#{type}-#{Time.now.to_i}"
end

#zfs_sendObject (protected)



211
212
213
214
215
216
217
218
# File 'lib/libosctl/exporter/zfs.rb', line 211

def zfs_send
  if opts[:compressed_send]
    'zfs send -c'

  else
    'zfs send'
  end
end