diff --git a/bin/rudy b/bin/rudy index dbe2b051..2ecab4c9 100755 --- a/bin/rudy +++ b/bin/rudy @@ -10,7 +10,7 @@ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') # Put our local lib in first place $:.unshift File.join(File.dirname(__FILE__), '..', 'vendor', 'highline-1.5.1', 'lib') -%w{amazon-ec2 drydock rye}.each { |dir| $:.unshift File.join(File.dirname(__FILE__), '..', '..', dir, 'lib') } +%w{amazon-ec2 caesars drydock rye}.each { |dir| $:.unshift File.join(File.dirname(__FILE__), '..', '..', dir, 'lib') } #require 'rubygems' #$SAFE = 1 # require is unsafe in Ruby 1.9?? diff --git a/lib/rudy/cli/routines.rb b/lib/rudy/cli/routines.rb index 90a10596..291d688a 100644 --- a/lib/rudy/cli/routines.rb +++ b/lib/rudy/cli/routines.rb @@ -15,8 +15,8 @@ def startup if @@global.environment == @@config.defaults.environment && @@global.role == @@config.defaults.role - puts - puts "Login with: rudy -u root ssh" + #puts + #puts "Login with: rudy -u root ssh" end end diff --git a/lib/rudy/config.rb b/lib/rudy/config.rb index 3bfac296..236b35ef 100644 --- a/lib/rudy/config.rb +++ b/lib/rudy/config.rb @@ -10,6 +10,7 @@ class Config < Caesars::Config dsl Rudy::Config::Routines::DSL dsl Rudy::Config::Machines::DSL dsl Rudy::Config::Networks::DSL + dsl Rudy::Config::Controls::DSL # TODO: auto-generate in caesars def accounts?; self.respond_to?(:accounts) && !self[:accounts].nil?; end diff --git a/lib/rudy/config/objects.rb b/lib/rudy/config/objects.rb index acdc345d..54e2d10e 100644 --- a/lib/rudy/config/objects.rb +++ b/lib/rudy/config/objects.rb @@ -18,12 +18,28 @@ class Defaults < Caesars class Networks < Caesars end + class Controls < Caesars + end + class Routines < Caesars forced_hash :create forced_hash :destroy forced_hash :restore forced_hash :mount - + forced_hash :before + + # Add remote shell commands to the DSL as forced Arrays. + # Example: + # ls :a, :l, "/tmp" # => :ls => [[:a, :l, "/tmp"]] + # ls :o # => :ls => [[:a, :l, "/tmp"], [:o]] + # NOTE: Beware of namespace conflicts in other areas of the DSL, + # specifically shell commands that have the same name as a keyword + # we want to use in the DSL. This includes commands that were added + # to Rye::Cmd before Rudy is 'require'd. + Rye::Cmd.instance_methods.each do |cmd| + forced_array cmd + end + end end \ No newline at end of file diff --git a/lib/rudy/routines.rb b/lib/rudy/routines.rb index 2afe2780..30880875 100644 --- a/lib/rudy/routines.rb +++ b/lib/rudy/routines.rb @@ -54,7 +54,8 @@ def generic_machine_runner(machine_action, routine, &routine_action) end rmach.send(machine_action) do |machine| - puts machine.liner_note + puts machine_separator(machine.name, machine.awsid) + print "Waiting for instance..." isup = Rudy::Utils.waiter(3, 120, STDOUT, "it's up!", 2) { inst = machine.get_instance @@ -66,6 +67,7 @@ def generic_machine_runner(machine_action, routine, &routine_action) Rudy::Utils.service_available?(machine.dns_public, 22) } + opts = { :keys => root_keypairpath, :user => 'root', :debug => nil } rbox = Rye::Box.new(machine.dns_public, opts) @@ -112,10 +114,12 @@ def task_separator(title) ("%s=== %s %s" % [$/, title, '='*dashes]) end - def machine_separator(title) - dashes = 52 - title.size # + def machine_separator(name, awsid) + dashes = 60 - name.size # dashes = 0 if dashes < 1 - ("%s=== %s %s" % [$/, title.bright, '='*dashes]).bright.color(:blue) + puts $/, '='*60 + puts 'MACHINE: %-40s (%s)' % [name.bright, awsid] + puts '='*60, $/ end end diff --git a/lib/rudy/routines/helpers/scripthelper.rb b/lib/rudy/routines/helpers/scripthelper.rb index c1f9e0e6..0be64404 100644 --- a/lib/rudy/routines/helpers/scripthelper.rb +++ b/lib/rudy/routines/helpers/scripthelper.rb @@ -42,6 +42,7 @@ def execute_command?(timing, routine) (routine.is_a?(Caesars::Hash) && routine.has_key?(timing)) end + # Returns a formatted string for printing command info def command_separator(cmd, user) cmd ||= "" spaces = 52 - cmd.size @@ -69,68 +70,55 @@ def execute_command(timing, routine, sconf, hostname, rbox) def rbox.rm(*args); cmd('rm', args); end if execute_command?(timing, routine) # i.e. before_local? - #puts "Connecting to #{hostname}" # TODO: verbose mode - begin - rbox.connect - rescue Net::SSH::AuthenticationFailed, Net::SSH::HostKeyMismatch => ex - STDERR.puts "Error connecting: #{ex.message}".color(:red) - STDERR.puts "Skipping scripts".color(:red) - end original_user = rbox.user - scripts = [routine[timing]].flatten - scripts.each do |script| - + users = routine[timing] || {} + users.each_pair do |user, commands| begin - user, command, *args = script.to_a.flatten.compact - - # TODO: Move the following to Rye. - # If there's a command with no args, we could have been given something - # like "ls -l /tmp". Safe-mode Rye requires the command to be sent - # separately so this is a quick-fix to do that. - if command && command.index(' ') && args.empty? - command, *args = command.strip.scan(/\A(.+?)\s(.+)/).flatten - end - rbox.switch_user user # does nothing if it's the same user - - if command.nil? || command.empty? - puts command_separator("No command specified", user) - next - end - - puts command_separator(rbox.preview_command(command, args), user) - - # NOTE: can we put this only in verbose mode? - #puts " Creating #{@@script_config_file}" - + rbox.connect(false) # does nothing if already connected + rescue Net::SSH::AuthenticationFailed, Net::SSH::HostKeyMismatch => ex + STDERR.puts "Error connecting: #{ex.message}".color(:red) + STDERR.puts "Skipping user #{user}".color(:red) + next + end + + begin # We need to create the config file for every script, # b/c the user may change and it would not be accessible. # We turn off safe mode so we can write the config file via SSH. # This will need to use SCP eventually; it is unsafe and error prone. rbox.safe = false + rbox.umask = "0077" # Ensure script is not readable conf_str = sconf.to_hash.to_yaml.tr("'", "''") puts rbox.echo("'#{conf_str}' > #{@@script_config_file}") rbox.safe = true rbox.chmod(600, @@script_config_file) - - - ret = rbox.send(command, args) - - puts ' ' << ret.stdout.join("#{$/} ") if !ret.stdout.empty? - puts " STDERR: #{ret.stderr.join("#{$/} ")}".color(:red) if !ret.stderr.empty? - - rescue Rye::CommandError => ex - puts " Exit code: #{ex.exit_code}".color(:red) - puts " STDERR: #{ex.stderr.join("#{$/} ")}".color(:red) - puts " STDOUT: #{ex.stdout.join("#{$/} ")}".color(:red) - rescue Rye::CommandNotFound => ex - puts " CommandNotFound: #{ex.message}".color(:red) + rescue => ex end - + commands.each_pair do |command, calls| + calls.each do |args| + begin + puts command_separator(rbox.preview_command(command, args), user) + ret = rbox.send(command, args) + puts ' ' << ret.stdout.join("#{$/} ") if !ret.stdout.empty? + STDERR.puts " STDERR: #{ret.stderr.join("#{$/} ")}".color(:red) if !ret.stderr.empty? + rescue Rye::CommandError => ex + STDERR.puts " Exit code: #{ex.exit_code}".color(:red) + STDERR.puts " STDERR: #{ex.stderr.join("#{$/} ")}".color(:red) + STDERR.puts " STDOUT: #{ex.stdout.join("#{$/} ")}".color(:red) + rescue Rye::CommandNotFound => ex + STDERR.puts " CommandNotFound: #{ex.message}".color(:red) + STDERR.puts ex.backtrace + end + end + end + rbox.rm(@@script_config_file) end + + # Return the borrowed rbox instance to the user it was provided with rbox.switch_user original_user else puts "Nothing to do" diff --git a/lib/rudy/routines/release.rb b/lib/rudy/routines/release.rb index 2fb26452..3236163a 100644 --- a/lib/rudy/routines/release.rb +++ b/lib/rudy/routines/release.rb @@ -11,8 +11,10 @@ def execute #puts rel generic_machine_runner(:list, routine) do |machine,rbox| - puts task_separator("CREATING REMOTE CHECKOUT") - #scm.create_remote_checkout(rbox) + if scm + puts task_separator("CREATING REMOTE CHECKOUT") + scm.create_remote_checkout(rbox) + end end puts "Done" diff --git a/lib/rudy/scm/git.rb b/lib/rudy/scm/git.rb index 2da15b24..7dea9bb8 100644 --- a/lib/rudy/scm/git.rb +++ b/lib/rudy/scm/git.rb @@ -1,14 +1,129 @@ require 'date' - +require 'grit' + module Rudy module SCM + class NotAWorkingCopy < Rudy::Error + def message + "You must be in the main directory of your working copy" + end + end + class RemoteError < Rudy::Error; end + class NoRemoteURI < Rudy::Error; end + class TooManyTags < Rudy::Error + def message; "Too many tag creation attempts!"; end + end + class NoRemotePath < Rudy::Error + def message + "Add a path for #{@obj} in your routines config" + end + end + class GIT + include Grit + attr_accessor :base_uri + attr_reader :repo, :rbox + attr_accessor :remote + attr_accessor :branch + attr_reader :rtag + + # * +args+ a hash of params from the git block in the routines config + # + def initialize(args={}) + args = { + :remote => :origin, + :branch => :master, + :path => nil + }.merge(args) + @remote, @branch, @path = args[:remote], args[:branch], args[:path] + raise NoRemotePath, :git if @path.nil? + @repo = Repo.new(Dir.pwd) if GIT.working_copy? + end + + def create_release(username=nil, msg=nil) + @rtag = generate_rtag(username) + msg ||= 'Another Release by Rudy!' + msg.tr!("'", "''") + ret = Rye.shell(:git, "tag", @rtag) # Use annotated? -a -m '#{msg}' + raise ret.stderr.join($/) if ret.exit_code > 0 + ret = Rye.shell(:git, "push #{@remote} #{rtag}") if @remote + raise ret.stderr.join($/) if ret.exit_code > 0 + @rtag + end + + # rel-2009-03-05-user-rev + def generate_rtag(username=nil) + now = Time.now + mon = now.mon.to_s.rjust(2, '0') + day = now.day.to_s.rjust(2, '0') + rev = "01" + criteria = ['rel', now.year, mon, day, rev] + criteria.insert(-2, username) if username + rev.succ! while valid_rtag?(criteria.join(Rudy::DELIM)) && rev.to_i < 100 + raise TooManyTags if rev.to_i >= 100 + criteria.join(Rudy::DELIM) + end - def initialize(args={:base => ''}) - @base_uri = args[:base] + def delete_rtag(rtag=nil) + rtag ||= @rtag + ret = Rye.shell(:git, 'tag', :d, rtag) + raise ret.stderr.join($/) if ret.exit_code > 0 # TODO: retest + # "git push origin :tag-name" deletes a remote tag + ret = Rye.shell(:git, "push #{@remote} :#{rtag}") if @remote + raise ret.stderr.join($/) if ret.exit_code > 0 + true end + + def create_remote_checkout(rbox) + raise RemoteError, "#{@path} exists" if rbox.file_exists?(@path) + begin + puts " " + rbox.git('clone', get_remote_uri, @path) + rbox.git('checkout', @rtag) + rescue Rye::CommandError => ex + puts ex.message + end + + end + + + def get_remote_uri + ret = Rye.shell(:git, "config", "remote.#{@remote}.url") + unless ret.exit_code == 0 && !ret.stdout.empty? + raise NoRemoteURI, "remote.#{@remote}.url not set" + end + ret.stdout.first + end + + #def has_remote?(remote) + # success = false + # (@repo.remotes || []).each do |r| + # end + # success + #end + + def valid_rtag?(tag) + # git tag -l tagname returns a 0 exit code and stdout is empty + # when a tag does not exit. When it does exist, the exit code + # is 0 and stdout contains the tagname. + ret = Rye.shell(:git, 'tag', :l, tag) + # change :l to :d for quick deleting above and return true + # OR: just change to :d to always recreate the same tag + (ret.exit_code == 0 && ret.stdout.to_s == tag) + end + + # Are all local changes committed? + def self.clean_working_copy?(path=Dir.pwd) + raise NotAWorkingCopy, path unless working_copy?(path) + Rye.shell(:git, 'diff').stdout == [] + end + + def self.working_copy?(path=Dir.pwd) + (File.exists?(File.join(path, '.git'))) + end + end end end \ No newline at end of file diff --git a/lib/rudy/scm/svn.rb b/lib/rudy/scm/svn.rb index 52ab8bc3..0c8fa521 100644 --- a/lib/rudy/scm/svn.rb +++ b/lib/rudy/scm/svn.rb @@ -12,7 +12,7 @@ def initialize(args={:base => ''}) def create_release(username=nil, msg=nil) local_uri, local_revision = local_info - rtag = generate_release_tag_name(username) + rtag = generate_rtag(username) release_uri = "#{@base_uri}/#{rtag}" msg ||= 'Another Release by Rudy!' msg.tr!("'", "\\'") @@ -24,12 +24,12 @@ def create_release(username=nil, msg=nil) end def switch_working_copy(tag) - raise "Invalid release tag (#{tag})." unless valid_uri?(tag) + raise "Invalid release tag (#{tag})." unless valid_rtag?(tag) `svn switch #{tag}` end # rel-2009-03-05-user-rev - def generate_release_tag_name(username=nil) + def generate_rtag(username=nil) now = Time.now mon = now.mon.to_s.rjust(2, '0') day = now.day.to_s.rjust(2, '0') @@ -38,7 +38,7 @@ def generate_release_tag_name(username=nil) criteria.insert(-2, username) if username tag = criteria.join(Rudy::DELIM) # Keep incrementing the revision number until we find the next one. - tag.succ! while (valid_uri?("#{@base_uri}/#{tag}")) + tag.succ! while (valid_rtag?("#{@base_uri}/#{tag}")) tag end @@ -55,12 +55,13 @@ def working_copy?(path) (File.exists?(File.join(path, '.svn'))) end - def valid_uri?(uri) + def valid_rtag?(uri) ret = `svn info #{uri} 2>&1` || '' # Valid SVN URIs will return some info (ret =~ /Repository UUID/) ? true : false end - def everything_checked_in? + # Are all local changes committed? + def clean_working_copy? `svn diff . 2>&1` == '' # svn diff should return nothing end end