Class: OsCtld::Container::Builder

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

Constant Summary collapse

ID_RX =
/^[a-z0-9_-]{1,100}$/i

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utils::Repository

#osctl_repo_get, #osctl_repo_ls

Methods included from Utils::SwitchUser

#ct_attach, #ct_control, #ct_exec, #ct_runscript, #ct_syscmd, #init_script, #unlink_file

Constructor Details

#initialize(ct, opts) ⇒ Builder

Returns a new instance of Builder

Parameters:

Options Hash (opts):

  • :cmd (Command::Base)
  • :reinstall (Boolean)


34
35
36
37
38
# File 'lib/osctld/container/builder.rb', line 34

def initialize(ct, opts)
  @ct = ct
  @opts = opts
  @errors = []
end

Instance Attribute Details

#ctObject (readonly)

Returns the value of attribute ct



28
29
30
# File 'lib/osctld/container/builder.rb', line 28

def ct
  @ct
end

#errorsObject (readonly)

Returns the value of attribute errors



28
29
30
# File 'lib/osctld/container/builder.rb', line 28

def errors
  @errors
end

Class Method Details

.create(pool, id, user, group, dataset = nil, opts = {}) ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/osctld/container/builder.rb', line 14

def self.create(pool, id, user, group, dataset = nil, opts = {})
  new(
    Container.new(
      pool,
      id,
      user,
      group,
      dataset || Container.default_dataset(pool, id),
      load: false
    ),
    opts
  )
end

Instance Method Details

#cleanup(opts = {}) ⇒ Object

Remove a partially created container when the building process failed

Parameters:

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

    options

Options Hash (opts):

  • :dataset (Boolean)

    destroy dataset or not



325
326
327
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
# File 'lib/osctld/container/builder.rb', line 325

def cleanup(opts = {})
  Console.remove(ct)
  zfs(:destroy, '-r', ct.dataset, valid_rcs: [1]) if opts[:dataset]

  syscmd("rm -rf #{ct.lxc_dir} #{ct.user_hook_script_dir}")
  File.unlink(ct.log_path) if File.exist?(ct.log_path)
  File.unlink(ct.config_path) if File.exist?(ct.config_path)

  DB::Containers.remove(ct)

  begin
    if ct.group.has_containers?(ct.user)
      CGroup.rmpath_all(ct.base_cgroup_path)

    else
      CGroup.rmpath_all(ct.group.full_cgroup_path(ct.user))
    end
  rescue SystemCallError
    # If some of the cgroups are busy, just leave them be
  end

  bashrc = File.join(ct.lxc_dir, '.bashrc')
  File.unlink(bashrc) if File.exist?(bashrc)

  grp_dir = ct.group.userdir(ct.user)

  if !ct.group.has_containers?(ct.user) && Dir.exist?(grp_dir)
    Dir.rmdir(grp_dir)
  end
end

#clear_snapshots(snaps) ⇒ Object



265
266
267
268
269
# File 'lib/osctld/container/builder.rb', line 265

def clear_snapshots(snaps)
  snaps.each do |snap|
    zfs(:destroy, nil, "#{ct.dataset}@#{snap}")
  end
end

#configure(distribution, version, arch) ⇒ Object



256
257
258
259
260
261
262
263
# File 'lib/osctld/container/builder.rb', line 256

def configure(distribution, version, arch)
  if @opts[:reinstall]
    ct.set(distribution: {name: distribution, version: version, arch: arch})

  else
    ct.configure(distribution, version, arch)
  end
end

#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)

    base snapshot

Returns:

  • (String)

    snapshot name



95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/osctld/container/builder.rb', line 95

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 -c #{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):

  • :mapping (Boolean)
  • :parents (Boolean)


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

def create_dataset(ds, opts = {})
  zfs_opts = {properties: {
    canmount: 'noauto',
  }}
  zfs_opts[:parents] = true if opts[:parents]
  zfs_opts[:properties].update({
    uidmap: ct.uid_map.map(&:to_s).join(','),
    gidmap: ct.gid_map.map(&:to_s).join(','),
  }) if opts[:mapping]

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

#create_root_dataset(opts = {}) ⇒ Object



68
69
70
71
# File 'lib/osctld/container/builder.rb', line 68

def create_root_dataset(opts = {})
  progress('Creating root dataset')
  create_dataset(ct.dataset, opts)
end

#exist?Boolean

Returns:

  • (Boolean)


64
65
66
# File 'lib/osctld/container/builder.rb', line 64

def exist?
  DB::Containers.contains?(ct.id, ct.pool)
end

#from_local_archive(template, opts = {}) ⇒ Object

Parameters:

  • template (String)

    path

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

    options

Options Hash (opts):

  • :distribution (String)
  • :version (String)


200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/osctld/container/builder.rb', line 200

def from_local_archive(template, opts = {})
  progress('Extracting template')
  syscmd("tar -xzf #{template} -C #{ct.rootfs}")

  shift_dataset

  distribution, version, arch = get_distribution_info(template)

  configure(
    opts[:distribution] || distribution,
    opts[:version] || version,
    opts[:arch] || arch
  )
end

#from_repo_archive(repo, tpl) ⇒ Object



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/osctld/container/builder.rb', line 153

def from_repo_archive(repo, tpl)
  r, w = IO.pipe
  tar = Process.spawn('tar', '-xz', '-C', ct.rootfs, in: r)
  r.close

  get = osctl_repo_get(repo, tpl, 'tar', w)

  _, tar_status = Process.wait2(tar)

  if get === false
    # format not found
    return false

  elsif tar_status.exitstatus != 0
    fail "unable to untar the template, exited with #{tar_status.exitstatus}"
  end

  true
end

#from_repo_stream(repo, tpl) ⇒ Object



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/osctld/container/builder.rb', line 173

def from_repo_stream(repo, tpl)
  get = nil
  wait_threads = nil

  Open3.pipeline_w(
    ['gunzip'],
    ['zfs', 'recv', '-F', ct.dataset.name]
  ) do |input, ts|
    get = osctl_repo_get(repo, tpl, 'zfs', input)
    wait_threads = ts
  end

  # format not found
  return false if get === false

  wait_threads.map(&:value).each do |st|
    next if st.exitstatus == 0
    fail "unable to recv the template, process exited with #{st.exitstatus}"
  end

  true
end

#from_repo_template(repo, tpl) ⇒ Object

Parameters:

Options Hash (tpl):

  • :vendor (String)
  • :variant (String)
  • :arch (String)
  • :distribution (String)
  • :version (String)


133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/osctld/container/builder.rb', line 133

def from_repo_template(repo, tpl)
  progress('Fetching and applying template')

  created = %i(from_repo_stream from_repo_archive).detect do |m|
    method(m).call(repo, tpl)
  end

  unless created
    raise TemplateNotFound, 'no supported template format available'
  end

  shift_dataset

  configure(
    tpl[:distribution],
    tpl[:version],
    tpl[:arch]
  )
end

#from_stream(ds = nil) ⇒ Object



215
216
217
218
219
220
221
222
223
224
225
# File 'lib/osctld/container/builder.rb', line 215

def from_stream(ds = nil)
  progress('Writing template stream')

  IO.popen("exec zfs recv -F #{ds || ct.dataset}", 'r+') do |io|
    yield(io)
  end

  if $?.exitstatus != 0
    fail "zfs recv failed with exit status #{$?.exitstatus}"
  end
end

#get_distribution_info(template) ⇒ Object



356
357
358
359
# File 'lib/osctld/container/builder.rb', line 356

def get_distribution_info(template)
  distribution, version, arch, *_ = File.basename(template).split('-')
  [distribution, version, arch]
end

#groupObject



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

def group
  ct.group
end

#monitorObject



317
318
319
# File 'lib/osctld/container/builder.rb', line 317

def monitor
  Monitor::Master.monitor(ct)
end

#poolObject



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

def pool
  ct.pool
end

#progress(msg) ⇒ Object (protected)



362
363
364
365
# File 'lib/osctld/container/builder.rb', line 362

def progress(msg)
  return unless @opts[:cmd]
  @opts[:cmd].send(:progress, msg)
end

#registerObject



306
307
308
309
310
311
312
313
314
315
# File 'lib/osctld/container/builder.rb', line 306

def register
  DB::Containers.sync do
    if DB::Containers.contains?(ct.id, ct.pool)
      false
    else
      DB::Containers.add(ct)
      true
    end
  end
end

#setup_ct_dirObject



110
111
112
113
114
# File 'lib/osctld/container/builder.rb', line 110

def setup_ct_dir
  # Chown to 0:0, zfs will shift it using the mapping
  File.chown(0, 0, ct.dir)
  File.chmod(0770, ct.dir)
end

#setup_log_fileObject



292
293
294
295
296
297
# File 'lib/osctld/container/builder.rb', line 292

def setup_log_file
  progress('Preparing log file')
  File.open(ct.log_path, 'w').close
  File.chmod(0660, ct.log_path)
  File.chown(0, ct.user.ugid, ct.log_path)
end

#setup_lxc_configsObject



287
288
289
290
# File 'lib/osctld/container/builder.rb', line 287

def setup_lxc_configs
  progress('Generating LXC configuration')
  ct.lxc_config.configure
end

#setup_lxc_homeObject



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/osctld/container/builder.rb', line 271

def setup_lxc_home
  progress('Configuring LXC home')

  unless ct.group.setup_for?(ct.user)
    dir = ct.group.userdir(ct.user)

    FileUtils.mkdir_p(dir, mode: 0751)
    File.chown(0, ct.user.ugid, dir)
  end

  Dir.mkdir(ct.lxc_dir, 0750)
  File.chown(0, ct.user.ugid, ct.lxc_dir)

  ct.configure_bashrc
end

#setup_rootfsObject



116
117
118
119
120
121
122
123
124
# File 'lib/osctld/container/builder.rb', line 116

def setup_rootfs
  if Dir.exist?(ct.rootfs)
    File.chmod(0755, ct.rootfs)
  else
    Dir.mkdir(ct.rootfs, 0755)
  end

  File.chown(0, 0, ct.rootfs)
end

#setup_user_hook_script_dirObject



299
300
301
302
303
304
# File 'lib/osctld/container/builder.rb', line 299

def setup_user_hook_script_dir
  return if Dir.exist?(ct.user_hook_script_dir)

  progress('Preparing user script hook dir')
  Dir.mkdir(ct.user_hook_script_dir, 0700)
end

#shift_datasetObject



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/osctld/container/builder.rb', line 227

def shift_dataset
  progress('Configuring UID/GID mapping')

  zfs(:unmount, nil, ct.dataset)
  zfs(
    :set,
    "uidmap=\"#{ct.uid_map.map(&:to_s).join(',')}\" "+
    "gidmap=\"#{ct.gid_map.map(&:to_s).join(',')}\"",
    ct.dataset
  )

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

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

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

    return if st.uid == ct.root_host_uid && st.gid == ct.root_host_gid

    zfs(:unmount, nil, ct.dataset)
    sleep(1 + i)
  end

  fail 'unable to configure UID/GID mapping'
end

#userObject



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

def user
  ct.user
end

#valid?Boolean

Returns:

  • (Boolean)


52
53
54
55
56
57
58
59
60
61
62
# File 'lib/osctld/container/builder.rb', line 52

def valid?
  if ID_RX !~ ct.id
    errors << "invalid ID, allowed characters: #{ID_RX.source}"
  end

  if !ct.dataset.on_pool?(ct.pool.name)
    errors << "dataset #{ct.dataset} does not belong to pool #{ct.pool.name}"
  end

  errors.empty?
end