# This file is part of RestConnection # # RestConnection is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # RestConnection is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with RestConnection. If not, see . require 'rest_connection/ssh_hax' class Server include RightScale::Api::Base extend RightScale::Api::BaseExtend include SshHax include RightScale::Api::Taggable extend RightScale::Api::TaggableExtend def self.filters [ :aws_id, :created_at, :deployment_href, :ip_address, :nickname, :private_ip_address, :updated_at ] end def self.create(opts) create_options = Hash.new create_options[self.resource_singular_name.to_sym] = opts create_options["cloud_id"] = opts[:cloud_id] if opts[:cloud_id] create_options[self.resource_singular_name.to_sym][:mci_href] = nil create_options[self.resource_singular_name.to_sym][:inputs] = nil location = connection.post(self.resource_plural_name,create_options) newrecord = self.new('href' => location) newrecord.reload newrecord.parameters #transform the parameters! newrecord end # The RightScale api returns the server parameters as a hash with "name" and "value". # This must be transformed into a hash in case we want to PUT this back to the API. def transform_parameters(parameters) new_params_hash = {} parameters.each do |parameter_hash| new_params_hash[parameter_hash["name"]] = parameter_hash["value"] end new_params_hash end # Since RightScale hands back the parameters with a "name" and "value" tags we should # transform them into the proper hash. This it the same for setting and getting. def parameters # if the parameters are an array of hashes, that means we need to transform. if @params['parameters'].is_a?(Array) @params['parameters'] = transform_parameters(@params['parameters']) end @params['parameters'] ||= {} end # This is overriding the default save with one that can massage the parameters def save self.parameters #transform the parameters, if they're not already!! self.tags #transform the tags, if they're not already!! uri = URI.parse(self.href) connection.put(uri.path, resource_singular_name.to_sym => @params) end # waits until the specified state is reached for this Server # *st <~String> the name of the state to wait for, eg. "operational" # *timeout <~Integer> optional, how long to wait for the state before declare failure (in seconds). def wait_for_state(st,timeout=1200) reload connection.logger("#{nickname} is #{self.state}") step = 15 catch_early_terminated = 60 / step while(timeout > 0) return true if state =~ /#{st}/ return true if state =~ /terminated|stopped/ && st =~ /terminated|stopped/ raise "FATAL error, this server is stranded and needs to be #{st}: #{nickname}, see audit: #{self.audit_link}" if state.include?('stranded') && !st.include?('stranded') raise "FATAL error, this server went to error state and needs to be #{st}: #{nickname}, see audit: #{self.audit_link}" if state.include?('error') && st !~ /error|terminated|stopped/ connection.logger("waiting for server #{nickname} to go #{st}, state is #{state}") if state =~ /terminated|stopped|inactive|error/ and st !~ /terminated|stopped|inactive|error/ if catch_early_terminated <= 0 raise "FATAL error, this server terminated when waiting for #{st}: #{nickname}" end catch_early_terminated -= 1 end sleep step timeout -= step reload end raise "FATAL, this server #{self.audit_link} timed out waiting for the state to be #{st}" if timeout <= 0 end # waits until the server is operational and dns_name is available def wait_for_operational_with_dns(state_wait_timeout=1200) timeout = 600 wait_for_state("operational", state_wait_timeout) step = 15 while(timeout > 0) self.settings break if self.reachable_ip connection.logger "waiting for IP for #{self.nickname}" sleep step timeout -= step end connection.logger "got IP: #{self.reachable_ip}" raise "FATAL, this server #{self.audit_link} timed out waiting for IP" if timeout <= 0 end def audit_link # proof of concept for now # server_id = self.href.split(/\//).last # audit_href = "https://my.rightscale.com/servers/#{server_id}#auditentries" audit_href = self.href.gsub(/api\//,"") + "#auditentries" "#{audit_href}" end def start if self.state == "stopped" t = URI.parse(self.href) return connection.post(t.path + '/start') else connection.logger("WARNING: was in #{self.state} so skiping start call") end end def stop # All instances will have a valid href including EBS instances that are "stopped" if self.current_instance_href t = URI.parse(self.href) connection.post(t.path + '/stop') else connection.logger("WARNING: was in #{self.state} and had a current_instance_href so skiping stop call") end end def force_stop if self.current_instance_href t = URI.parse(self.href) connection.post(t.path + '/stop') connection.post(t.path + '/stop') else connection.logger("WARNING: was in #{self.state} and had a current_instance_href so skiping stop call") end end # Uses ServerInternal api to start and stop EBS based instances def start_ebs # @server_internal = ServerInternal.new(:href => self.href) # @server_internal.start if self.state == "stopped" t = URI.parse(self.href) return connection.post(t.path + '/start_ebs') else connection.logger("WARNING: was in #{self.state} so skiping start_ebs call") end end def stop_ebs # @server_internal = ServerInternal.new(:href => self.href) # @server_internal.stop if self.current_instance_href t = URI.parse(self.href) connection.post(t.path + '/stop_ebs') else connection.logger("WARNING: was in #{self.state} and had a current_instance_href so skiping stop_ebs call") end end # Works on v4 and v5 images. # *executable can be an <~Executable> or <~RightScript> def run_executable(executable, opts=nil, ignore_lock=false) script_options = Hash.new script_options[:server] = Hash.new if executable.is_a?(Executable) if executable.recipe? script_options[:server][:recipe] = executable.recipe else script_options[:server][:right_script_href] = executable.right_script.href end elsif executable.is_a?(RightScript) script_options[:server][:right_script_href] = executable.href else raise "Invalid class passed to run_executable, needs Executable or RightScript, was:#{executable.class}" end if not opts.nil? and opts.has_key?(:ignore_lock) script_options[:server][:ignore_lock] = "true" opts.delete(:ignore_lock) end serv_href = URI.parse(self.href) script_options[:server][:parameters] = opts unless opts.nil? script_options[:server][:ignore_lock] = "true" if ignore_lock location = connection.post(serv_href.path + '/run_executable', script_options) AuditEntry.new('href' => location) end # This should be used with v4 images only. def run_script(script,opts=nil) if script.is_a?(Executable) script = script.right_script end serv_href = URI.parse(self.href) script_options = Hash.new script_options[:server] = Hash.new script_options[:server][:right_script_href] = script.href script_options[:server][:parameters] = opts unless opts.nil? location = connection.post(serv_href.path + '/run_script', script_options) Status.new('href' => location) end def set_input(name, value) serv_href = URI.parse(self.href) connection.put(serv_href.path, :server => {:parameters => {name.to_sym => value} }) end def set_inputs(hash = {}) set_current_inputs(hash) set_next_inputs(hash) end def set_current_inputs(hash = {}) if self.current_instance_href and self.state != "stopped" self.reload_as_current serv_href = URI.parse(self.href) connection.put(serv_href.path, :server => {:parameters => hash}) settings self.reload_as_next end end def set_next_inputs(hash = {}) serv_href = URI.parse(self.href) connection.put(serv_href.path, :server => {:parameters => hash}) settings end def set_template(href) serv_href = URI.parse(self.href) connection.put(serv_href.path, :server => {:server_template_href => href}) end def settings serv_href = URI.parse(self.href) @params.merge! connection.get(serv_href.path + "/settings") end def attach_volume(params) hash = {} hash[:server] = params serv_href = URI.parse(self.href) connection.post(serv_href.path + "/attach_volume", hash) end def get_sketchy_data(params = {}) serv_href = URI.parse(self.href) @params.merge! connection.get(serv_href.path + "/get_sketchy_data", params) end def monitoring serv_href = URI.parse(self.href) @params.merge! connection.get(serv_href.path + "/monitoring") end # takes Bool argument to wait for state change (insurance that we can detect a reboot happened) def reboot(wait_for_state = false) reload old_state = self.state serv_href = URI.parse(self.href) connection.post(serv_href.path + "/reboot") if wait_for_state wait_for_state_change(old_state) end end def alert_specs # TODO end def relaunch self.stop self.wait_for_state("stopped") self.start end def wait_for_state_change(old_state = nil) timeout = 60*7 timer = 0 while(timer < timeout) reload old_state = self.state unless old_state connection.logger("#{nickname} is #{self.state}") return true if self.state != old_state sleep 30 timer += 30 connection.logger("waiting for server #{nickname} to change from #{old_state} state.") end raise("FATAL: timeout after #{timeout}s waiting for state change") end # Reload the server's basic information from the current server instance def reload_as_current uri = URI.parse(self.href + "/current") @params.merge! connection.get(uri.path) end # Reload the server's basic information from the next server instance def reload_as_next uri = URI.parse(self.href.gsub(/\/current/,"")) @params.merge! connection.get(uri.path) end # Complex logic for determining the cloud_id of even a stopped server def cloud_id self.settings if self.state == "operational" return self["cloud_id"] end cloud_ids = RestConnection::AWS_CLOUDS.map { |hsh| hsh["cloud_id"] } api0_1 = false begin api0_1 = true if Ec2SshKeyInternal.find_all rescue end # Try ssh keys if self.ec2_ssh_key_href and api0_1 ref = self.ec2_ssh_key_href cloud_ids.each { |cloud| if Ec2SshKeyInternal.find_by_cloud_id(cloud.to_s).select { |o| o.href == ref }.first return cloud end } end # Try security groups if self.ec2_security_groups_href self.ec2_security_groups_href.each { |sg| cloud_ids.each { |cloud| if Ec2SecurityGroup.find_by_cloud_id(cloud.to_s).select { |o| o.href == sg }.first return cloud end } } end raise "Could not determine cloud_id...try setting an ssh key or security group" end def run_executable_and_wait_for_completed(executable, opts=nil) run_executable(executable, opts).wait_for_completed end def dns_name self.settings unless self["dns_name"] self["dns_name"] end def private_ip self.settings unless @params["private-ip-address"] @params["private-ip-address"] end def reachable_ip return self.dns_name if self.dns_name return self.private_ip if self.private_ip nil end # Override Taggable mixin so that it sets tags on both next and current instances def tags(*args) self.reload_as_next if self.href =~ /current/ super(*args) end def current_tags(reload=true) self.reload_as_next if self.href =~ /current/ ret = [] if self.current_instance_href ret = Tag.search_by_href(self.current_instance_href).map { |h| h["name"] } end ret end def add_tags(*args) self.reload_as_next if self.href =~ /current/ return false if args.empty? args.uniq! Tag.set(self.href, args) Tag.set(self.current_instance_href, args) if self.current_instance_href self.tags(true) end def remove_tags(*args) self.reload_as_next if self.href =~ /current/ return false if args.empty? args.uniq! Tag.unset(self.href, args) Tag.unset(self.current_instance_href, args) if self.current_instance_href self.tags(true) end def get_tags_by_namespace(namespace) ret = {} tags = {"self" => self.tags(true)} tags["current_instance"] = self.current_tags if self.current_instance_href tags.each { |res,ary| ret[res] ||= {} ary.each { |tag| next unless tag.start_with?("#{namespace}:") key = tag.split("=").first.split(":")[1..-1].join(":") value = tag.split(":")[1..-1].join(":").split("=")[1..-1].join("=") ret[res][key] = value } } return ret end def clear_tags(namespace = nil) tags = self.tags(true) tags.deep_merge! self.current_tags if self.current_instance_href tags = tags.select { |tag| tag.start_with?("#{namespace}:") } if namespace self.remove_tags(*tags) end end