~nchohan/+junk/mytools

« back to all changes in this revision

Viewing changes to lib/common_functions.rb

  • Committer: root
  • Date: 2010-11-03 07:43:57 UTC
  • Revision ID: root@appscale-image0-20101103074357-xea7ja3sor3x93oc
init

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/ruby -w
 
2
# Programmer: Chris Bunch
 
3
 
 
4
require 'digest/sha1'
 
5
require 'net/http'
 
6
require 'openssl'
 
7
require 'open-uri'
 
8
require 'socket'
 
9
require 'timeout'
 
10
require 'yaml'
 
11
 
 
12
require 'app_controller_client'
 
13
 
 
14
MAX_FILE_SIZE = 1000000
 
15
 
 
16
EMAIL_REGEX = /\A[[:print:]]+@[[:print:]]+\.[[:print:]]+\Z/
 
17
PASSWORD_REGEX = /\A[[:print:]]{6,}\Z/
 
18
IP_REGEX = /\d+\.\d+\.\d+\.\d+/
 
19
FQDN_REGEX = /[\w\d\.\-]+/
 
20
IP_OR_FQDN = /#{IP_REGEX}|#{FQDN_REGEX}/
 
21
 
 
22
AS_VERSION = "AppScale Tools, Version 1.4, http://appscale.cs.ucsb.edu"
 
23
 
 
24
PYTHON_CONFIG = "app.yaml"
 
25
JAVA_CONFIG = "war/WEB-INF/appengine-web.xml"
 
26
 
 
27
VALID_TABLE_TYPES = ["hbase", "hypertable", "mysql", "cassandra", "voldemort"] +
 
28
  ["mongodb", "memcachedb", "scalaris", "simpledb"]
 
29
VALID_CLOUD_TYPES = ["ec2", "euca", "hybrid"]
 
30
 
 
31
module CommonFunctions
 
32
  # cgb: added in shell function for backticks so that we can unit test it
 
33
  # since flexmock doesn't like backticks since its name is non-alphanumeric
 
34
  # e.g., its name is Kernel, :`
 
35
  def self.shell(command)
 
36
    `#{command}`
 
37
  end
 
38
 
 
39
  def self.get_login_ip(head_node_ip, secret_key)
 
40
    acc = AppControllerClient.new(head_node_ip, secret_key)
 
41
    all_nodes = acc.get_all_public_ips()
 
42
 
 
43
    all_nodes.each { |node|
 
44
        acc_new = AppControllerClient.new(node, secret_key)
 
45
        roles = acc_new.status(print_output=false)
 
46
        return node if roles.match(/Is currently:(.*)login/)
 
47
    }
 
48
 
 
49
    abort("Unable to find login ip address!")
 
50
  end
 
51
 
 
52
  def self.clear_app(app_path, force=false)
 
53
    return if !File.exists?(app_path)
 
54
    return if app_path !~ /\A\/tmp/ and !force
 
55
    remove_me = app_path.scan(/(\A.*)\//).flatten.to_s
 
56
    FileUtils.rm_rf(remove_me, :secure => true)
 
57
  end
 
58
 
 
59
  def self.validate_appname(app_name)
 
60
    disallowed = ["none", "auth", "login", "new_user", "load_balancer"]
 
61
    disallowed.each { |not_allowed|
 
62
      abort("App can't be called '#{not_allowed}'") if app_name == not_allowed 
 
63
    }
 
64
    abort("App name can only contain alphanumerics and .-@") if app_name =~ /[^[:alnum:].@-]/
 
65
    return app_name
 
66
  end
 
67
  
 
68
  def self.get_ips_from_yaml(ips)
 
69
    return "using_tools" if ips.nil?
 
70
 
 
71
    ips_to_use = []
 
72
    if !ips[:servers].nil?
 
73
      ips[:servers].each { |ip|
 
74
        if ip =~ IP_REGEX
 
75
          ips_to_use << ip
 
76
        else
 
77
          ips_to_use << CommonFunctions.convert_fqdn_to_ip(ip)
 
78
        end
 
79
      }
 
80
      ips_to_use = ips_to_use.join(":")
 
81
    end
 
82
    
 
83
    return ips_to_use
 
84
  end
 
85
  
 
86
  def self.get_credentials(testing)
 
87
    if testing
 
88
      return "a@a.a", "aaaaaa"
 
89
    else
 
90
      return CommonFunctions.get_email, CommonFunctions.get_password
 
91
    end
 
92
  end
 
93
 
 
94
  def self.wait_until_redirect(host, url_suffix)
 
95
    uri = "http://#{host}#{url_suffix}"
 
96
    loop {
 
97
      response = ""
 
98
      begin
 
99
        response = Net::HTTP.get_response(URI.parse(uri))
 
100
      rescue Errno::ECONNREFUSED, EOFError
 
101
        sleep(1)
 
102
        next
 
103
      rescue Exception => e
 
104
        abort("[unexpected] We were unable to see if your app is running. We saw an exception of type #{e.class}")
 
105
      end
 
106
      
 
107
      return if response['location'] != "http://#{host}/status"
 
108
      sleep(1)
 
109
    }
 
110
  end
 
111
 
 
112
  def self.user_has_cmd?(command)
 
113
    output = CommonFunctions.shell("which #{command}")
 
114
    if output == ""
 
115
      return false
 
116
    else
 
117
      return true
 
118
    end
 
119
  end
 
120
 
 
121
  def self.convert_fqdn_to_ip(host)
 
122
    nslookup = CommonFunctions.shell("nslookup #{host}")
 
123
    ip = nslookup.scan(/#{host}\nAddress:\s+(#{IP_REGEX})/).flatten.to_s
 
124
    abort("Couldn't convert #{host} to an IP address. Result of nslookup was \n#{nslookup}") if ip.nil? or ip == ""
 
125
    return ip
 
126
  end
 
127
 
 
128
  def self.encrypt_password(user, pass)
 
129
    Digest::SHA1.hexdigest(user + pass)
 
130
  end
 
131
 
 
132
  def self.sleep_until_port_is_open(ip, port, use_ssl=false)
 
133
    loop {
 
134
      return if CommonFunctions.is_port_open?(ip, port, use_ssl)
 
135
      Kernel.sleep(1)
 
136
    }
 
137
  end
 
138
 
 
139
  def self.sleep_until_port_is_closed(ip, port, use_ssl=false)
 
140
    loop {
 
141
      return unless CommonFunctions.is_port_open?(ip, port, use_ssl)
 
142
      Kernel.sleep(1)
 
143
    }
 
144
  end
 
145
 
 
146
  def self.is_port_open?(ip, port, use_ssl=false)
 
147
    begin
 
148
      Timeout::timeout(1) do
 
149
        begin
 
150
          sock = TCPSocket.new(ip, port)
 
151
          if use_ssl
 
152
            ssl_context = OpenSSL::SSL::SSLContext.new() 
 
153
            unless ssl_context.verify_mode 
 
154
              ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE 
 
155
            end 
 
156
            sslsocket = OpenSSL::SSL::SSLSocket.new(sock, ssl_context) 
 
157
            sslsocket.sync_close = true 
 
158
            sslsocket.connect          
 
159
          end
 
160
          sock.close
 
161
          return true
 
162
        rescue Exception
 
163
          return false
 
164
        end
 
165
      end
 
166
    rescue Timeout::Error
 
167
    end
 
168
  
 
169
    return false
 
170
  end
 
171
 
 
172
  def self.run_remote_command(ip, command, public_key_loc, want_output)
 
173
    if public_key_loc.class == Array
 
174
      public_key_loc.each { |key|
 
175
        key = File.expand_path(key)
 
176
      }
 
177
 
 
178
      remote_cmd = "ssh -i #{public_key_loc.join(' -i ')} -o StrictHostkeyChecking=no 2>&1 root@#{ip} '#{command}"
 
179
    else
 
180
      public_key_loc = File.expand_path(public_key_loc)
 
181
      remote_cmd = "ssh -i #{public_key_loc} -o StrictHostkeyChecking=no root@#{ip} '#{command} "
 
182
    end
 
183
    
 
184
    if want_output
 
185
      remote_cmd << "> /tmp/#{ip}.log 2>&1 &' &"
 
186
    else
 
187
      remote_cmd << "> /dev/null 2>&1 &' &"
 
188
    end
 
189
 
 
190
    Kernel.system remote_cmd
 
191
    return remote_cmd
 
192
  end
 
193
  
 
194
  def self.find_real_ssh_key(ssh_keys, host)
 
195
    ssh_keys.each { |key|
 
196
      key = File.expand_path(key)
 
197
      return_value = CommonFunctions.shell("ssh -i #{key} -o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no 2>&1 root@#{host} 'touch /tmp/foo'; echo $? ").chomp
 
198
      return key if return_value == "0"
 
199
    }
 
200
    
 
201
    return nil
 
202
  end
 
203
 
 
204
  def self.scp_file(local_file_loc, remote_file_loc, target_ip, public_key_loc)
 
205
    cmd = ""
 
206
    local_file_loc = File.expand_path(local_file_loc)
 
207
    
 
208
    if public_key_loc.class == Array
 
209
      public_key_loc.each { |key|
 
210
        key = File.expand_path(key)
 
211
      }
 
212
      
 
213
      cmd = "scp -i #{public_key_loc.join(' -i ')} -o StrictHostkeyChecking=no 2>&1 #{local_file_loc} root@#{target_ip}:#{remote_file_loc}"
 
214
    else
 
215
      public_key_loc = File.expand_path(public_key_loc)
 
216
      cmd = "scp -i #{public_key_loc} -o StrictHostkeyChecking=no 2>&1 #{local_file_loc} root@#{target_ip}:#{remote_file_loc}"
 
217
    end
 
218
 
 
219
    cmd << "; echo $? > ~/.appscale/retval"
 
220
 
 
221
    retval_loc = File.expand_path("~/.appscale/retval")
 
222
    FileUtils.rm_f(retval_loc)
 
223
 
 
224
    begin
 
225
      Timeout::timeout(-1) { CommonFunctions.shell("#{cmd}") }
 
226
    rescue Timeout::Error
 
227
      abort("Remotely copying over files failed. Is the destination machine on and reachable from this computer? We tried the following command:\n\n#{cmd}")
 
228
    end
 
229
 
 
230
    loop {
 
231
      break if File.exists?(retval_loc)
 
232
      sleep(5)
 
233
    }
 
234
 
 
235
    retval = (File.open(retval_loc) { |f| f.read }).chomp
 
236
 
 
237
    fails = 0
 
238
    loop {
 
239
      break if retval == "0"
 
240
      puts "\n\n[#{cmd}] returned #{retval} instead of 0 as expected. Will try to copy again momentarily..."
 
241
      fails += 1
 
242
      abort("SCP failed") if fails >= 5
 
243
      sleep(1)
 
244
      CommonFunctions.shell("#{cmd}")
 
245
      retval = (File.open(retval_loc) { |f| f.read }).chomp
 
246
    }
 
247
 
 
248
    return cmd
 
249
  end
 
250
 
 
251
  def self.get_email
 
252
    email = nil
 
253
    puts "\nThis AppScale instance is linked to an e-mail address giving it administrator privileges."
 
254
    
 
255
    loop {
 
256
      print "Enter your desired administrator e-mail address: "
 
257
      STDOUT.flush
 
258
      email = STDIN.gets.chomp
 
259
      
 
260
      if email =~ EMAIL_REGEX
 
261
        break
 
262
      else
 
263
        puts "The response you typed in was not an e-mail address. Please try again.\n\n"
 
264
      end
 
265
    }
 
266
    
 
267
    return email
 
268
  end
 
269
 
 
270
  def self.get_password
 
271
    pass = nil
 
272
    puts "\nThe new administrator password must be at least six characters long and can include non-alphanumeric characters."
 
273
    
 
274
    loop {
 
275
      system "stty -echo" # Turn off character echoing
 
276
      print "Enter your new password: "
 
277
      STDOUT.flush
 
278
      new_pass = STDIN.gets.chomp
 
279
      print "\nEnter again to verify: "
 
280
      STDOUT.flush
 
281
      verify_pass = STDIN.gets.chomp
 
282
      system "stty echo" # Next release: find a platform independent solution
 
283
      
 
284
      if new_pass == verify_pass
 
285
        pass = new_pass
 
286
        
 
287
        if pass =~ PASSWORD_REGEX
 
288
          break
 
289
        else
 
290
          puts "\n\nThe password you typed in was not at least six characters long. Please try again.\n\n"
 
291
        end
 
292
      else
 
293
        puts "\n\nPasswords entered do not match. Please try again.\n\n"
 
294
      end
 
295
    }
 
296
    
 
297
    return pass
 
298
  end
 
299
  
 
300
  def self.get_from_yaml(keyname, tag, required=true)
 
301
    location_file = File.expand_path("~/.appscale/locations-#{keyname}.yaml")
 
302
 
 
303
    abort("An AppScale instance is not currently running with the provided keyname, \"#{keyname}\".") unless File.exists?(location_file)  
 
304
    
 
305
    begin
 
306
      tree = YAML.load_file(location_file)
 
307
    rescue ArgumentError
 
308
      if required
 
309
        abort("The yaml file you provided was malformed. Please correct any errors in it and try again.")
 
310
      else
 
311
        return nil
 
312
      end
 
313
    end
 
314
    
 
315
    value = tree[tag]
 
316
    
 
317
    bad_yaml_format_msg = "The file #{location_file} is in the wrong format and doesn't contain a #{tag} tag. Please make sure the file is in the correct format and try again"
 
318
    abort(bad_yaml_format_msg) if value.nil? and required
 
319
    return value
 
320
  end
 
321
 
 
322
  def self.get_load_balancer_ip(keyname, required=true)
 
323
    return CommonFunctions.get_from_yaml(keyname, :load_balancer)
 
324
  end
 
325
 
 
326
  def self.get_load_balancer_id(keyname, required=true)
 
327
    return CommonFunctions.get_from_yaml(keyname, :instance_id)  
 
328
  end
 
329
 
 
330
  def self.get_table(keyname, required=true)
 
331
    return CommonFunctions.get_from_yaml(keyname, :table, required)
 
332
  end
 
333
 
 
334
  def self.get_db_master_ip(keyname, required=true)
 
335
    return CommonFunctions.get_from_yaml(keyname, :db_master, required)
 
336
  end
 
337
 
 
338
  def self.get_head_node_ip(keyname, required=true)
 
339
    CommonFunctions.get_from_yaml(keyname, :shadow)
 
340
  end
 
341
 
 
342
  def self.get_secret_key(keyname, required=true)
 
343
    CommonFunctions.get_from_yaml(keyname, :secret)
 
344
  end
 
345
 
 
346
  def self.write_node_file(head_node_ip, instance_id, table, secret, db_master_ip)
 
347
    tree = { :load_balancer => head_node_ip, :instance_id => instance_id , 
 
348
             :table => table, :shadow => head_node_ip, 
 
349
             :secret => secret , :db_master => db_master_ip }
 
350
    loc_path = File.expand_path(LOCATIONS_YAML)
 
351
    File.open(loc_path, "w") {|file| YAML.dump(tree, file)}
 
352
  end
 
353
 
 
354
  def self.get_random_alphanumeric(length=10)
 
355
    random = ""
 
356
    possible = "0123456789abcdefghijklmnopqrstuvxwyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
 
357
    possibleLength = possible.length
 
358
 
 
359
    length.times { |index|
 
360
      random << possible[rand(possibleLength)]
 
361
    }
 
362
 
 
363
    return random
 
364
  end
 
365
 
 
366
  def self.get_appname_from_tar(fullpath)
 
367
    appname, file, language = CommonFunctions.get_app_info(fullpath, PYTHON_CONFIG)
 
368
 
 
369
    if appname.nil? or file.nil? or language.nil?
 
370
      appname, file, language = CommonFunctions.get_app_info(fullpath, JAVA_CONFIG)
 
371
    end
 
372
 
 
373
    if appname.nil? or file.nil? or language.nil?
 
374
      abort("We could not find a valid app.yaml or web.xml file in your application.")
 
375
    end
 
376
 
 
377
    return appname, file, language
 
378
  end
 
379
  
 
380
  def self.move_app(temp_dir, filename, app_file, fullpath)
 
381
    if File.directory?(fullpath)
 
382
      begin
 
383
        `cp -r #{fullpath}/* /tmp/#{temp_dir}/`
 
384
      rescue Errno::EACCES
 
385
        abort("Copying the file to /tmp failed")
 
386
      end
 
387
      return
 
388
    else
 
389
      begin
 
390
        FileUtils.cp(fullpath, "/tmp/#{temp_dir}/#{filename}")
 
391
      rescue Errno::EACCES
 
392
        abort("Copying the file to /tmp failed")
 
393
      end
 
394
 
 
395
      FileUtils.rm_f("/tmp/#{temp_dir}/#{app_file}")
 
396
      tar_file = CommonFunctions.shell("cd /tmp/#{temp_dir}; tar zxvfm #{filename} 2>&1; echo $?").chomp
 
397
      tar_ret_val = tar_file.scan(/\d+\Z/).to_s
 
398
      abort("Untar'ing the given tar file in /tmp failed") if tar_ret_val != "0"
 
399
    end
 
400
    return
 
401
  end
 
402
 
 
403
  def self.warn_on_large_app_size(fullpath)
 
404
    size = File.size(fullpath)
 
405
    if size > MAX_FILE_SIZE
 
406
      puts "Warning: Your application is large enough that it may have problems being uploaded into certain databases. Will continue in 5 seconds..."
 
407
      sleep(5)
 
408
    end
 
409
  end
 
410
 
 
411
  def self.get_app_info(fullpath, app_file)
 
412
    abort("AppEngine file not found") unless File.exists?(fullpath)
 
413
    filename = fullpath.scan(/\/?([\w\.]+\Z)/).flatten.to_s
 
414
 
 
415
    temp_dir = CommonFunctions.get_random_alphanumeric
 
416
    FileUtils.rm_rf("/tmp/#{temp_dir}", :secure => true)
 
417
    FileUtils.mkdir_p("/tmp/#{temp_dir}")
 
418
 
 
419
    CommonFunctions.move_app(temp_dir, filename, app_file, fullpath)
 
420
    app_yaml_loc = app_file
 
421
    if !File.exists?("/tmp/#{temp_dir}/#{app_file}")
 
422
      FileUtils.rm_rf("/tmp/#{temp_dir}", :secure => true)
 
423
      return nil, nil, nil
 
424
    end
 
425
 
 
426
    if app_file == PYTHON_CONFIG
 
427
      appname = CommonFunctions.get_appname_via_yaml(temp_dir, app_yaml_loc)
 
428
      language = "python"
 
429
      if File.directory?(fullpath)
 
430
        temp_dir2 = CommonFunctions.get_random_alphanumeric
 
431
        FileUtils.rm_rf("/tmp/#{temp_dir2}", :secure => true)
 
432
        FileUtils.mkdir_p("/tmp/#{temp_dir2}")
 
433
        CommonFunctions.shell("cd /tmp/#{temp_dir}; tar -czf ../#{temp_dir2}/#{appname}.tar.gz .")
 
434
        file = "/tmp/#{temp_dir2}/#{appname}.tar.gz"
 
435
      else
 
436
        file = fullpath
 
437
      end
 
438
    elsif app_file == JAVA_CONFIG
 
439
      appname = CommonFunctions.get_appname_via_xml(temp_dir, app_yaml_loc)
 
440
      language = "java"
 
441
      FileUtils.rm_rf("/tmp/#{temp_dir}/war/WEB-INF/lib/", :secure => true)
 
442
      FileUtils.mkdir_p("/tmp/#{temp_dir}/war/WEB-INF/lib")
 
443
      temp_dir2 = CommonFunctions.get_random_alphanumeric
 
444
      FileUtils.rm_rf("/tmp/#{temp_dir2}", :secure => true)
 
445
      FileUtils.mkdir_p("/tmp/#{temp_dir2}")
 
446
      FileUtils.rm_f("/tmp/#{temp_dir}/#{filename}")
 
447
      CommonFunctions.shell("cd /tmp/#{temp_dir}; tar -czf ../#{temp_dir2}/#{appname}.tar.gz .")
 
448
      file = "/tmp/#{temp_dir2}/#{appname}.tar.gz"
 
449
    else
 
450
      FileUtils.rm_rf("/tmp/#{temp_dir}", :secure => true)
 
451
      abort("appname was #{app_file}, which was not a recognized value.")
 
452
    end
 
453
 
 
454
    if appname.nil?
 
455
      FileUtils.rm_rf("/tmp/#{temp_dir}", :secure => true)
 
456
      abort("AppEngine tar file is invalid - Doesn't have an app name in #{app_file}")
 
457
    end
 
458
 
 
459
    FileUtils.rm_rf("/tmp/#{temp_dir}", :secure => true)
 
460
    CommonFunctions.warn_on_large_app_size(file)
 
461
    return appname, file, language 
 
462
  end
 
463
 
 
464
  def self.get_appname_via_yaml(temp_dir, app_yaml_loc)
 
465
    app_yaml_loc = "/tmp/" + temp_dir + "/" + app_yaml_loc
 
466
    
 
467
    begin
 
468
      tree = YAML.load_file(app_yaml_loc.chomp)
 
469
    rescue ArgumentError
 
470
      abort("The yaml file you provided was malformed. Please correct any errors in it and try again.")
 
471
    end
 
472
    
 
473
    appname = tree["application"]
 
474
    return appname
 
475
  end
 
476
 
 
477
  def self.get_appname_via_xml(temp_dir, xml_loc)
 
478
    xml_loc = "/tmp/" + temp_dir + "/" + xml_loc
 
479
    web_xml_contents = (File.open(xml_loc) { |f| f.read }).chomp
 
480
    appname = web_xml_contents.scan(/<application>([\w\d-]+)<\/application>/).flatten.to_s
 
481
    appname = nil if appname == ""
 
482
    return appname
 
483
  end
 
484
 
 
485
  private
 
486
 
 
487
  def self.grab_file filename
 
488
    filename = File.expand_path(filename)
 
489
    content = nil
 
490
    begin
 
491
      content = File.open(filename) { |f| f.read.chomp! }
 
492
    rescue Errno::ENOENT
 
493
    end
 
494
    content
 
495
  end
 
496
end