Class: Lxc

Inherits:
Object
  • Object
show all
Extended by:
Helpers
Includes:
Helpers
Defined in:
lib/vagabond/cookbooks/lxc/libraries/lxc.rb

Defined Under Namespace

Modules: Helpers Classes: CommandFailed, Pathname

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Helpers

command

Constructor Details

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

name

name of container

args

Argument hash

- :base_path -> path to container directory
- :dnsmasq_lease_file -> path to lease file
- :net_device -> network device to use within container for ssh connection


174
175
176
177
178
179
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 174

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]
end

Class Attribute Details

.base_pathObject

Returns the value of attribute base_path.



88
89
90
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 88

def base_path
  @base_path
end

.use_sudoObject

Returns the value of attribute use_sudo.



87
88
89
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 87

def use_sudo
  @use_sudo
end

Instance Attribute Details

#base_pathObject (readonly)

Returns the value of attribute base_path.



81
82
83
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 81

def base_path
  @base_path
end

#lease_fileObject (readonly)

Returns the value of attribute lease_file.



81
82
83
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 81

def lease_file
  @lease_file
end

#nameObject (readonly)

Returns the value of attribute name.



81
82
83
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 81

def name
  @name
end

#preferred_deviceObject (readonly)

Returns the value of attribute preferred_device.



81
82
83
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 81

def preferred_device
  @preferred_device
end

Class Method Details

.connection_alive?(ip) ⇒ Boolean

ip

IP address

Returns if IP address is alive

Returns:

  • (Boolean)


163
164
165
166
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 163

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

.exists?(name) ⇒ Boolean

name

name of container

Returns if container exists

Returns:

  • (Boolean)


120
121
122
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 120

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

.frozenObject

List frozen containers



114
115
116
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 114

def frozen
  full_list[:frozen]
end

.full_listObject

Return full container information list



151
152
153
154
155
156
157
158
159
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 151

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) ⇒ Object

name

Name of container

Returns information about given container



135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 135

def info(name)
  res = {:state => nil, :pid => nil}
  info = run_command("#{sudo}lxc-info -n #{name}").stdout.split("\n")
  if(info.first)
    parts = info.first.split(' ')
    res[:state] = parts.last.downcase.to_sym
    parts = info.last.split(' ')
    res[:pid] = parts.last.to_i
    res
  else
    res[:state] = :unknown
    res[:pid] = -1
  end
end

.listObject

List of containers



125
126
127
128
129
130
131
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 125

def list
  Dir.glob(File.join(base_path, '*')).map do |item|
    if(File.directory?(item) && File.exists?(File.join(item, 'config')))
      File.basename(item)
    end
  end.compact
end

.runningObject

List running containers



104
105
106
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 104

def running
  full_list[:running]
end

.stoppedObject

List stopped containers



109
110
111
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 109

def stopped
  full_list[:stopped]
end

.sudoObject



94
95
96
97
98
99
100
101
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 94

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

Instance Method Details

#container_command(cmd, retries = 1) ⇒ Object

cmd

Shell command string

retries

Number of retry attempts (1 second sleep interval)

Runs command in container via ssh



444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 444

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_configObject Also known as: config

Full path to container configuration file



308
309
310
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 308

def container_config
  container_path.join('config')
end

#container_ip(retries = 0, raise_on_fail = false) ⇒ Object

retries

Number of discovery attempt (3 second sleep intervals)

Returns container IP



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 203

def container_ip(retries=0, raise_on_fail=false)
  (retries.to_i + 1).times do
    ip = 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_pathObject Also known as: path

Full path to container



302
303
304
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 302

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

#container_rootfsObject Also known as: rootfs



313
314
315
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 313

def container_rootfs
  container_path.join('rootfs')
end

#detect_home(set_if_missing = false) ⇒ Object

Detect HOME environment variable. If not an acceptable value, set to /root or /tmp



429
430
431
432
433
434
435
436
437
438
439
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 429

def detect_home(set_if_missing=false)
  if(ENV['HOME'] && Pathname.new(ENV['HOME']).absolute?)
    ENV['HOME']
  else
    home = File.directory?('/root') && File.writable?('/root') ? '/root' : '/tmp'
    if(set_if_missing)
      ENV['HOME'] = home
    end
    home
  end
end

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



373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 373

def direct_container_command(command, args={})
  com = "#{sudo}ssh root@#{args[:ip] || container_ip} -i /opt/hw-lxc-config/id_rsa -oStrictHostKeyChecking=no '#{command}'"
  begin
    cmd = Mixlib::ShellOut.new(com,
      :live_stream => args[:live_stream],
      :timeout => args[:timeout] || 1200
    )
    cmd.run_command
    cmd.error!
    true
  rescue
    raise if args[:raise_on_failure]
    false
  end
end

#exists?Boolean

Returns if container exists

Returns:

  • (Boolean)


182
183
184
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 182

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

#expand_path(path) ⇒ Object



318
319
320
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 318

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

#freezeObject

Freeze the container



347
348
349
350
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 347

def freeze
  run_command("#{sudo}lxc-freeze -n #{name}")
  wait_for_state(:frozen)
end

#frozen?Boolean

Returns if container is frozen

Returns:

  • (Boolean)


197
198
199
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 197

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

#hw_detected_addressObject



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 259

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

#leased_addressObject

Container address via dnsmasq lease



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 241

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

#logObject



465
466
467
468
469
470
471
472
473
474
475
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 465

def log
  if(defined?(Chef::Log))
    Chef::Log
  else
    unless(@logger)
      require 'logger'
      @logger = Logger.new('/dev/null')
    end
    @logger
  end
end

#lxc_stored_addressObject

Container address via lxc config file



226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 226

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

#pidObject



326
327
328
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 326

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

#proc_detected_address(base = '/run/netns') ⇒ Object



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 279

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

#run_command(cmd, args = {}) ⇒ Object

Simple helper to shell out



391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 391

def run_command(cmd, args={})
  retries = args[:allow_failure_retry].to_i
  begin
    shlout = Mixlib::ShellOut.new(cmd, 
      :logger => defined?(Chef::Log) ? Chef::Log.logger : log,
      :live_stream => args[:livestream] ? nil : STDOUT,
      :timeout => args[:timeout] || 1200,
      :environment => {'HOME' => detect_home}
    )
    shlout.run_command
    shlout.error!
    shlout
  rescue Mixlib::ShellOut::ShellCommandFailed, CommandFailed, Mixlib::ShellOut::CommandTimeout
    if(retries > 0)
      log.warn "LXC run command failed: #{cmd}"
      log.warn "Retrying command. #{args[:allow_failure_retry].to_i - retries} of #{args[:allow_failure_retry].to_i} retries remain"
      sleep(0.3)
      retries -= 1
      retry
    elsif(args[:allow_failure])
      true
    else
      raise
    end
  end
end

#running?Boolean

Returns if container is running

Returns:

  • (Boolean)


187
188
189
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 187

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

#shutdownObject

Shutdown the container



359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 359

def shutdown
  run_command("#{sudo}lxc-shutdown -n #{name}")
  wait_for_state(:stopped, :timeout => 120)
  # 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?)
      raise "Failed to shutdown container: #{name}"
    end
  end
end

#start(*args) ⇒ Object

Start the container



331
332
333
334
335
336
337
338
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 331

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

#stateObject



322
323
324
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 322

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

#stopObject

Stop the container



341
342
343
344
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 341

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

#stopped?Boolean

Returns if container is stopped

Returns:

  • (Boolean)


192
193
194
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 192

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

#sudoObject



297
298
299
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 297

def sudo
  self.class.sudo
end

#unfreezeObject

Unfreeze the container



353
354
355
356
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 353

def unfreeze
  run_command("#{sudo}lxc-unfreeze -n #{name}")
  wait_for_state(:running)
end

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



418
419
420
421
422
423
424
425
# File 'lib/vagabond/cookbooks/lxc/libraries/lxc.rb', line 418

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