module RSpecSystem # Base class for a NodeSet driver. If you want to create a new driver, you # should sub-class this and override all the methods below. # # @abstract Subclass and override methods to create a new NodeSet variant. class NodeSet::Base attr_reader :config attr_reader :setname attr_reader :custom_prefabs_path attr_reader :nodes attr_reader :destroy # @!group Abstract Methods # Create new NodeSet, populating necessary data structures. # # @abstract override for providing global storage and setup-level code def initialize(setname, config, custom_prefabs_path, options) @setname = setname @config = config @custom_prefabs_path = custom_prefabs_path @destroy = options[:destroy] @nodes = {} config['nodes'].each do |k,v| @nodes[k] = RSpecSystem::Node.node_from_yaml(self, k, v, custom_prefabs_path) end end # Setup the NodeSet by starting all nodes. # # @return [void] def setup launch connect configure end # Launch nodes # # @return [void] # @abstract Override this method and provide your own launch code def launch raise RuntimeError "Unimplemented method #launch" end # Connect nodes # # @return [void] # @abstract Override this method and provide your own connect code def connect raise RuntimeError "Unimplemented method #connect" end # Configure nodes # # This is the global configure method that sets up a node before tests are # run, making sure any important preparation steps are executed. # # * fixup profile to stop using mesg to avoid extraneous noise # * ntp synchronisation # * hostname & hosts setup # # @return [void] # @abstract Override this method and provide your own configure code def configure nodes.each do |k,v| rs_storage = RSpec.configuration.rs_storage[:nodes][k] # Fixup profile to avoid noise if v.facts['osfamily'] == 'Debian' shell(:n => k, :c => "sed -i 's/^mesg n/# mesg n/' /root/.profile") end # Setup ntp if v.facts['osfamily'] == 'Debian' then shell(:n => k, :c => 'apt-get install -y ntpdate') elsif v.facts['osfamily'] == 'RedHat' then if v.facts['lsbmajdistrelease'] == '5' then shell(:n => k, :c => 'yum install -y ntp') else shell(:n => k, :c => 'yum install -y ntpdate') end end shell(:n => k, :c => 'ntpdate -u pool.ntp.org') # Grab IP address for host, if we don't already have one rs_storage[:ipaddress] ||= shell(:n => k, :c => "ip a|awk '/g/{print$2}' | cut -d/ -f1 | head -1").stdout.chomp # Configure local hostname and hosts file shell(:n => k, :c => "hostname #{k}") if v.facts['osfamily'] == 'Debian' then shell(:n => k, :c => "echo '#{k}' > /etc/hostname") end hosts = <<-EOS #{rs_storage[:ipaddress]} #{k} 127.0.0.1 #{k} localhost ::1 #{k} localhost EOS shell(:n => k, :c => "echo '#{hosts}' > /etc/hosts") # Display setup for diagnostics shell(:n => k, :c => 'cat /etc/hosts') shell(:n => k, :c => 'hostname') shell(:n => k, :c => 'hostname -f') end nil end # Shutdown the NodeSet by shutting down or pausing all nodes. # # @return [void] # @abstract Override this method and provide your own node teardown code def teardown raise RuntimeError "Unimplemented method #teardown" end # Run a command on a host in the NodeSet. # # @param opts [Hash] options hash containing :n (node) and :c (command) # @return [Hash] a hash containing :stderr, :stdout and :exit_code # @abstract Override this method providing your own shell running code def run(opts) dest = opts[:n].name cmd = opts[:c] ssh = RSpec.configuration.rs_storage[:nodes][dest][:ssh] ssh_exec!(ssh, cmd) end # Copy a file to the host in the NodeSet. # # @param opts [Hash] options # @option opts [RSpecHelper::Node] :d destination node # @option opts [String] :sp source path # @option opts [String] :dp destination path # @return [Boolean] returns true if command succeeded, false otherwise # @abstract Override this method providing your own file transfer code def rcp(opts) dest = opts[:d].name source = opts[:sp] dest_path = opts[:dp] # Do the copy and print out results for debugging ssh = RSpec.configuration.rs_storage[:nodes][dest][:ssh] begin ssh.scp.upload! source.to_s, dest_path.to_s, :recursive => true rescue => e log.error("Error with scp of file #{source} to #{dest}:#{dest_path}") raise e end true end # @!group Common Methods # Return environment type def provider_type self.class::PROVIDER_TYPE end # Return default node # # @return [RSpecSystem::Node] default node for this nodeset def default_node dn = config['default_node'] if dn.nil? if nodes.length == 1 dn = nodes.first[1] return dn else raise "No default node" end else return nodes[dn] end end # Return a random string of chars, used for temp dir creation # # @return [String] string of 50 random characters A-Z and a-z def random_string(length = 50) o = [('a'..'z'),('A'..'Z')].map{|i| i.to_a}.flatten (0...length).map{ o[rand(o.length)] }.join end # Generates a random string for use in remote transfers. # # @return [String] a random path # @todo Very Linux dependant, probably need to consider OS X and Windows at # least. def tmppath '/tmp/' + random_string end # Connect via SSH in a resilient way # # @param [Hash] opts # @option opts [String] :host Host to connect to # @option opts [String] :user User to connect as # @option opts [Hash] :net_ssh_options Options hash as used by `Net::SSH.start` # @return [Net::SSH::Connection::Session] # @api protected def ssh_connect(opts = {}) ssh_sleep = RSpec.configuration.rs_ssh_sleep ssh_tries = RSpec.configuration.rs_ssh_tries ssh_timeout = RSpec.configuration.rs_ssh_timeout tries = 0 begin timeout(ssh_timeout) do output << bold(color("localhost$", :green)) << " ssh -l #{opts[:user]} #{opts[:host]}\n" Net::SSH.start(opts[:host], opts[:user], opts[:net_ssh_options]) end rescue Timeout::Error, SystemCallError => e tries += 1 output << e.message << "\n" if tries < ssh_tries log.info("Sleeping for #{ssh_sleep} seconds then trying again ...") sleep ssh_sleep retry else log.error("Inability to connect to host, already tried #{tries} times, throwing exception") raise e end end end # Execute command via SSH. # # A special version of exec! from Net::SSH that returns exit code and exit # signal as well. This method is blocking. # # @param ssh [Net::SSH::Connection::Session] an active ssh session # @param command [String] command to execute # @return [Hash] a hash of results def ssh_exec!(ssh, command) r = { :stdout => '', :stderr => '', :exit_code => nil, :exit_signal => nil, } ssh.open_channel do |channel| channel.exec(command) do |ch, success| unless success abort "FAILED: couldn't execute command (ssh.channel.exec)" end channel.on_data do |ch,data| d = data output << d r[:stdout]+=d end channel.on_extended_data do |ch,type,data| d = data output << d r[:stderr]+=d end channel.on_request("exit-status") do |ch,data| c = data.read_long output << bold("Exit code:") << " #{c}\n" r[:exit_code] = c end channel.on_request("exit-signal") do |ch, data| s = data.read_string output << bold("Exit signal:") << " #{s}\n" r[:exit_signal] = s end end end ssh.loop r end # Return a random mac address # # @return [String] a random mac address def randmac "080027" + (1..3).map{"%0.2X"%rand(256)}.join end end end