2
# Programmer: Chris Bunch
12
require 'app_controller_client'
14
MAX_FILE_SIZE = 1000000
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}/
22
AS_VERSION = "AppScale Tools, Version 1.4, http://appscale.cs.ucsb.edu"
24
PYTHON_CONFIG = "app.yaml"
25
JAVA_CONFIG = "war/WEB-INF/appengine-web.xml"
27
VALID_TABLE_TYPES = ["hbase", "hypertable", "mysql", "cassandra", "voldemort"] +
28
["mongodb", "memcachedb", "scalaris", "simpledb"]
29
VALID_CLOUD_TYPES = ["ec2", "euca", "hybrid"]
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)
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()
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/)
49
abort("Unable to find login ip address!")
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)
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
64
abort("App name can only contain alphanumerics and .-@") if app_name =~ /[^[:alnum:].@-]/
68
def self.get_ips_from_yaml(ips)
69
return "using_tools" if ips.nil?
72
if !ips[:servers].nil?
73
ips[:servers].each { |ip|
77
ips_to_use << CommonFunctions.convert_fqdn_to_ip(ip)
80
ips_to_use = ips_to_use.join(":")
86
def self.get_credentials(testing)
88
return "a@a.a", "aaaaaa"
90
return CommonFunctions.get_email, CommonFunctions.get_password
94
def self.wait_until_redirect(host, url_suffix)
95
uri = "http://#{host}#{url_suffix}"
99
response = Net::HTTP.get_response(URI.parse(uri))
100
rescue Errno::ECONNREFUSED, EOFError
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}")
107
return if response['location'] != "http://#{host}/status"
112
def self.user_has_cmd?(command)
113
output = CommonFunctions.shell("which #{command}")
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 == ""
128
def self.encrypt_password(user, pass)
129
Digest::SHA1.hexdigest(user + pass)
132
def self.sleep_until_port_is_open(ip, port, use_ssl=false)
134
return if CommonFunctions.is_port_open?(ip, port, use_ssl)
139
def self.sleep_until_port_is_closed(ip, port, use_ssl=false)
141
return unless CommonFunctions.is_port_open?(ip, port, use_ssl)
146
def self.is_port_open?(ip, port, use_ssl=false)
148
Timeout::timeout(1) do
150
sock = TCPSocket.new(ip, port)
152
ssl_context = OpenSSL::SSL::SSLContext.new()
153
unless ssl_context.verify_mode
154
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
156
sslsocket = OpenSSL::SSL::SSLSocket.new(sock, ssl_context)
157
sslsocket.sync_close = true
166
rescue Timeout::Error
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)
178
remote_cmd = "ssh -i #{public_key_loc.join(' -i ')} -o StrictHostkeyChecking=no 2>&1 root@#{ip} '#{command}"
180
public_key_loc = File.expand_path(public_key_loc)
181
remote_cmd = "ssh -i #{public_key_loc} -o StrictHostkeyChecking=no root@#{ip} '#{command} "
185
remote_cmd << "> /tmp/#{ip}.log 2>&1 &' &"
187
remote_cmd << "> /dev/null 2>&1 &' &"
190
Kernel.system remote_cmd
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"
204
def self.scp_file(local_file_loc, remote_file_loc, target_ip, public_key_loc)
206
local_file_loc = File.expand_path(local_file_loc)
208
if public_key_loc.class == Array
209
public_key_loc.each { |key|
210
key = File.expand_path(key)
213
cmd = "scp -i #{public_key_loc.join(' -i ')} -o StrictHostkeyChecking=no 2>&1 #{local_file_loc} root@#{target_ip}:#{remote_file_loc}"
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}"
219
cmd << "; echo $? > ~/.appscale/retval"
221
retval_loc = File.expand_path("~/.appscale/retval")
222
FileUtils.rm_f(retval_loc)
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}")
231
break if File.exists?(retval_loc)
235
retval = (File.open(retval_loc) { |f| f.read }).chomp
239
break if retval == "0"
240
puts "\n\n[#{cmd}] returned #{retval} instead of 0 as expected. Will try to copy again momentarily..."
242
abort("SCP failed") if fails >= 5
244
CommonFunctions.shell("#{cmd}")
245
retval = (File.open(retval_loc) { |f| f.read }).chomp
253
puts "\nThis AppScale instance is linked to an e-mail address giving it administrator privileges."
256
print "Enter your desired administrator e-mail address: "
258
email = STDIN.gets.chomp
260
if email =~ EMAIL_REGEX
263
puts "The response you typed in was not an e-mail address. Please try again.\n\n"
270
def self.get_password
272
puts "\nThe new administrator password must be at least six characters long and can include non-alphanumeric characters."
275
system "stty -echo" # Turn off character echoing
276
print "Enter your new password: "
278
new_pass = STDIN.gets.chomp
279
print "\nEnter again to verify: "
281
verify_pass = STDIN.gets.chomp
282
system "stty echo" # Next release: find a platform independent solution
284
if new_pass == verify_pass
287
if pass =~ PASSWORD_REGEX
290
puts "\n\nThe password you typed in was not at least six characters long. Please try again.\n\n"
293
puts "\n\nPasswords entered do not match. Please try again.\n\n"
300
def self.get_from_yaml(keyname, tag, required=true)
301
location_file = File.expand_path("~/.appscale/locations-#{keyname}.yaml")
303
abort("An AppScale instance is not currently running with the provided keyname, \"#{keyname}\".") unless File.exists?(location_file)
306
tree = YAML.load_file(location_file)
309
abort("The yaml file you provided was malformed. Please correct any errors in it and try again.")
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
322
def self.get_load_balancer_ip(keyname, required=true)
323
return CommonFunctions.get_from_yaml(keyname, :load_balancer)
326
def self.get_load_balancer_id(keyname, required=true)
327
return CommonFunctions.get_from_yaml(keyname, :instance_id)
330
def self.get_table(keyname, required=true)
331
return CommonFunctions.get_from_yaml(keyname, :table, required)
334
def self.get_db_master_ip(keyname, required=true)
335
return CommonFunctions.get_from_yaml(keyname, :db_master, required)
338
def self.get_head_node_ip(keyname, required=true)
339
CommonFunctions.get_from_yaml(keyname, :shadow)
342
def self.get_secret_key(keyname, required=true)
343
CommonFunctions.get_from_yaml(keyname, :secret)
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)}
354
def self.get_random_alphanumeric(length=10)
356
possible = "0123456789abcdefghijklmnopqrstuvxwyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
357
possibleLength = possible.length
359
length.times { |index|
360
random << possible[rand(possibleLength)]
366
def self.get_appname_from_tar(fullpath)
367
appname, file, language = CommonFunctions.get_app_info(fullpath, PYTHON_CONFIG)
369
if appname.nil? or file.nil? or language.nil?
370
appname, file, language = CommonFunctions.get_app_info(fullpath, JAVA_CONFIG)
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.")
377
return appname, file, language
380
def self.move_app(temp_dir, filename, app_file, fullpath)
381
if File.directory?(fullpath)
383
`cp -r #{fullpath}/* /tmp/#{temp_dir}/`
385
abort("Copying the file to /tmp failed")
390
FileUtils.cp(fullpath, "/tmp/#{temp_dir}/#{filename}")
392
abort("Copying the file to /tmp failed")
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"
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..."
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
415
temp_dir = CommonFunctions.get_random_alphanumeric
416
FileUtils.rm_rf("/tmp/#{temp_dir}", :secure => true)
417
FileUtils.mkdir_p("/tmp/#{temp_dir}")
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)
426
if app_file == PYTHON_CONFIG
427
appname = CommonFunctions.get_appname_via_yaml(temp_dir, app_yaml_loc)
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"
438
elsif app_file == JAVA_CONFIG
439
appname = CommonFunctions.get_appname_via_xml(temp_dir, app_yaml_loc)
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"
450
FileUtils.rm_rf("/tmp/#{temp_dir}", :secure => true)
451
abort("appname was #{app_file}, which was not a recognized value.")
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}")
459
FileUtils.rm_rf("/tmp/#{temp_dir}", :secure => true)
460
CommonFunctions.warn_on_large_app_size(file)
461
return appname, file, language
464
def self.get_appname_via_yaml(temp_dir, app_yaml_loc)
465
app_yaml_loc = "/tmp/" + temp_dir + "/" + app_yaml_loc
468
tree = YAML.load_file(app_yaml_loc.chomp)
470
abort("The yaml file you provided was malformed. Please correct any errors in it and try again.")
473
appname = tree["application"]
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 == ""
487
def self.grab_file filename
488
filename = File.expand_path(filename)
491
content = File.open(filename) { |f| f.read.chomp! }