Class: Toaster::Util

Inherits:
Object
  • Object
show all
Defined in:
lib/toaster/util/util.rb

Overview

Author: Waldemar Hummer ([email protected])

Class Method Summary collapse

Class Method Details

.build_file_hash_for_ohai(paths, hash_to_fill = nil) ⇒ Object



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/toaster/util/util.rb', line 208

def self.build_file_hash_for_ohai(paths, hash_to_fill = nil)
  files = hash_to_fill ? hash_to_fill : {}
  paths.each do |path|
    if File.exist?(path)
      fileObj = File.new(path)
      statObj = File::Stat.new(path)
      files[path] = {
        "mode" => statObj.mode,
        "bytes" => fileObj.size,
        "ctime" => fileObj.ctime.to_i,
        "mtime" => fileObj.mtime.to_i,
        "owner" => "#{statObj.uid}:#{statObj.gid}"
      }
      if File.symlink?(path)
        files[path]["symlink_to"] = File.readlink(path)
      end
      if File.directory?(path)
        entries = Dir.entries(path).size - 2
        entries = 0 if entries < 0
        entries_rec = -1
        if path != "" && !path.match(/^\/+((dev)|(etc)|(lib)|(lib32)|(lib64)|(proc)|(opt)|(run)|(sys)|(usr)|(var))\/*$/)
          # find number of descendants (recursively)
          entries_rec = `find #{path} | wc -l`.strip
        end
        files[path]["type"] = "dir"
        files[path]["num_entries"] = entries
        files[path]["entries_recursive"] = entries_rec
      else
        md5 = Toaster::Util.file_md5(path)
        files[path]["hash"] = md5.strip if md5
      end
    else
      # nil indicates that the file does not exist..
      files[path] = nil
    end
  end
  return files
end

.child_process_pidsObject



140
141
142
143
144
145
146
147
148
149
# File 'lib/toaster/util/util.rb', line 140

def self.child_process_pids()
  pid = Process.pid
  child_pids = `ps o pid= --ppid #{pid}`
  status = $?
  result = child_pids.strip.split(/\s+/)
  # the PID of the forked "ps" process is usually also 
  # part of this list, hence remove it from the result
  result.delete("#{status.pid}")
  return result
end

.diff_dirs(dir1, dir2) ⇒ Object



247
248
249
250
251
252
253
254
255
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
289
290
291
292
293
294
# File 'lib/toaster/util/util.rb', line 247

def self.diff_dirs(dir1, dir2)
  require "diffy"
  out = "<style type=\"text/css\">" +
        ".diff_dir .content { border: 1px solid #999999; padding: 3px; }\n" +
        ".changed, .changed a { color: #000000; }\n" +
        ".not_changed, .not_changed a { color: #999999; }\n" +
        ".filename a { cursor: pointer; text-decoration: underline; }\n" +
        "</style>" +
        "<ul class=\"diff_dir\">"
  files = Set.new
  files.merge(`find #{dir1}/ -printf '%P\n'`.strip.split("\n"))
  files.merge(`find #{dir2}/ -printf '%P\n'`.strip.split("\n"))
  files.delete("")
  files.each_with_index do |file,idx|
    f1 = "#{dir1}/#{file}"
    f2 = "#{dir2}/#{file}"
    if !File.directory?(f1) && !File.directory?(f2)
      tmp_files = []
      if !File.exist?(f1)
        `mkdir -p '#{File.dirname(f1)}'`
        write(f1, "")
        tmp_files << f1
      end
      if !File.exist?(f2)
        `mkdir -p '#{File.dirname(f2)}'`
        write(f2, "")
        tmp_files << f2
      end
      begin
        thediff = ::Diffy::Diff.new(f1, f2, :source => 'files').to_s(:html)
        changed = thediff.include?("class=\"del\"") || thediff.include?("class=\"ins\"")
        out += "<li class=\"file #{changed ? 'changed' : 'not_changed'}\" id=\"diff_file_#{idx}\">"
        out += "<div class=\"filename\">" +
          "<a onclick=\"$('#diff_file_#{idx} .content').toggle('blind', {}, 50);\">#{file}</a></div>"
        out += "<div class=\"content\">"
        out += thediff
        out += "</div></li>"
      rescue => e
        out += "Cannot create diff: #{e}\n"
      end
      tmp_files.each do |file|
        File.delete(file)
      end
    end
  end
  out += "</ul>"
  return out
end

.empty?(value) ⇒ Boolean

Returns:

  • (Boolean)


41
42
43
# File 'lib/toaster/util/util.rb', line 41

def self.empty?(value)
  return value.nil? || (value.to_s.strip == "")
end

.exec_in_parallel(collection, &block) ⇒ Object



329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'lib/toaster/util/util.rb', line 329

def self.exec_in_parallel(collection, &block) 
  num_threads = 0
  result_queue = Queue.new
  results = []
  collection.each do |c|
    num_threads += 1
    Thread.start {
      begin 
        result_queue << block.call(c)
      rescue Object => ex
        result_queue << ex
      end
    }
  end
  (1..num_threads).each do
    results << result_queue.pop
  end
  return results
end

.exec_timeout(sec = 10*60, klass = nil, kill_forked_processes = true, &block) ⇒ Object

based on: www.ruby-doc.org/stdlib-1.9.3/libdoc/timeout/rdoc/Timeout.html added: functionality to kill external forked processes on timeout



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/toaster/util/util.rb', line 154

def self.exec_timeout(sec=10*60, klass=nil, kill_forked_processes=true, &block)
  return yield(sec) if sec == nil or sec.zero?
  exception = klass || Class.new(Timeout::ExitException)

  children_before = child_process_pids()

  begin
    begin
      x = Thread.current
      y = Thread.start {
        begin
          sleep sec
        rescue => e
          x.raise e
        else

          # terminate all sub-processes
          # forked by the block execution
          if kill_forked_processes
            puts "DEBUG: child processes before task execution: #{children_before}"
            children_after = child_process_pids()
            puts "DEBUG: child processes during task execution, after timeout (#{sec}sec): #{children_after}"
            diff = children_after - children_before
            puts "INFO: Killing forked subprocesses: #{diff}" if !diff.empty?
            diff.each do |kill_pid|
              puts "DEBUG: Command of process #{kill_pid}: '#{get_process_details(kill_pid)['cmd']}'"
            end
            kill_processes(diff)
          end

          x.raise exception, "execution expired"
        end
      }
      return yield(sec)
    ensure
      if y
        y.kill
        y.join # make sure y is dead.
      end
    end
  rescue exception => e
    rej = /\A#{Regexp.quote(__FILE__)}:#{__LINE__-4}\z/
    (bt = e.backtrace).reject! {|m| rej =~ m}
    level = -caller(Timeout::CALLER_OFFSET).size
    while Timeout::THIS_FILE =~ bt[level]
      bt.delete_at(level)
      level += 1
    end
    raise if klass            # if exception class is specified, it
                              # would be expected outside.
    raise RuntimeError, e.message, e.backtrace
  end
end

.exec_with_output_timestamps(cmd) ⇒ Object



349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/toaster/util/util.rb', line 349

def self.exec_with_output_timestamps(cmd)
  pid, stdin, stdout, stderr = Open4::popen4(cmd)
  start_time = Time.now.to_f
  last_time = start_time
  output = ""
  stdout.each_line do |line|
    now = Time.now.to_f
    diff1 = now - start_time
    diff2 = now - last_time
    last_time = now
    output += "[#{'%.3f' % diff1},#{'%.4f' % diff2}] #{line}"
  end
  return output
end

.file_empty?(file) ⇒ Boolean

Returns:

  • (Boolean)


45
46
47
48
# File 'lib/toaster/util/util.rb', line 45

def self.file_empty?(file)
  return true if !File.exist?(file)
  return self.empty?(File.read(file))
end

.file_md5(file) ⇒ Object

compute the MD5 hash over the contents of a given file



93
94
95
# File 'lib/toaster/util/util.rb', line 93

def self.file_md5(file)
  return md5(file, true)
end

.generate_short_uidObject



38
39
40
# File 'lib/toaster/util/util.rb', line 38

def self.generate_short_uid()
  return rand_hex_string(16)
end

.generate_uuidObject

generate a UUID, for example: 550e8400-e29b-41d4-a716-446655440000



16
17
18
19
20
21
22
23
24
25
# File 'lib/toaster/util/util.rb', line 16

def self.generate_uuid
  # For now, generate a pseudo-UUID here. Do not use the 'uuid' package
  # since it seems to conflict with some other gems installed on IWD instances.
  # Error message was:
  # FATAL: Gem::LoadError: Unable to activate macaddr-1.6.1, because systemu-2.2.0 conflicts with systemu (~> 2.5.0)

  uuid = rand_hex_string(16) + "-" + rand_hex_string(4) + "-" + 
    rand_hex_string(4) + "-" + rand_hex_string(4) + "-" + rand_hex_string(12)
  return uuid
end

.get_home_dirObject



33
34
35
36
37
# File 'lib/toaster/util/util.rb', line 33

def self.get_home_dir()
  user = `whoami`
  return "/root/" if user.strip == "root"
  return Dir.home
end

.get_machine_idObject



26
27
28
29
30
31
32
# File 'lib/toaster/util/util.rb', line 26

def self.get_machine_id() 
  filename = File.join(get_home_dir(), ".toaster.testing.machine_id")
  return File.read(filename).strip if File.exist?(filename)
  puts "Saving machine ID to file #{filename}"
  File.open(filename, 'w') { |f| f.write(self.rand_hex_string(16)) }
  return File.read(filename).strip
end

.get_process_details(pid) ⇒ Object



322
323
324
325
326
327
# File 'lib/toaster/util/util.rb', line 322

def self.get_process_details(pid)
  cmd = `ps o cmd= --pid #{pid}`.strip
  return {
    "cmd" => cmd
  }
end

.ip_address?(str) ⇒ Boolean

Returns:

  • (Boolean)


55
56
57
# File 'lib/toaster/util/util.rb', line 55

def self.ip_address?(str)
  return is_ip_address(str)
end

.ipaddressObject

return local IP address



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/toaster/util/util.rb', line 105

def self.ipaddress()
  # turn off reverse DNS resolution temporarily
  orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true
  ip = nil
  begin
    UDPSocket.open do |s|
      s.connect '8.8.8.8', 1
      ip = s.addr.last
    end
  ensure
    Socket.do_not_reverse_lookup = orig
  end
  if !is_ip_address(ip)
    # TODO: IP lookup using ifconfig...
  end
  return ip
end

.is_ip_address(str) ⇒ Object



59
60
61
62
63
64
65
# File 'lib/toaster/util/util.rb', line 59

def self.is_ip_address(str)
  return false if !str
  str = str.to_s
  m = str.match(/([0-9]{1,3}\.){3}[0-9]{1,3}/)
  m = m.nil? ? "" : m.to_s
  return (m == str)
end

.kill(pid) ⇒ Object



303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/toaster/util/util.rb', line 303

def self.kill(pid)
  `kill #{pid}`
  max_wait = 5
  (1..max_wait).each do
    if !process_alive?(pid)
      return
    end
    sleep 1
  end
  # process is still alive --> kill with SIGKILL
  `kill -sigkill #{pid}`
end

.kill_processes(pids) ⇒ Object



296
297
298
299
300
301
# File 'lib/toaster/util/util.rb', line 296

def self.kill_processes(pids)
  pids = [pids] if !pids.kind_of?(Array)
  pids.each do |pid|
    kill(pid)
  end
end

.latest_timestamp(list, pattern) ⇒ Object



421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/toaster/util/util.rb', line 421

def self.latest_timestamp(list, pattern)
  latest = 0
  list.each do |item|
    if item.to_s.match(pattern).to_s == item.to_s
      time = item.to_s.gsub(pattern, '\1')
      time = time.to_f
      if time > latest
        latest = time
      end
    end
  end
  return latest
end

.latest_timestamp_item(list, pattern) ⇒ Object



405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/toaster/util/util.rb', line 405

def self.latest_timestamp_item(list, pattern)
  latest = 0
  latest_item = nil
  list.each do |item|
    if item.to_s.match(pattern).to_s == item.to_s
      time = item.to_s.gsub(pattern, '\1')
      time = time.to_f
      if time > latest
        latest = time
        latest_item = item
      end
    end
  end
  return latest_item
end

.match_any(str, pattern_list = [".*"]) ⇒ Object



129
130
131
132
133
134
# File 'lib/toaster/util/util.rb', line 129

def self.match_any(str, pattern_list=[".*"])
  pattern_list.each do |p|
    return true if str.match(/#{p}/)
  end
  return false
end

.md5(str_or_file, read_from_file = false) ⇒ Object

compute the MD5 hash of a given string or file



73
74
75
76
77
78
79
80
81
82
# File 'lib/toaster/util/util.rb', line 73

def self.md5(str_or_file, read_from_file = false)
  if read_from_file
    return nil if !File.exist?(str_or_file)
    file = File.expand_path(str_or_file)
    md5 = `md5sum #{file}`
    md5.gsub!(/^([a-zA-Z0-9]+) .*/,'\1')
    return md5
  end
  return Digest::MD5.hexdigest(str_or_file)
end

.mktmpdir(&block) ⇒ Object

Dir.mktmpdir fails under certain JRuby versions, hence we provide our own simple implementation here.. see, e.g., jira.codehaus.org/browse/JRUBY-6178



388
389
390
391
392
393
394
395
396
397
398
# File 'lib/toaster/util/util.rb', line 388

def self.mktmpdir(&block)
  file = Tempfile.new('toaster_tmp_dir')
  path = file.path
  file.unlink
  Dir.mkdir(path)
  begin
    block.call(path)
  ensure
    FileUtils.rm_rf(path)
  end
end

.mktmpfile(relative_to = "/tmp/", &block) ⇒ Object



364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# File 'lib/toaster/util/util.rb', line 364

def self.mktmpfile(relative_to="/tmp/", &block)
  name = ""
  begin
    name = `mktemp`.strip
    File.delete(name) if File.exist?(name)
    name = File.basename(name)
    name = "#{relative_to}#{name}"
  end while File.exist?(name)
  # we have determined the file name. now run the block.
  `touch #{name}`
  if block
    begin
      block.call(name)
    ensure
      File.delete(name)
    end
  else 
    return name
  end
end

.pidObject



136
137
138
# File 'lib/toaster/util/util.rb', line 136

def self.pid()
  return Process.pid
end


125
126
127
# File 'lib/toaster/util/util.rb', line 125

def self.print_backtrace(ex, max_lines = -1, start_line = 0)
  puts "#{ex}\n#{ex.backtrace[start_line..max_lines].join("\n")}\n..."
end

.process_alive?(pid) ⇒ Boolean

Returns:

  • (Boolean)


316
317
318
319
320
# File 'lib/toaster/util/util.rb', line 316

def self.process_alive?(pid)
  `ps --pid #{pid}`
  code = $?
  return code.exitstatus == 0
end

.project_root_dirObject



84
85
86
# File 'lib/toaster/util/util.rb', line 84

def self.project_root_dir()
  return File.expand_path(File.join(File.dirname(__FILE__), "..", "..", ".."))
end

.rand_hex_string(length) ⇒ Object

generate a (pseudo-)random string of a given length



51
52
53
# File 'lib/toaster/util/util.rb', line 51

def self.rand_hex_string(length)
  return (0..length).to_a.map{|a| rand(16).to_s(16)}.join
end

.starts_with?(string, prefix) ⇒ Boolean

Returns:

  • (Boolean)


400
401
402
403
# File 'lib/toaster/util/util.rb', line 400

def self.starts_with?(string, prefix)
  prefix = prefix.to_s
  string[0, prefix.length] == prefix
end

.str_eql?(o1, o2) ⇒ Boolean

check if the string representation of two objects is equal

Returns:

  • (Boolean)


68
69
70
# File 'lib/toaster/util/util.rb', line 68

def self.str_eql?(o1, o2)
  return "#{o1}" == "#{o2}"
end

.toaster_executableObject



88
89
90
# File 'lib/toaster/util/util.rb', line 88

def self.toaster_executable()
  return File.join(project_root_dir(), "bin", "toaster")
end

.write(file, content, overwrite = false) ⇒ Object



97
98
99
100
101
102
# File 'lib/toaster/util/util.rb', line 97

def self.write(file, content, overwrite=false)
  if !overwrite && File.exist?(file)
    raise "File exists and no overwrite option provided: #{file}"
  end
  File.open(file, 'w') {|f| f.write(content) }
end