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}

" 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