Class: OsCtld::Container::DatasetBuilder

Inherits:
Object
  • Object
show all
Includes:
OsCtl::Lib::Utils::Log, OsCtl::Lib::Utils::System, Utils::SwitchUser
Defined in:
lib/osctld/container/dataset_builder.rb

Instance Method Summary collapse

Methods included from Utils::SwitchUser

#ct_attach, #ct_syscmd

Constructor Details

#initialize(opts = {}) ⇒ DatasetBuilder

Returns a new instance of DatasetBuilder.

Parameters:

  • opts (Hash) (defaults to: {})

Options Hash (opts):

  • :cmd (Command::Base)


14
15
16
# File 'lib/osctld/container/dataset_builder.rb', line 14

def initialize(opts = {})
  @builder_opts = opts
end

Instance Method Details

#copy_datasets(src, dst, from: nil) ⇒ String

Returns snapshot name.

Parameters:

  • src (Array<OsCtl::Lib::Zfs::Dataset>)
  • dst (Array<OsCtl::Lib::Zfs::Dataset>)
  • from (String, nil) (defaults to: nil) —

    base snapshot

Returns:

  • (String) —

    snapshot name



48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/osctld/container/dataset_builder.rb', line 48

def copy_datasets(src, dst, from: nil)
  snap = "osctl-copy-#{from ? 'incr' : 'base'}-#{Time.now.to_i}"
  zfs(:snapshot, nil, src.map { |ds| "#{ds}@#{snap}" }.join(' '))

  zipped = src.zip(dst)

  zipped.each do |src_ds, dst_ds|
    progress("Copying dataset #{src_ds.relative_name}")
    syscmd("zfs send -p -L #{from ? "-i @#{from}" : ''} #{src_ds}@#{snap} " \
           "| zfs recv -F #{dst_ds}")
  end

  snap
end

#create_dataset(ds, opts = {}) ⇒ Object

Parameters:

  • ds (OsCtl::Lib::Zfs::Dataset)
  • opts (Hash) (defaults to: {}) —

    options

Options Hash (opts):

  • :uid_map (OsCtl::Lib::IdMap)
  • :gid_map (OsCtl::Lib::IdMap)
  • :parents (Boolean)
  • :properties (Hash)


24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/osctld/container/dataset_builder.rb', line 24

def create_dataset(ds, opts = {})
  zfs_opts = {
    properties: {
      canmount: 'noauto'
    }.merge(opts[:properties] || {})
  }
  zfs_opts[:parents] = true if opts[:parents]

  if opts[:uid_map]
    zfs_opts[:properties][:uidmap] = opts[:uid_map].map(&:to_s).join(',')
  end

  if opts[:gid_map]
    zfs_opts[:properties][:gidmap] = opts[:gid_map].map(&:to_s).join(',')
  end

  ds.create!(**zfs_opts)
  ds.mount(recursive: true)
end

#from_local_archive(image, dir, ds, opts = {}) ⇒ Object

Parameters:

  • image (String) —

    image path

  • dir (String) —

    dir to extract it to

  • ds (OsCtl::Lib::Zfs::Dataset)
  • opts (Hash) (defaults to: {}) —

    options

Options Hash (opts):

  • :distribution (String)
  • :version (String)
  • :mapping (Boolean)


70
71
72
73
74
# File 'lib/osctld/container/dataset_builder.rb', line 70

def from_local_archive(image, dir, ds, opts = {})
  progress('Extracting image')
  syscmd("tar -xzf #{image} -C #{dir}")
  shift_dataset(ds, opts) if opts[:mapping]
end

#from_tar_stream(image, member, compression, ds) ⇒ Object

Parameters:

  • image (String) —

    image path

  • member (String) —

    file from the tar to use

  • compression (:gzip, :off) —

    compression type

  • ds (OsCtl::Lib::Zfs::Dataset)

Raises:



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/osctld/container/dataset_builder.rb', line 80

def from_tar_stream(image, member, compression, ds)
  progress('Writing data stream')

  commands = [
    ['tar', '-xOf', image, member]
  ]

  case compression
  when :gzip
    commands << ['gunzip']
  when :off
    # no command
  else
    raise "unexpected compression type '#{compression}'"
  end

  commands << ['zfs', 'recv', '-F', ds.to_s]

  command_string = commands.map { |c| Shellwords.join(c) }.join(' | ')
  pipeline_script = <<~BASH
    #{command_string}
    pipeline_status=("${PIPESTATUS[@]}")
    printf '%s\n' "${pipeline_status[*]}" > "$1"
    for stage_status in "${pipeline_status[@]}"; do
      if (( stage_status != 0 )); then
        exit "$stage_status"
      fi
    done
  BASH

  # Note that we intentionally use shell to run the pipeline. Whenever ruby
  # is more involved in the process, we start to experience random deadlocks
  # when the zfs receive hangs.
  stderr_output, stage_statuses, status = Tempfile.create('osctld-import-stream-stderr') do |stderr|
    Tempfile.create('osctld-import-stream-status') do |status_file|
      pid = Process.spawn(
        'bash',
        '-c', pipeline_script,
        'osctld-import-stream', status_file.path,
        err: stderr
      )
      wait_result = Process.wait2(pid)
      status_file.rewind
      statuses = status_file.read.strip.split.map { |v| Integer(v, 10) }
      stderr.rewind

      [stderr.read, statuses, wait_result[1]]
    end
  end

  complete_status = stage_statuses.length == commands.length
  return nil if status.success? && complete_status && stage_statuses.all?(&:zero?)

  shell_status =
    if status.signaled?
      "signal #{status.termsig}"
    else
      status.exitstatus.to_s
    end

  failed_stages = commands.each_with_index.filter_map do |command, i|
    stage_status = stage_statuses[i]
    next if stage_status.nil? || stage_status == 0

    "stage #{i + 1} '#{Shellwords.join(command)}' exited with #{stage_status}"
  end

  detail =
    if !complete_status
      "pipeline reported #{stage_statuses.length} of #{commands.length} stage statuses; " \
        "shell exited with #{shell_status}"
    elsif failed_stages.any?
      "#{failed_stages.join(', ')}; shell exited with #{shell_status}"
    else
      "shell exited with #{shell_status} despite successful stage statuses"
    end

  msg = "failed to import stream: #{detail}"
  stderr_output = stderr_output.strip
  stderr_tail = stderr_output.bytesize > 4096 ? stderr_output.byteslice(-4096, 4096) : stderr_output
  msg = "#{msg}, stderr: #{stderr_tail}" unless stderr_tail.empty?

  raise CommandFailed, msg
end

#progress(msg) ⇒ Object (protected)



214
215
216
217
218
# File 'lib/osctld/container/dataset_builder.rb', line 214

def progress(msg)
  return unless @builder_opts[:cmd]

  @builder_opts[:cmd].send(:progress, msg)
end

#shift_dataset(ds, opts = {}) ⇒ Object

Parameters:

  • ds (OsCtl::Lib::Zfs::Dataset)
  • opts (Hash) (defaults to: {}) —

    options

Options Hash (opts):

  • :uid_map (OsCtl::Lib::IdMap)
  • :gid_map (OsCtl::Lib::IdMap)


169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/osctld/container/dataset_builder.rb', line 169

def shift_dataset(ds, opts = {})
  progress('Configuring UID/GID mapping')

  set_opts = []

  if opts[:uid_map]
    set_opts << "\"uidmap=#{opts[:uid_map].map(&:to_s).join(',')}\""
  end

  if opts[:gid_map]
    set_opts << "\"gidmap=#{opts[:gid_map].map(&:to_s).join(',')}\""
  end

  if set_opts.empty?
    raise 'provide uid_map or gid_map'
  end

  zfs(:unmount, nil, ds, valid_rcs: [1])
  zfs(:set, set_opts.join(' '), ds)

  5.times do |i|
    zfs(:mount, nil, ds)

    f = Tempfile.create(['.ugid-map-test'], ds.mountpoint)
    f.close

    st = File.stat(f.path)
    File.unlink(f.path)

    uid_ok = !opts[:uid_map] || st.uid == opts[:uid_map].ns_to_host(0)
    gid_ok = !opts[:gid_map] || st.gid == opts[:gid_map].ns_to_host(0)

    if uid_ok && gid_ok
      return # rubocop:disable Lint/NonLocalExitFromIterator
    end

    zfs(:unmount, nil, ds)
    sleep(1 + i)
  end

  raise 'unable to configure UID/GID mapping'
end