Class: Lxc

Inherits:
Object
  • Object
show all
Extended by:
Helpers
Includes:
Helpers
Defined in:
lib/elecksee.rb,
lib/elecksee/lxc.rb,
lib/elecksee/clone.rb,
lib/elecksee/helpers.rb,
lib/elecksee/storage.rb,
lib/elecksee/ephemeral.rb,
lib/elecksee/helpers/copies.rb,
lib/elecksee/helpers/options.rb,
lib/elecksee/lxc_file_config.rb,
lib/elecksee/storage/overlay_mount.rb,
lib/elecksee/storage/virtual_device.rb,
lib/elecksee/storage/overlay_directory.rb

Overview

LXC interface

Defined Under Namespace

Modules: Helpers, Storage Classes: Clone, CommandFailed, CommandResult, Ephemeral, FileConfig, Pathname, Timeout

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Helpers

child_process_command, detect_home, log, mixlib_shellout_command, run_command, sudo

Constructor Details

#initialize(name, args = {}) ⇒ Lxc

Create new instance

Parameters:

  • name (String)

    container name

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

Options Hash (args):

  • :base_path (String)

    path to container

  • :dnsmasq_lease_file (String)

    path to lease file

  • :net_device (String)

    network device within container for ssh connection

  • :ssh_key (String)

    path to ssh key

  • :ssh_password (String)

    ssh password

  • :ssh_user (String)

    ssh user



171
172
173
174
175
176
177
178
179
# File 'lib/elecksee/lxc.rb', line 171

def initialize(name, args={})
  @name = name
  @base_path = args[:base_path] || self.class.base_path
  @lease_file = args[:dnsmasq_lease_file] || '/var/lib/misc/dnsmasq.leases'
  @preferred_device = args[:net_device]
  @ssh_key = args.fetch(:ssh_key, self.class.default_ssh_key)
  @ssh_password = args.fetch(:ssh_password, self.class.default_ssh_password)
  @ssh_user = args.fetch(:ssh_user, self.class.default_ssh_user)
end

Class Attribute Details

.base_pathString

Returns base path for containers.

Returns:

  • (String)

    base path for containers



47
48
49
# File 'lib/elecksee/lxc.rb', line 47

def base_path
  @base_path
end

.container_command_viaSymbol

Returns default command method.

Returns:

  • (Symbol)

    default command method



57
58
59
# File 'lib/elecksee/lxc.rb', line 57

def container_command_via
  @container_command_via
end

.default_ssh_keyString, NilClass

Returns path to default ssh key.

Returns:

  • (String, NilClass)

    path to default ssh key



51
52
53
# File 'lib/elecksee/lxc.rb', line 51

def default_ssh_key
  @default_ssh_key
end

.default_ssh_passwordString, NilClass

Returns default ssh password.

Returns:

  • (String, NilClass)

    default ssh password



53
54
55
# File 'lib/elecksee/lxc.rb', line 53

def default_ssh_password
  @default_ssh_password
end

.default_ssh_userString, NilClass

Returns default ssh user.

Returns:

  • (String, NilClass)

    default ssh user



55
56
57
# File 'lib/elecksee/lxc.rb', line 55

def default_ssh_user
  @default_ssh_user
end

.shellout_helperSymbol

Returns :mixlib_shellout or :childprocess.

Returns:

  • (Symbol)

    :mixlib_shellout or :childprocess



49
50
51
# File 'lib/elecksee/lxc.rb', line 49

def shellout_helper
  @shellout_helper
end

.use_sudoTruthy, String

Returns use sudo when required (set to string for custom sudo command).

Returns:

  • (Truthy, String)

    use sudo when required (set to string for custom sudo command)



45
46
47
# File 'lib/elecksee/lxc.rb', line 45

def use_sudo
  @use_sudo
end

Instance Attribute Details

#base_pathString (readonly)

Returns base path of container.

Returns:

  • (String)

    base path of container



28
29
30
# File 'lib/elecksee/lxc.rb', line 28

def base_path
  @base_path
end

#lease_fileString (readonly)

Returns path to dnsmasq lease file.

Returns:

  • (String)

    path to dnsmasq lease file



30
31
32
# File 'lib/elecksee/lxc.rb', line 30

def lease_file
  @lease_file
end

#nameString (readonly)

Returns name of container.

Returns:

  • (String)

    name of container



26
27
28
# File 'lib/elecksee/lxc.rb', line 26

def name
  @name
end

#preferred_deviceString (readonly)

Returns network device to use for ssh connection.

Returns:

  • (String)

    network device to use for ssh connection



32
33
34
# File 'lib/elecksee/lxc.rb', line 32

def preferred_device
  @preferred_device
end

#ssh_keyString, NilClass

Returns path to default ssh key.

Returns:

  • (String, NilClass)

    path to default ssh key



34
35
36
# File 'lib/elecksee/lxc.rb', line 34

def ssh_key
  @ssh_key
end

#ssh_passwordString, NilClass

Returns ssh password.

Returns:

  • (String, NilClass)

    ssh password



36
37
38
# File 'lib/elecksee/lxc.rb', line 36

def ssh_password
  @ssh_password
end

#ssh_userString, NilClass

Returns ssh user.

Returns:

  • (String, NilClass)

    ssh user



38
39
40
# File 'lib/elecksee/lxc.rb', line 38

def ssh_user
  @ssh_user
end

Class Method Details

.connection_alive?(ip) ⇒ TrueClass, FalseClass

IP address is currently active

Parameters:

  • ip (String)

Returns:

  • (TrueClass, FalseClass)


155
156
157
158
# File 'lib/elecksee/lxc.rb', line 155

def connection_alive?(ip)
  %x{ping -c 1 -W 1 #{ip}}
  $?.exitstatus == 0
end

.exists?(name) ⇒ TrueClass, FalseClass

Container currently exists

Parameters:

  • name (String)

    name of container

Returns:

  • (TrueClass, FalseClass)


99
100
101
# File 'lib/elecksee/lxc.rb', line 99

def exists?(name)
  list.include?(name)
end

.frozenArray<String>

Currently frozen container names

Returns:

  • (Array<String>)


91
92
93
# File 'lib/elecksee/lxc.rb', line 91

def frozen
  full_list[:frozen]
end

.full_listHash

Full list of containers grouped by state

Returns:

  • (Hash)


141
142
143
144
145
146
147
148
149
# File 'lib/elecksee/lxc.rb', line 141

def full_list
  res = {}
  list.each do |item|
    item_info = info(item)
    res[item_info[:state]] ||= []
    res[item_info[:state]] << item
  end
  res
end

.info(name) ⇒ Hash

Information available for given container

Parameters:

  • name (String)

    name of container

Returns:

  • (Hash)


115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/elecksee/lxc.rb', line 115

def info(name)
  if(exists?(name))
    info = run_command("#{sudo}lxc-info -n #{name}", :allow_failure_retry => 3, :allow_failure => true)
  end
  if(info)
    Hash[
      info.stdout.split("\n").map do |string|
        string.split(': ').map(&:strip)
      end.map do |key, value|
        key = key.tr(' ', '_').downcase.to_sym
        if(key == :state)
          value = value.downcase.to_sym
        elsif(value.to_i.to_s == value)
          value = value.to_i
        end
        [key, value]
      end
    ]
  else
    Hash[:state, :unknown, :pid, -1]
  end
end

.listArray<String>

List of all containers

Returns:

  • (Array<String>)

    container names



106
107
108
109
# File 'lib/elecksee/lxc.rb', line 106

def list
  run_command('lxc-ls', :sudo => true).
    stdout.split(/\s/).map(&:strip).compact
end

.runningArray<String>

Currently running container names

Returns:

  • (Array<String>)


77
78
79
# File 'lib/elecksee/lxc.rb', line 77

def running
  full_list[:running]
end

.stoppedArray<String>

Currently stopped container names

Returns:

  • (Array<String>)


84
85
86
# File 'lib/elecksee/lxc.rb', line 84

def stopped
  full_list[:stopped]
end

.sudoString

Returns sudo command.

Returns:

  • (String)

    sudo command



60
61
62
63
64
65
66
67
# File 'lib/elecksee/lxc.rb', line 60

def sudo
  case use_sudo
  when TrueClass
    'sudo '
  when String
    "#{use_sudo} "
  end
end

Instance Method Details

#connection(args = {}) ⇒ Rye::Box

Provide connection to running container

Returns:



464
465
466
467
468
469
470
471
472
473
# File 'lib/elecksee/lxc.rb', line 464

def connection(args={})
  Rye::Box.new(args.fetch(:ip, container_ip(3)),
    :user => ssh_user,
    :password => ssh_password,
    :password_prompt => false,
    :keys => [ssh_key],
    :safe => false,
    :paranoid => false
  )
end

#container_command(cmd, retries = 1) ⇒ CommandResult

Note:

retries are over 1 second intervals

Run command within container

Parameters:

  • cmd (String)

    command to run

  • retries (Integer) (defaults to: 1)

    number of retry attempts

Returns:



566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
# File 'lib/elecksee/lxc.rb', line 566

def container_command(cmd, retries=1)
  begin
    detect_home(true)
    direct_container_command(cmd,
      :ip => container_ip(5),
      :live_stream => STDOUT,
      :raise_on_failure => true
    )
  rescue => e
    if(retries.to_i > 0)
      log.info "Encountered error running container command (#{cmd}): #{e}"
      log.info "Retrying command..."
      retries = retries.to_i - 1
      sleep(1)
      retry
    else
      raise e
    end
  end
end

#container_configPathname Also known as: config

Returns path to configuration file.

Returns:

  • (Pathname)

    path to configuration file



330
331
332
# File 'lib/elecksee/lxc.rb', line 330

def container_config
  container_path.join('config')
end

#container_ip(retries = 0, raise_on_fail = false) ⇒ String, NilClass

Note:

retries are executed on 3 second sleep intervals

Current IP address of container

Parameters:

  • retries (Integer) (defaults to: 0)

    number of times to retry discovery

  • raise_on_fail (TrueClass, FalseClass) (defaults to: false)

    raise exception on failure

Returns:

  • (String, NilClass)

    IP address



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/elecksee/lxc.rb', line 207

def container_ip(retries=0, raise_on_fail=false)
  (retries.to_i + 1).times do
    ip = info_detected_address ||
      proc_detected_address ||
      hw_detected_address ||
      leased_address ||
      lxc_stored_address
    if(ip.is_a?(Array))
      # Filter any found loopbacks
      ip.delete_if{|info| info[:device].start_with?('lo') }
      ip = ip.detect do |info|
        if(@preferred_device)
          info[:device] == @preferred_device
        else
          true
        end
      end
      ip = ip[:address] if ip
    end
    return ip if ip && self.class.connection_alive?(ip)
    log.warn "LXC IP discovery: Failed to detect live IP"
    sleep(3) if retries > 0
  end
  raise "Failed to detect live IP address for container: #{name}" if raise_on_fail
end

#container_pathPathname Also known as: path

Returns path to container.

Returns:



324
325
326
# File 'lib/elecksee/lxc.rb', line 324

def container_path
  Pathname.new(@base_path).join(name)
end

#container_rootfsPathname Also known as: rootfs

Returns path to rootfs.

Returns:



336
337
338
339
340
341
342
343
# File 'lib/elecksee/lxc.rb', line 336

def container_rootfs
  if(File.exists?(config))
    r_path = File.readlines(config).detect do |line|
      line.start_with?('lxc.rootfs')
    end.to_s.split('=').last.to_s.strip
  end
  r_path.to_s.empty? ? container_path.join('rootfs') : Pathname.new(r_path)
end

#destroyself

Destroy the container

Returns:

  • (self)


428
429
430
431
432
433
434
# File 'lib/elecksee/lxc.rb', line 428

def destroy
  unless stopped?
    stop
  end
  run_command("lxc-destroy -n #{name}", :sudo => true)
  self
end

#direct_container_command(command, args = {}) ⇒ CommandResult Also known as: knife_container

Execute command within running container

Parameters:

  • command (String)
  • args (Hash) (defaults to: {})

Options Hash (args):

  • :timeout (Integer)
  • :live_stream (TrueClass, FalseClass)
  • :raise_on_failure (TrueClass, FalseClass)

Returns:



483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
# File 'lib/elecksee/lxc.rb', line 483

def direct_container_command(command, args={})
  if(args.fetch(:run_as, Lxc.container_command_via).to_sym == :ssh)
    begin
      result = connection(args).execute command
      CommandResult.new(result)
    rescue Rye::Err => e
      if(args[:raise_on_failure])
        raise CommandFailed.new(
          "Command failed: #{command}",
          CommandResult.new(e)
        )
      else
        false
      end
    end
  else
    command(
      "lxc-attach -n #{name} #{command}",
      args.merge(:sudo => true)
    )
  end
end

#execute(command, opts = {}) ⇒ CommandResult

Note:

container must be stopped

Execute command within the container

Parameters:

  • command (String)

    command to execute

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

    options passed to #tmp_execute_script

Returns:



442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/elecksee/lxc.rb', line 442

def execute(command, opts={})
  if(stopped?)
    cmd = Shellwords.split(command)
    result = nil
    begin
      tmp_execute_script(command, opts) do |script_path|
        result = run_command("lxc-execute -n #{name} -- #{script_path}", :sudo => true)
      end
    rescue => e
      if(e.result.stderr.downcase.include?('failed to find an lxc-init'))
        $stderr.puts "ERROR: Missing `lxc-init` installation on container (#{name}). Install lxc-init on container before using `#execute`!"
      end
      raise
    end
  else
    raise "Cannot execute against running container (#{name})"
  end
end

#exists?TrueClass, FalseClass

Returns container exists.

Returns:

  • (TrueClass, FalseClass)

    container exists



182
183
184
# File 'lib/elecksee/lxc.rb', line 182

def exists?
  self.class.exists?(name)
end

#expand_path(path) ⇒ Pathname

Expand path within containers rootfs

Parameters:

  • path (String)

    relative path

Returns:

  • (Pathname)

    full path within container



350
351
352
# File 'lib/elecksee/lxc.rb', line 350

def expand_path(path)
  container_rootfs.join(path)
end

#freezeself

Freeze the container

Returns:

  • (self)


390
391
392
393
394
# File 'lib/elecksee/lxc.rb', line 390

def freeze
  run_command("lxc-freeze -n #{name}", :sudo => true)
  wait_for_state(:frozen)
  self
end

#frozen?TrueClass, FalseClass

Returns container is currently frozen.

Returns:

  • (TrueClass, FalseClass)

    container is currently frozen



197
198
199
# File 'lib/elecksee/lxc.rb', line 197

def frozen?
  self.class.info(name)[:state] == :frozen
end

#hw_detected_addressString, NilClass

Container address discovered via device

Returns:

  • (String, NilClass)

    IP address



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/elecksee/lxc.rb', line 281

def hw_detected_address
  if(container_config.readable?)
    hw = File.readlines(container_config).detect{|line|
      line.include?('hwaddr')
    }.to_s.split('=').last.to_s.downcase
    if(File.exists?(container_config) && !hw.empty?)
      running? # need to do a list!
      ip = File.readlines('/proc/net/arp').detect{|line|
        line.downcase.include?(hw)
      }.to_s.split(' ').first.to_s.strip
      if(ip.to_s.empty?)
        nil
      else
        log.info "LXC Discovery: Found container address via HW addr: #{ip}"
        ip
      end
    end
  end
end

#info_detected_addressString, NilClass

Container address discovered via info

Returns:

  • (String, NilClass)

    IP address



274
275
276
# File 'lib/elecksee/lxc.rb', line 274

def info_detected_address
  self.class.info(name)[:ip]
end

#leased_addressString, NilClass

Container address discovered via dnsmasq lease

Returns:

  • (String, NilClass)

    IP address



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/elecksee/lxc.rb', line 253

def leased_address
  ip = nil
  if(File.exists?(@lease_file))
    leases = File.readlines(@lease_file).map{|line| line.split(' ')}
    leases.each do |lease|
      if(lease.include?(name))
        ip = lease[2]
      end
    end
  end
  if(ip.to_s.empty?)
    nil
  else
    log.info "LXC Discovery: Found container address via DHCP lease: #{ip}"
    ip
  end
end

#lxc_stored_addressString, NilClass

Container address defined within the container’s config file

Returns:

  • (String, NilClass)

    IP address



236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/elecksee/lxc.rb', line 236

def lxc_stored_address
  if(File.exists?(container_config))
    ip = File.readlines(container_config).detect{|line|
      line.include?('ipv4')
    }.to_s.split('=').last.to_s.strip
    if(ip.to_s.empty?)
      nil
    else
      log.info "LXC Discovery: Found container address via storage: #{ip}"
      ip
    end
  end
end

#pidInteger, Symbol

Returns process ID or :unknown.

Returns:

  • (Integer, Symbol)

    process ID or :unknown



360
361
362
# File 'lib/elecksee/lxc.rb', line 360

def pid
  self.class.info(name)[:pid]
end

#proc_detected_address(base = '/run/netns') ⇒ String, NilClass

Container address discovered via process

Parameters:

  • base (String) (defaults to: '/run/netns')

    path to netns

Returns:

  • (String, NilClass)

    IP address



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/elecksee/lxc.rb', line 305

def proc_detected_address(base='/run/netns')
  if(pid != -1)
    Dir.mktmpdir do |t_dir|
      name = File.basename(t_dir)
      path = File.join(base, name)
      system("#{sudo}mkdir -p #{base}")
      system("#{sudo}ln -s /proc/#{pid}/ns/net #{path}")
      res = %x{#{sudo}ip netns exec #{name} ip -4 addr show scope global | grep inet}
      system("#{sudo}rm -f #{path}")
      ips = res.split("\n").map do |line|
        parts = line.split(' ')
        {:address => parts[1].to_s.sub(%r{/.+$}, ''), :device => parts.last}
      end
      ips.empty? ? nil : ips
    end
  end
end

#running?TrueClass, FalseClass

Returns container is currently running.

Returns:

  • (TrueClass, FalseClass)

    container is currently running



187
188
189
# File 'lib/elecksee/lxc.rb', line 187

def running?
  self.class.info(name)[:state] == :running
end

#shutdownself

Shutdown the container

Returns:

  • (self)


408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/elecksee/lxc.rb', line 408

def shutdown
  # This block is for fedora/centos/anyone else that does not like lxc-shutdown
  if(running?)
    container_command('shutdown -h now')
    wait_for_state(:stopped, :timeout => 120)
    # If still running here, something is wrong
    if(running?)
      run_command("lxc-stop -n #{name}", :sudo => true)
      wait_for_state(:stopped, :timeout => 120)
      if(running?)
        raise "Failed to shutdown container: #{name}"
      end
    end
  end
  self
end

#start(*args) ⇒ self

Start the container

Parameters:

  • args (Symbol)

    argument list (:no_daemon to foreground)

Returns:

  • (self)


368
369
370
371
372
373
374
375
376
# File 'lib/elecksee/lxc.rb', line 368

def start(*args)
  if(args.include?(:no_daemon))
    run_command("lxc-start -n #{name}", :sudo => true)
  else
    run_command("lxc-start -n #{name} -d", :sudo => true)
    wait_for_state(:running)
  end
  self
end

#stateSymbol

Returns current state.

Returns:

  • (Symbol)

    current state



355
356
357
# File 'lib/elecksee/lxc.rb', line 355

def state
  self.class.info(name)[:state]
end

#stopself

Stop the container

Returns:

  • (self)


381
382
383
384
385
# File 'lib/elecksee/lxc.rb', line 381

def stop
  run_command("lxc-stop -n #{name}", :allow_failure_retry => 3, :sudo => true)
  wait_for_state([:stopped, :unknown])
  self
end

#stopped?TrueClass, FalseClass

Returns container is currently stopped.

Returns:

  • (TrueClass, FalseClass)

    container is currently stopped



192
193
194
# File 'lib/elecksee/lxc.rb', line 192

def stopped?
  self.class.info(name)[:state] == :stopped
end

#tmp_execute_script(command, opts) {|command_script| ... } ⇒ Object

Write command to temporary file with networking enablement wrapper and yield relative path

Parameters:

  • command (String)
  • opts (Hash)

Yields:

  • block to execute

Yield Parameters:

  • command_script (String)

    path to file

Returns:

  • (Object)

    result of block



534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
# File 'lib/elecksee/lxc.rb', line 534

def tmp_execute_script(command, opts)
  script_path = "tmp/#{SecureRandom.uuid}"
  File.open(rootfs.join(script_path), 'w') do |file|
    file.puts '#!/bin/sh'
    unless(opts[:networking] == false)
      file.write <<-EOS
/etc/network/if-pre-up.d/bridge > /dev/null 2>&1
ifdown eth0 > /dev/null 2>&1
ifup eth0 > /dev/null 2>&1
EOS
    end
    file.puts command
    file.puts "RESULT=$?"
    unless(opts[:networking] == false)
      file.puts "ifdown eth0 > /dev/null 2>&1"
    end
    file.puts "exit $RESULT"
  end
  FileUtils.chmod(0755, rootfs.join(script_path))
  begin
    yield "/#{script_path}"
  ensure
    FileUtils.rm(rootfs.join(script_path))
  end
end

#unfreezeself

Unfreeze the container

Returns:

  • (self)


399
400
401
402
403
# File 'lib/elecksee/lxc.rb', line 399

def unfreeze
  run_command("lxc-unfreeze -n #{name}", :sudo => true)
  wait_for_state(:running)
  self
end

#wait_for_state(desired_state, args = {}) ⇒ self

Wait for container to reach given state

Parameters:

  • desired_state (Symbol, Array<Symbol>)
  • args (Hash) (defaults to: {})

Options Hash (args):

  • :timeout (Integer)
  • :sleep_interval (Numeric)

Returns:

  • (self)


514
515
516
517
518
519
520
521
522
523
# File 'lib/elecksee/lxc.rb', line 514

def wait_for_state(desired_state, args={})
  args[:sleep_interval] ||= 1.0
  wait_total = 0.0
  desired_state = [desired_state].flatten.compact.map(&:to_sym)
  until(desired_state.include?(state) || (args[:timeout].to_i > 0 && wait_total.to_i > args[:timeout].to_i))
    sleep(args[:sleep_interval])
    wait_total += args[:sleep_interval]
  end
  self
end