require 'thread'
require 'net/ssh'
require 'yaml'
tools_home = `which appscale-run-instances`
if tools_home.length > 0
# AppScale-Tools are installed on the local machine
lib_dir = File.join(File.dirname(tools_home), "..", "lib")
tools_impl = File.join(lib_dir, "appscale_tools.rb")
if File.exists?(tools_impl)
# AppScale-Tools have been installed manually
# by building the source or by similar means.
# (as opposed to installing the appscale-tools gem)
# Add the lib directory into the load path.
$:.unshift lib_dir
end
end
require 'appscale_tools'
$mutex = Mutex.new
def report_error(title, msg)
@title = title
@error = msg
return erb :error
end
def validate_appscale_credentials(user, pass1, pass2)
if user.nil? or user.length == 0
return [false, "Administrator username not provided"]
elsif pass1.nil? or pass2.nil? or pass1.length == 0 or pass2.length == 0
return [false, "Administrator password not provided"]
elsif pass1 != pass2
return [false, "Password entries do not match"]
elsif pass1.length < 6
return [false, "Password must contain at least 6 characters"]
else
return [true, '']
end
end
def validate_ssh_credentials(keyname, root_password, ips_yaml)
if keyname.nil? or keyname.length == 0
return [false, "AppScale key name not provided"]
elsif root_password.nil? or root_password == 0
return [false, "Root password for AppScale machines not provided"]
else
# Randomly pick a server and try to connect to it via SSH using the
# provided credentials. This will guard againt invalid root passwords
# entered by the user and any obvious network level issues which might
# prevent AppsCake from connecting to the specified machines. The test
# assumes that all hosts can be accessed using the same root password
# and they are on the same network (if one is reachable, then all are
# reachable) which is the case for most typical AppScale deployments.
# In scenarios where the above assumption is not the case (i.e. some
# machines are reachable and some are not) the test may pass, but the
# final deployment may fail. These runtime errors will go to the deployment
# logs generated by AppsCake per each deployment which can also be accessed
# over the web.
node_layout = NodeLayout.new(ips_yaml, {:database => "cassandra"})
ips = node_layout.nodes.collect { |node| node.id }
begin
Net::SSH.start(ips[0], 'root', :password => root_password, :timeout => 10) do |ssh|
ssh.exec('ls')
end
rescue Timeout::Error
return [false, "Connection timed out for #{ips[0]}"]
rescue Errno::EHOSTUNREACH
return [false, "Host unreachable error for #{ips[0]}"]
rescue Errno::ECONNREFUSED
return [false, "Connection refused for #{ips[0]}"]
rescue Net::SSH::AuthenticationFailed
return [false, "Authentication failed for #{ips[0]} - Please ensure that the specified" +
" root password is correct"]
rescue Exception => e
return [false, "Unexpected runtime error connecting to #{ips[0]}"]
end
return [true, '']
end
end
def validate_yaml(yaml_str)
if yaml_str.nil? or yaml_str.length == 0
return [false, "ips.yaml configuration not provided"]
end
yaml = YAML.load(yaml_str)
node_layout = NodeLayout.new(yaml, {})
if !node_layout.valid?
errors = node_layout.errors
error_result = ""
for error in errors
if !error.nil? and error.length > 0
if error_result.length > 0
error_result += ", "
end
error_result += error
end
end
return [false, error_result]
end
success_result = ""
yaml.each do |symbol, value|
role = symbol.to_s
success_result += "
#{role}
"
if value.kind_of?(Array)
value.each do |val|
success_result += "- #{val}
"
end
else
success_result += "- #{value}
"
end
success_result += "
"
end
[true, success_result, yaml]
end
def validate_ec2_cluster_settings(min, max, ami)
if min.nil? or min.length == 0
return [false, "Minimum number of nodes unspecified"]
elsif max.nil? or max.length == 0
return [false, "Maximum number of nodes unspecified"]
elsif ami.nil? or ami.length == 0
return [false, "AMI ID not specified"]
elsif min.to_i <= 0
return [false, "Minimum number of nodes must be positive"]
elsif max.to_i < min.to_i
return [false, "Maximum number of nodes must not be smaller than the minimum numberof nodes"]
else
return [true, ""]
end
end
def validate_ec2_credentials(username, access_key, secret_key, region)
if username.nil? or username.length == 0
return [false, "EC2 username not specified"]
elsif access_key.nil? or access_key.length == 0
return [false, "EC2 access key not specified"]
elsif secret_key.nil? or secret_key.length == 0
return [false, "EC2 secret key not specified"]
elsif region.nil? or region.length == 0
return [false, "EC2 region not specified"]
else
output = CommonFunctions::shell("ec2-describe-regions -O #{access_key} -W #{secret_key}")
if output.nil? or output.length == 0
return [false, "Unable to execute EC2 command line tools"]
elsif output.include?"AuthFailure"
return [false, "EC2 authentication failed. Invalid EC2 access key or/and secret key."]
elsif !output.include?region
return [false, "Invalid EC2 region. This region is not available for your account."]
end
end
[true, ""]
end
def validate_ec2_certificate_uploads(username, pk_upload, cert_upload)
if username.nil? or username.length == 0
return [false, "EC2 username not specified"]
elsif pk_upload.nil?
return [false, "Primary key not uploaded"]
elsif pk_upload[:type] != "application/x-x509-ca-cert" and
pk_upload[:type] != "application/x-pem-file"
return [false, "Invalid primary key format: #{pk_upload[:type]}"]
elsif cert_upload.nil?
return [false, "X509 certificate not uploaded"]
elsif cert_upload[:type] != "application/x-x509-ca-cert" and
cert_upload[:type] != "application/x-pem-file"
return [false, "Invalid certificate format: #{cert_upload[:type]}"]
else
timestamp = Time.now.to_i
cert_dir = File.expand_path(File.join(File.dirname(__FILE__), "..", "certificates"))
File.open(File.join(cert_dir, "#{username}_#{timestamp}_pk.pem"), "w") do |f|
f.write(pk_upload[:tempfile].read)
end
File.open(File.join(cert_dir, "#{username}_#{timestamp}_cert.pem"), "w") do |f|
f.write(cert_upload[:tempfile].read)
end
end
[true, timestamp]
end
def locked?
$mutex.synchronize do
return File.exists?('appscake.lock')
end
end
def lock
$mutex.synchronize do
if !File.exists?('appscake.lock')
File.open('appscake.lock', 'w') do |file|
file.write('AppsCake.lock')
end
return true
else
return false
end
end
end
def unlock
$mutex.synchronize do
if File.exists?('appscake.lock')
File.delete('appscake.lock')
return true
else
return false
end
end
end
def deploy(options)
30.times do |i|
puts "Deploying..."
sleep(1)
end
end
def add_key(options)
puts "Generating RSA keys..."
end
def redirect_standard_io(timestamp)
begin
orig_stderr = $stderr.clone
orig_stdout = $stdout.clone
log_path = File.join(File.expand_path(File.dirname(__FILE__)), "..", "logs")
$stderr.reopen File.new(File.join(log_path, "deploy-#{timestamp}.log"), "w")
$stderr.sync = true
$stdout.reopen File.new(File.join(log_path, "deploy-#{timestamp}.log"), "w")
$stdout.sync = true
retval = yield
rescue Exception => e
puts "[__ERROR__] Runtime error in deployment process: #{e.message}"
$stdout.reopen orig_stdout
$stderr.reopen orig_stderr
raise e
ensure
$stdout.reopen orig_stdout
$stderr.reopen orig_stderr
end
retval
end
# Initiates a task in the background to deploy AppScale on a virtualized
# cluster. Returns a 3-element array as the result of the operation. The
# first element of the array is a boolean value indicating success or
# failure. In case of success, the second value will be the timestamp
# on which the task was launched. The third value will be the process ID
# of the newly launched task. In case of failure the second and third values
# will provide detailed error information.
def deploy_on_virtual_cluster(params, add_key_options, run_instances_options)
if lock
begin
timestamp = Time.now.to_i
pid = fork do
begin
redirect_standard_io(timestamp) do
key_file = File.expand_path("~/.appscale/#{params[:virtual_keyname]}")
if File.exists?(key_file)
puts "AppScale key '#{params[:virtual_keyname]}' found on the disk. Reusing..."
else
puts "AppScale key '#{params[:virtual_keyname]}' not found on the disk. Generating..."
AppScaleTools.add_keypair(add_key_options)
end
AppScaleTools.run_instances(run_instances_options)
end
ensure
# If the fork was successful, the sub-process should release the lock
unlock
end
end
Process.detach(pid)
return [true, timestamp, pid]
rescue Exception => e
# If something went wrong with the fork, release the lock immediately and return
unlock
return [false, "Unexpected Runtime Error", "Runtime error while executing" +
" appscale tools: #{e.message}"]
end
else
return [false, "Server Busy", "AppsCake is currently busy deploying a cloud." +
" Please try again later."]
end
end
def deploy_on_ec2(params, run_instances_options, cert_timestamp)
if lock
begin
timestamp = Time.now.to_i
pid = fork do
ENV['EC2_REGION'] = params[:region]
ENV['EC2_PRIVATE_KEY'] = File.join(File.dirname(__FILE__), "..", "certificates",
"#{params[:username]}_#{cert_timestamp}_pk.pem")
ENV['EC2_CERT'] = File.join(File.dirname(__FILE__), "..", "certificates",
"#{params[:username]}_#{cert_timestamp}_cert.pem")
ENV['EC2_ACCESS_KEY'] = params[:access_key]
ENV['EC2_SECRET_KEY'] = params[:secret_key]
ENV['EC2_JVM_ARGS'] = "-Djavax.net.ssl.trustStore=#{ENV['JAVA_HOME']}/lib/security/cacerts"
ENV['EC2_URL'] = "https://ec2.#{params[:region]}.amazonaws.com"
ENV['S3_URL'] = "https://s3.amazonaws.com:443"
begin
redirect_standard_io(timestamp) do
AppScaleTools.run_instances(run_instances_options)
end
ensure
# If the fork was successful, the sub-process should release the lock
unlock
end
end
Process.detach(pid)
return [true, timestamp, pid]
rescue Exception => e
# If something went wrong with the fork, release the lock immediately and return
unlock
return [false, "Unexpected Runtime Error", "Runtime error while executing" +
" appscale tools: #{e.message}"]
end
else
return [false, "Server Busy", "AppsCake is currently busy deploying a cloud." +
" Please try again later."]
end
end