elecksee 1.0.22 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/elecksee/lxc.rb CHANGED
@@ -1,8 +1,9 @@
1
- require 'elecksee/helpers/base'
1
+ require 'elecksee'
2
2
  require 'securerandom'
3
3
  require 'shellwords'
4
4
  require 'pathname'
5
5
  require 'tmpdir'
6
+ require 'rye'
6
7
 
7
8
  class Lxc
8
9
 
@@ -10,6 +11,10 @@ class Lxc
10
11
  # begin with '/', and that's dumb. So we'll make our own Pathname,
11
12
  # with a #join that uses File
12
13
  class Pathname < ::Pathname
14
+ # Join arguments using ::File.join
15
+ #
16
+ # @param args [String] argument list
17
+ # @return [String]
13
18
  def join(*args)
14
19
  self.class.new(::File.join(self.to_path, *args))
15
20
  end
@@ -17,16 +22,39 @@ class Lxc
17
22
 
18
23
  include Helpers
19
24
 
20
- attr_reader :name, :base_path, :lease_file, :preferred_device
25
+ # @return [String] name of container
26
+ attr_reader :name
27
+ # @return [String] base path of container
28
+ attr_reader :base_path
29
+ # @return [String] path to dnsmasq lease file
30
+ attr_reader :lease_file
31
+ # @return [String] network device to use for ssh connection
32
+ attr_reader :preferred_device
33
+ # @return [String, NilClass] path to default ssh key
34
+ attr_accessor :ssh_key
35
+ # @return [String, NilClass] ssh password
36
+ attr_accessor :ssh_password
37
+ # @return [String, NilClass] ssh user
38
+ attr_accessor :ssh_user
21
39
 
22
40
  class << self
23
41
 
24
42
  include Helpers
25
43
 
44
+ # @return [Truthy, String] use sudo when required (set to string for custom sudo command)
26
45
  attr_accessor :use_sudo
46
+ # @return [String] base path for containers
27
47
  attr_accessor :base_path
48
+ # @return [Symbol] :mixlib_shellout or :childprocess
28
49
  attr_accessor :shellout_helper
29
-
50
+ # @return [String, NilClass] path to default ssh key
51
+ attr_accessor :default_ssh_key
52
+ # @return [String, NilClass] default ssh password
53
+ attr_accessor :default_ssh_password
54
+ # @return [String, NilClass] default ssh user
55
+ attr_accessor :default_ssh_user
56
+
57
+ # @return [String] sudo command
30
58
  def sudo
31
59
  case use_sudo
32
60
  when TrueClass
@@ -36,44 +64,56 @@ class Lxc
36
64
  end
37
65
  end
38
66
 
67
+ # @return [String] base path for containers
39
68
  def base_path
40
69
  @base_path || '/var/lib/lxc'
41
70
  end
42
71
 
43
- # List running containers
72
+ # Currently running container names
73
+ #
74
+ # @return [Array<String>]
44
75
  def running
45
76
  full_list[:running]
46
77
  end
47
78
 
48
- # List stopped containers
79
+ # Currently stopped container names
80
+ #
81
+ # @return [Array<String>]
49
82
  def stopped
50
83
  full_list[:stopped]
51
84
  end
52
85
 
53
- # List frozen containers
86
+ # Currently frozen container names
87
+ #
88
+ # @return [Array<String>]
54
89
  def frozen
55
90
  full_list[:frozen]
56
91
  end
57
92
 
58
- # name:: name of container
59
- # Returns if container exists
93
+ # Container currently exists
94
+ #
95
+ # @param name [String] name of container
96
+ # @return [TrueClass, FalseClass]
60
97
  def exists?(name)
61
98
  list.include?(name)
62
99
  end
63
100
 
64
- # List of containers
101
+ # List of all containers
102
+ #
103
+ # @return [Array<String>] container names
65
104
  def list
66
- Dir.glob(File.join(base_path, '*')).map do |item|
67
- if(File.directory?(item) && File.exists?(File.join(item, 'config')))
68
- File.basename(item)
69
- end
70
- end.compact
105
+ run_command('lxc-ls', :sudo => true).
106
+ stdout.split(/\s/).map(&:strip).compact
71
107
  end
72
108
 
73
- # name:: Name of container
74
- # Returns information about given container
109
+ # Information available for given container
110
+ #
111
+ # @param name [String] name of container
112
+ # @return [Hash]
75
113
  def info(name)
76
- info = run_command("#{sudo}lxc-info -n #{name}", :allow_failure_retry => 3, :allow_failure => true)
114
+ if(exists?(name))
115
+ info = run_command("#{sudo}lxc-info -n #{name}", :allow_failure_retry => 3, :allow_failure => true)
116
+ end
77
117
  if(info)
78
118
  Hash[
79
119
  info.stdout.split("\n").map do |string|
@@ -93,7 +133,9 @@ class Lxc
93
133
  end
94
134
  end
95
135
 
96
- # Return full container information list
136
+ # Full list of containers grouped by state
137
+ #
138
+ # @return [Hash]
97
139
  def full_list
98
140
  res = {}
99
141
  list.each do |item|
@@ -104,51 +146,69 @@ class Lxc
104
146
  res
105
147
  end
106
148
 
107
- # ip:: IP address
108
- # Returns if IP address is alive
149
+ # IP address is currently active
150
+ #
151
+ # @param ip [String]
152
+ # @return [TrueClass, FalseClass]
109
153
  def connection_alive?(ip)
110
154
  %x{ping -c 1 -W 1 #{ip}}
111
155
  $?.exitstatus == 0
112
156
  end
113
157
  end
114
158
 
115
- # name:: name of container
116
- # args:: Argument hash
117
- # - :base_path -> path to container directory
118
- # - :dnsmasq_lease_file -> path to lease file
119
- # - :net_device -> network device to use within container for ssh connection
159
+ # Create new instance
160
+ #
161
+ # @param name [String] container name
162
+ # @param args [Hash]
163
+ # @option args [String] :base_path path to container
164
+ # @option args [String] :dnsmasq_lease_file path to lease file
165
+ # @option args [String] :net_device network device within container for ssh connection
166
+ # @option args [String] :ssh_key path to ssh key
167
+ # @option args [String] :ssh_password ssh password
168
+ # @option args [String] :ssh_user ssh user
120
169
  def initialize(name, args={})
121
170
  @name = name
122
171
  @base_path = args[:base_path] || self.class.base_path
123
172
  @lease_file = args[:dnsmasq_lease_file] || '/var/lib/misc/dnsmasq.leases'
124
173
  @preferred_device = args[:net_device]
174
+ @ssh_key = args.fetch(:ssh_key, self.class.default_ssh_key)
175
+ @ssh_password = args.fetch(:ssh_password, self.class.default_ssh_password)
176
+ @ssh_user = args.fetch(:ssh_user, self.class.default_ssh_user)
125
177
  end
126
178
 
127
- # Returns if container exists
179
+ # @return [TrueClass, FalseClass] container exists
128
180
  def exists?
129
181
  self.class.exists?(name)
130
182
  end
131
183
 
132
- # Returns if container is running
184
+ # @return [TrueClass, FalseClass] container is currently running
133
185
  def running?
134
186
  self.class.info(name)[:state] == :running
135
187
  end
136
188
 
137
- # Returns if container is stopped
189
+ # @return [TrueClass, FalseClass] container is currently stopped
138
190
  def stopped?
139
191
  self.class.info(name)[:state] == :stopped
140
192
  end
141
193
 
142
- # Returns if container is frozen
194
+ # @return [TrueClass, FalseClass] container is currently frozen
143
195
  def frozen?
144
196
  self.class.info(name)[:state] == :frozen
145
197
  end
146
198
 
147
- # retries:: Number of discovery attempt (3 second sleep intervals)
148
- # Returns container IP
199
+ # Current IP address of container
200
+ #
201
+ # @param retries [Integer] number of times to retry discovery
202
+ # @param raise_on_fail [TrueClass, FalseClass] raise exception on failure
203
+ # @return [String, NilClass] IP address
204
+ # @note retries are executed on 3 second sleep intervals
149
205
  def container_ip(retries=0, raise_on_fail=false)
150
206
  (retries.to_i + 1).times do
151
- ip = proc_detected_address || hw_detected_address || leased_address || lxc_stored_address
207
+ ip = info_detected_address ||
208
+ proc_detected_address ||
209
+ hw_detected_address ||
210
+ leased_address ||
211
+ lxc_stored_address
152
212
  if(ip.is_a?(Array))
153
213
  # Filter any found loopbacks
154
214
  ip.delete_if{|info| info[:device].start_with?('lo') }
@@ -168,7 +228,9 @@ class Lxc
168
228
  raise "Failed to detect live IP address for container: #{name}" if raise_on_fail
169
229
  end
170
230
 
171
- # Container address via lxc config file
231
+ # Container address defined within the container's config file
232
+ #
233
+ # @return [String, NilClass] IP address
172
234
  def lxc_stored_address
173
235
  if(File.exists?(container_config))
174
236
  ip = File.readlines(container_config).detect{|line|
@@ -183,7 +245,9 @@ class Lxc
183
245
  end
184
246
  end
185
247
 
186
- # Container address via dnsmasq lease
248
+ # Container address discovered via dnsmasq lease
249
+ #
250
+ # @return [String, NilClass] IP address
187
251
  def leased_address
188
252
  ip = nil
189
253
  if(File.exists?(@lease_file))
@@ -202,6 +266,16 @@ class Lxc
202
266
  end
203
267
  end
204
268
 
269
+ # Container address discovered via info
270
+ #
271
+ # @return [String, NilClass] IP address
272
+ def info_detected_address
273
+ self.class.info(name)[:ip]
274
+ end
275
+
276
+ # Container address discovered via device
277
+ #
278
+ # @return [String, NilClass] IP address
205
279
  def hw_detected_address
206
280
  if(container_config.readable?)
207
281
  hw = File.readlines(container_config).detect{|line|
@@ -222,6 +296,10 @@ class Lxc
222
296
  end
223
297
  end
224
298
 
299
+ # Container address discovered via process
300
+ #
301
+ # @param base [String] path to netns
302
+ # @return [String, NilClass] IP address
225
303
  def proc_detected_address(base='/run/netns')
226
304
  if(pid != -1)
227
305
  Dir.mktmpdir do |t_dir|
@@ -240,18 +318,19 @@ class Lxc
240
318
  end
241
319
  end
242
320
 
243
- # Full path to container
321
+ # @return [Pathname] path to container
244
322
  def container_path
245
323
  Pathname.new(@base_path).join(name)
246
324
  end
247
325
  alias_method :path, :container_path
248
326
 
249
- # Full path to container configuration file
327
+ # @return [Pathname] path to configuration file
250
328
  def container_config
251
329
  container_path.join('config')
252
330
  end
253
331
  alias_method :config, :container_config
254
332
 
333
+ # @return [Pathname] path to rootfs
255
334
  def container_rootfs
256
335
  if(File.exists?(config))
257
336
  r_path = File.readlines(config).detect do |line|
@@ -262,19 +341,28 @@ class Lxc
262
341
  end
263
342
  alias_method :rootfs, :container_rootfs
264
343
 
344
+ # Expand path within containers rootfs
345
+ #
346
+ # @param path [String] relative path
347
+ # @return [Pathname] full path within container
265
348
  def expand_path(path)
266
349
  container_rootfs.join(path)
267
350
  end
268
351
 
352
+ # @return [Symbol] current state
269
353
  def state
270
354
  self.class.info(name)[:state]
271
355
  end
272
356
 
357
+ # @return [Integer, Symbol] process ID or :unknown
273
358
  def pid
274
359
  self.class.info(name)[:pid]
275
360
  end
276
361
 
277
362
  # Start the container
363
+ #
364
+ # @param args [Symbol] argument list (:no_daemon to foreground)
365
+ # @return [self]
278
366
  def start(*args)
279
367
  if(args.include?(:no_daemon))
280
368
  run_command("lxc-start -n #{name}", :sudo => true)
@@ -282,52 +370,73 @@ class Lxc
282
370
  run_command("lxc-start -n #{name} -d", :sudo => true)
283
371
  wait_for_state(:running)
284
372
  end
373
+ self
285
374
  end
286
375
 
287
376
  # Stop the container
377
+ #
378
+ # @return [self]
288
379
  def stop
289
380
  run_command("lxc-stop -n #{name}", :allow_failure_retry => 3, :sudo => true)
290
381
  wait_for_state(:stopped)
382
+ self
291
383
  end
292
384
 
293
385
  # Freeze the container
386
+ #
387
+ # @return [self]
294
388
  def freeze
295
389
  run_command("lxc-freeze -n #{name}", :sudo => true)
296
390
  wait_for_state(:frozen)
391
+ self
297
392
  end
298
393
 
299
394
  # Unfreeze the container
395
+ #
396
+ # @return [self]
300
397
  def unfreeze
301
398
  run_command("lxc-unfreeze -n #{name}", :sudo => true)
302
399
  wait_for_state(:running)
400
+ self
303
401
  end
304
402
 
305
403
  # Shutdown the container
404
+ #
405
+ # @return [self]
306
406
  def shutdown
307
- run_command("lxc-shutdown -n #{name}", :sudo => true)
308
- wait_for_state(:stopped, :timeout => 120)
309
407
  # This block is for fedora/centos/anyone else that does not like lxc-shutdown
310
408
  if(running?)
311
409
  container_command('shutdown -h now')
312
410
  wait_for_state(:stopped, :timeout => 120)
313
411
  # If still running here, something is wrong
314
412
  if(running?)
315
- raise "Failed to shutdown container: #{name}"
413
+ run_command("lxc-stop -n #{name}", :sudo => true)
414
+ wait_for_state(:stopped, :timeout => 120)
415
+ if(running?)
416
+ raise "Failed to shutdown container: #{name}"
417
+ end
316
418
  end
317
419
  end
420
+ self
318
421
  end
319
422
 
320
423
  # Destroy the container
424
+ #
425
+ # @return [self]
321
426
  def destroy
322
427
  unless stopped?
323
428
  stop
324
429
  end
325
430
  run_command("lxc-destroy -n #{name}", :sudo => true)
431
+ self
326
432
  end
327
433
 
328
- # command:: command string
329
- # opts:: option hash (:networking)
330
- # Execute command string within container
434
+ # Execute command within the container
435
+ #
436
+ # @param command [String] command to execute
437
+ # @param opts [Hash] options passed to #tmp_execute_script
438
+ # @return [CommandResult]
439
+ # @note container must be stopped
331
440
  def execute(command, opts={})
332
441
  if(stopped?)
333
442
  cmd = Shellwords.split(command)
@@ -347,22 +456,47 @@ class Lxc
347
456
  end
348
457
  end
349
458
 
459
+ # Execute command within running container
460
+ #
461
+ # @param command [String]
462
+ # @param args [Hash]
463
+ # @option args [Integer] :timeout
464
+ # @option args [TrueClass, FalseClass] :live_stream
465
+ # @option args [TrueClass, FalseClass] :raise_on_failure
466
+ # @return [CommandResult]
350
467
  def direct_container_command(command, args={})
351
468
  begin
352
- run_command(
353
- "ssh root@#{args[:ip] || container_ip} -i /opt/hw-lxc-config/id_rsa -oStrictHostKeyChecking=no '#{command}'",
354
- :sudo => true,
355
- :timeout => args[:timeout],
356
- :live_stream => args[:live_stream]
469
+ box = Rye::Box.new(
470
+ args.fetch(:ip, container_ip(3)),
471
+ :user => ssh_user,
472
+ :password => ssh_password,
473
+ :password_prompt => false,
474
+ :keys => [ssh_key],
475
+ :safe => false,
476
+ :paranoid => false
357
477
  )
358
- true
359
- rescue
360
- raise if args[:raise_on_failure]
361
- false
478
+ result = box.execute command
479
+ CommandResult.new(result)
480
+ rescue Rye::Err => e
481
+ if(args[:raise_on_failure])
482
+ raise CommandFailed.new(
483
+ "Command failed: #{command}",
484
+ CommandResult.new(e)
485
+ )
486
+ else
487
+ false
488
+ end
362
489
  end
363
490
  end
364
491
  alias_method :knife_container, :direct_container_command
365
492
 
493
+ # Wait for container to reach given state
494
+ #
495
+ # @param desired_state [Symbol]
496
+ # @param args [Hash]
497
+ # @option args [Integer] :timeout
498
+ # @option args [Numeric] :sleep_interval
499
+ # @return [self]
366
500
  def wait_for_state(desired_state, args={})
367
501
  args[:sleep_interval] ||= 1.0
368
502
  wait_total = 0.0
@@ -370,10 +504,18 @@ class Lxc
370
504
  sleep(args[:sleep_interval])
371
505
  wait_total += args[:sleep_interval]
372
506
  end
507
+ self
373
508
  end
374
509
 
375
- # command:: command string
376
- # Write command to temporary script with networking support wrap
510
+ # Write command to temporary file with networking enablement wrapper
511
+ # and yield relative path
512
+ #
513
+ # @param command [String]
514
+ # @param opts [Hash]
515
+ # @options opts [TrueClass, FalseClass] enable networking
516
+ # @yield block to execute
517
+ # @yieldparam command_script [String] path to file
518
+ # @return [Object] result of block
377
519
  def tmp_execute_script(command, opts)
378
520
  script_path = "tmp/#{SecureRandom.uuid}"
379
521
  File.open(rootfs.join(script_path), 'w') do |file|
@@ -400,23 +542,12 @@ EOS
400
542
  end
401
543
  end
402
544
 
403
- # Detect HOME environment variable. If not an acceptable
404
- # value, set to /root or /tmp
405
- def detect_home(set_if_missing=false)
406
- if(ENV['HOME'] && Pathname.new(ENV['HOME']).absolute?)
407
- ENV['HOME']
408
- else
409
- home = File.directory?('/root') && File.writable?('/root') ? '/root' : '/tmp'
410
- if(set_if_missing)
411
- ENV['HOME'] = home
412
- end
413
- home
414
- end
415
- end
416
-
417
- # cmd:: Shell command string
418
- # retries:: Number of retry attempts (1 second sleep interval)
419
- # Runs command in container via ssh
545
+ # Run command within container
546
+ #
547
+ # @param cmd [String] command to run
548
+ # @param retries [Integer] number of retry attempts
549
+ # @return [CommandResult]
550
+ # @note retries are over 1 second intervals
420
551
  def container_command(cmd, retries=1)
421
552
  begin
422
553
  detect_home(true)
@@ -438,18 +569,9 @@ EOS
438
569
  end
439
570
  end
440
571
 
441
- def log
442
- if(defined?(Chef))
443
- Chef::Log
444
- else
445
- unless(@logger)
446
- require 'logger'
447
- @logger = Logger.new('/dev/null')
448
- end
449
- @logger
450
- end
451
- end
452
-
453
572
  end
454
573
 
574
+ # Make default settings
455
575
  Lxc.shellout_helper = :mixlib_shellout
576
+ Lxc.default_ssh_key = '/opt/hw-lxc-config/id_rsa'
577
+ Lxc.default_ssh_user = 'root'
@@ -1,11 +1,21 @@
1
+ require 'elecksee'
2
+
1
3
  class Lxc
4
+ # Configuration file interface
2
5
  class FileConfig
3
6
 
7
+ # @return [Array]
4
8
  attr_reader :network
9
+ # @return [String] path to configuration file
5
10
  attr_reader :base
6
11
 
7
12
  class << self
8
13
 
14
+ # Convert object to Hash if possible
15
+ #
16
+ # @param thing [Object]
17
+ # @return [Hash]
18
+ # @note used mostly for the lxc resource within chef
9
19
  def convert_to_hash(thing)
10
20
  if(defined?(Chef) && thing.is_a?(Chef::Resource))
11
21
  result = Hash[*(
@@ -26,6 +36,10 @@ class Lxc
26
36
  result || thing
27
37
  end
28
38
 
39
+ # Symbolize keys within hash
40
+ #
41
+ # @param thing [Hashish]
42
+ # @return [Hash]
29
43
  def symbolize_hash(thing)
30
44
  if(defined?(Mash))
31
45
  Mash.new(thing)
@@ -38,6 +52,10 @@ class Lxc
38
52
  end
39
53
  end
40
54
 
55
+ # Generate configuration file contents
56
+ #
57
+ # @param resource [Hashish]
58
+ # @return [String]
41
59
  def generate_config(resource)
42
60
  resource = symbolize_hash(convert_to_hash(resource))
43
61
  config = []
@@ -79,6 +97,9 @@ class Lxc
79
97
 
80
98
  end
81
99
 
100
+ # Create new instance
101
+ #
102
+ # @param path [String]
82
103
  def initialize(path)
83
104
  raise 'LXC config file not found' unless File.exists?(path)
84
105
  @path = path
@@ -89,6 +110,9 @@ class Lxc
89
110
 
90
111
  private
91
112
 
113
+ # Parse the configuration file
114
+ #
115
+ # @return [TrueClass]
92
116
  def parse!
93
117
  cur_net = nil
94
118
  File.readlines(@path).each do |line|
@@ -115,6 +139,8 @@ class Lxc
115
139
  end
116
140
  end
117
141
  @network << cur_net if cur_net
142
+ true
118
143
  end
144
+
119
145
  end
120
146
  end