Class: OsCtld::Container::Importer

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

Overview

An interface for reading tar archives generated by OsCtl::Lib::Container::Exporter

Constant Summary collapse

BLOCK_SIZE =
32 * 1024

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pool, io, ct_id: nil, image_file: nil) ⇒ Importer

Returns a new instance of Importer.



15
16
17
18
19
20
# File 'lib/osctld/container/importer.rb', line 15

def initialize(pool, io, ct_id: nil, image_file: nil)
  @pool = pool
  @tar = Gem::Package::TarReader.new(io)
  @image_file = image_file
  @ct_id = ct_id
end

Instance Attribute Details

#image_fileObject (readonly, protected)

Returns the value of attribute image_file.



253
254
255
# File 'lib/osctld/container/importer.rb', line 253

def image_file
  @image_file
end

#metadataObject (readonly, protected)

Returns the value of attribute metadata.



253
254
255
# File 'lib/osctld/container/importer.rb', line 253

def 
  @metadata
end

#poolObject (readonly, protected)

Returns the value of attribute pool.



253
254
255
# File 'lib/osctld/container/importer.rb', line 253

def pool
  @pool
end

#tarObject (readonly, protected)

Returns the value of attribute tar.



253
254
255
# File 'lib/osctld/container/importer.rb', line 253

def tar
  @tar
end

Instance Method Details

#closeObject



247
248
249
# File 'lib/osctld/container/importer.rb', line 247

def close
  tar.close
end

#copy_file_to_disk(entry, dst) ⇒ Object (protected)

Copy file from the tar archive to disk

Parameters:

  • entry (Gem::Package::TarReader::Entry)
  • dst (String)


395
396
397
398
399
# File 'lib/osctld/container/importer.rb', line 395

def copy_file_to_disk(entry, dst)
  File.open(dst, 'w', entry.header.mode & 0o7777) do |df|
    df.write(entry.read(BLOCK_SIZE)) until entry.eof?
  end
end

#create_datasets(builder, accept_existing: false, properties: {}) ⇒ Object

Create the root and all descendants datasets

Parameters:

  • builder (Container::Builder)
  • accept_existing (Boolean) (defaults to: false)
  • properties (Hash<String, String>) (defaults to: {})


191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/osctld/container/importer.rb', line 191

def create_datasets(builder, accept_existing: false, properties: {})
  datasets(builder).each do |ds|
    next if accept_existing && ds.exist?

    builder.create_dataset(
      ds,
      mapping: builder.ctrc.map_mode == 'zfs',
      parents: ds.root?,
      properties:
    )
  end
end

#create_new_userUser (protected)

Returns:



291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/osctld/container/importer.rb', line 291

def create_new_user
  name = ct_id

  db = DB::Users.find(name, pool)
  return db if db

  # The user does not exist, create him
  Commands::User::Create.run!(
    pool: pool.name,
    name:,
    standalone: false
  )

  DB::Users.find(name, pool) || (raise 'expected user')
end

#ct_idObject



52
53
54
# File 'lib/osctld/container/importer.rb', line 52

def ct_id
  @ct_id || ['container']
end

#datasets(builder) ⇒ Array<OsCtl::Lib::Zfs::Dataset>

Parameters:

Returns:

  • (Array<OsCtl::Lib::Zfs::Dataset>)


236
237
238
239
240
241
242
243
244
245
# File 'lib/osctld/container/importer.rb', line 236

def datasets(builder)
  return @datasets if @datasets

  @datasets = [builder.ctrc.dataset] + ['datasets'].map do |name|
    OsCtl::Lib::Zfs::Dataset.new(
      File.join(builder.ctrc.dataset.name, name),
      base: builder.ctrc.dataset.name
    )
  end
end

#get_container_configHash

Returns:

  • (Hash)


121
122
123
# File 'lib/osctld/container/importer.rb', line 121

def get_container_config
  OsCtl::Lib::ConfigFile.load_yaml(tar.seek('config/container.yml', &:read))
end

#get_or_create_groupGroup

Load the group from the archive and register it, or create a new group

If a group with the same name already exists and all its parameters are the same, the existing group is returned. Otherwise an exception is raised.

Returns:



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/osctld/container/importer.rb', line 143

def get_or_create_group
  if has_group?
    name = ['group']

    db = DB::Groups.find(name, pool)
    grp = load_group

    if db.nil?
      # The group does not exist, create it
      Commands::Group::Create.run!(
        pool: pool.name,
        name: grp.name
      )

      return DB::Groups.find(name, pool) || (raise 'expected group')
    end

    db

  else
    DB::Groups.default(pool)
  end
end

#get_or_create_userUser

Load the user from the archive and register him, or create a new user

If a user with the same name already exists and all his parameters are the same, the existing user is returned. Otherwise an exception is raised.

Returns:



130
131
132
133
134
135
136
# File 'lib/osctld/container/importer.rb', line 130

def get_or_create_user
  if has_user?
    load_or_create_user
  else
    create_new_user
  end
end

#group_nameObject



44
45
46
# File 'lib/osctld/container/importer.rb', line 44

def group_name
  ['group']
end

#has_ct_id?Boolean

Returns:

  • (Boolean)


56
57
58
# File 'lib/osctld/container/importer.rb', line 56

def has_ct_id?
  !ct_id.nil?
end

#has_group?Boolean

Returns:

  • (Boolean)


48
49
50
# File 'lib/osctld/container/importer.rb', line 48

def has_group?
  !group_name.nil?
end

#has_user?Boolean

Returns:

  • (Boolean)


40
41
42
# File 'lib/osctld/container/importer.rb', line 40

def has_user?
  !user_name.nil?
end

#import_all_datasets(builder) ⇒ Object

Import all datasets

Parameters:



206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/osctld/container/importer.rb', line 206

def import_all_datasets(builder)
  case ['format']
  when 'zfs'
    import_streams(builder, datasets(builder))

  when 'tar'
    unpack_rootfs(builder)

  else
    raise "unsupported archive format '#{['format']}'"
  end
end

#import_root_dataset(builder) ⇒ Object

Import just the root dataset

Parameters:



221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/osctld/container/importer.rb', line 221

def import_root_dataset(builder)
  case ['format']
  when 'zfs'
    import_streams(builder, [datasets(builder).first])

  when 'tar'
    unpack_rootfs(builder)

  else
    raise "unsupported archive format '#{['format']}'"
  end
end

#import_stream(builder, ds, name, required) ⇒ Object (protected)



358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/osctld/container/importer.rb', line 358

def import_stream(builder, ds, name, required)
  raise 'image_file needs to be set for import_stream()' if image_file.nil?

  found = nil

  stream_names(name).each do |file, compression|
    tf = tar.find { |entry| entry.full_name == file }

    if tf.nil?
      tar.rewind
      next
    end

    found = [tf, compression]
    break
  end

  if found.nil?
    tar.rewind
    raise "unable to import: #{name} not found" if required

    return
  end

  entry, compression = found
  builder.from_tar_stream(image_file, entry.full_name, compression, ds)
  tar.rewind
end

#import_streams(builder, datasets) ⇒ Object (protected)

Load ZFS data streams from the archive and write them to appropriate datasets

Parameters:



312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/osctld/container/importer.rb', line 312

def import_streams(builder, datasets)
  datasets.each do |ds|
    import_stream(builder, ds, File.join(ds.relative_name, 'base'), true)
    import_stream(builder, ds, File.join(ds.relative_name, 'incremental'), false)
  end

  tar.seek('snapshots.yml') do |entry|
    snapshots = OsCtl::Lib::ConfigFile.load_yaml(entry.read)

    datasets.each do |ds|
      snapshots.each { |snap| zfs(:destroy, nil, "#{ds}@#{snap}") }
    end
  end
end

#install_user_hook_scripts(ct) ⇒ Object

Load user-defined script hooks from the archive and install them

Parameters:



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/osctld/container/importer.rb', line 169

def install_user_hook_scripts(ct)
  tar.each do |entry|
    next unless entry.full_name.start_with?('hooks/')

    name = entry.full_name[('hooks/'.length - 1)..]

    if entry.directory?
      FileUtils.mkdir_p(
        File.join(ct.user_hook_script_dir, name),
        mode: entry.header.mode & 0o7777
      )
    elsif entry.file?
      copy_file_to_disk(entry, File.join(ct.user_hook_script_dir, name))
    end
  end
end

#load_ct(opts) ⇒ Container

Create a new instance of OsCtld::Container as described by the tar archive

The returned CT is not registered in the internal database, it may even conflict with a CT already registered in the database.

Parameters:

  • opts (Hash)

    options

Options Hash (opts):

Returns:



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/osctld/container/importer.rb', line 103

def load_ct(opts)
  id = opts[:id] || ['container']
  user = opts[:user] || get_or_create_user
  group = opts[:group] || get_or_create_group
  ct_opts = opts[:ct_opts] || {}
  ct_opts[:load_from] = tar.seek('config/container.yml', &:read)

  Container.new(
    pool,
    id,
    user,
    group,
    opts[:dataset] || Container.default_dataset(pool, id),
    ct_opts
  )
end

#load_groupGroup?

Create a new instance of Group as described by the tar archive

The returned group is not registered in the internal database, it may even conflict with a group already registered in the database.

Returns:



80
81
82
83
84
85
86
87
88
89
# File 'lib/osctld/container/importer.rb', line 80

def load_group
  return unless has_group?

  Group.new(
    pool,
    ['group'],
    config: tar.seek('config/group.yml', &:read),
    devices: false
  )
end

#load_metadataObject

Load metadata describing the archive

Loading the metadata is the first thing that should be done, because all other methods depend on its result.



26
27
28
29
30
31
32
33
34
# File 'lib/osctld/container/importer.rb', line 26

def 
  ret = tar.seek('metadata.yml') do |entry|
    OsCtl::Lib::ConfigFile.load_yaml(entry.read)
  end
  raise 'metadata.yml not found' unless ret

  @metadata = ret
  ret
end

#load_or_create_userUser (protected)

Returns:



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/osctld/container/importer.rb', line 256

def load_or_create_user
  name = ['user']

  db = DB::Users.find(name, pool)
  u = load_user

  if db.nil?
    # The user does not exist, create him
    Commands::User::Create.run!(
      pool: pool.name,
      name: u.name,
      ugid: u.ugid,
      uid_map: u.uid_map.export,
      gid_map: u.gid_map.export
    )

    return DB::Users.find(name, pool) || (raise 'expected user')
  end

  # Free the newly allocated ugid, use ugid from the existing user
  UGidRegistry.remove(u.ugid) if u.ugid != db.ugid

  %i[uid_map gid_map].each do |param|
    mine = db.send(param)
    other = u.send(param)
    next if mine == other

    raise "user #{pool.name}:#{name} already exists: #{param} mismatch: " \
          "existing #{mine}, trying to import #{other}"
  end

  db
end

#load_userUser?

Create a new instance of User as described by the tar archive

The returned user is not registered in the internal database, it may even conflict with a user already registered in the database.

Returns:



65
66
67
68
69
70
71
72
73
# File 'lib/osctld/container/importer.rb', line 65

def load_user
  return unless has_user?

  User.new(
    pool,
    ['user'],
    config: tar.seek('config/user.yml', &:read)
  )
end

#stream_names(name) ⇒ Object (protected)



387
388
389
390
# File 'lib/osctld/container/importer.rb', line 387

def stream_names(name)
  base = File.join('rootfs', "#{name}.dat")
  [[base, :off], ["#{base}.gz", :gzip]]
end

#unpack_rootfs(builder) ⇒ Object (protected)

Parameters:



328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/osctld/container/importer.rb', line 328

def unpack_rootfs(builder)
  raise 'image_file needs to be set for import_stream()' if image_file.nil?

  # Ensure the dataset is mounted
  builder.ctrc.dataset.mount(recursive: true)

  # Create private/
  builder.setup_rootfs

  tf = tar.find { |entry| entry.full_name == 'rootfs/base.tar.gz' }
  raise 'rootfs archive not found' if tf.nil?

  tar.rewind

  commands = [
    ['tar', '-xOf', image_file, tf.full_name],
    ['tar', '--xattrs-include=security.capability', '--xattrs', '-xz', '-C', builder.ctrc.rootfs]
  ]

  command_string = commands.map { |c| c.join(' ') }.join(' | ')

  pid = Process.spawn(command_string)
  Process.wait(pid)

  return unless $?.exitstatus != 0

  raise "failed to unpack rootfs: command '#{command_string}' " \
        "exited with #{$?.exitstatus}"
end

#user_nameObject



36
37
38
# File 'lib/osctld/container/importer.rb', line 36

def user_name
  ['user']
end