rudy 0.6.8 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. data/CHANGES.txt +15 -2
  2. data/README.rdoc +30 -23
  3. data/Rakefile +5 -5
  4. data/Rudyfile +87 -66
  5. data/bin/rudy +120 -167
  6. data/bin/rudy-ec2 +17 -13
  7. data/bin/rudy-s3 +6 -4
  8. data/bin/rudy-sdb +5 -4
  9. data/lib/annoy.rb +1 -1
  10. data/lib/console.rb +1 -3
  11. data/lib/rudy.rb +11 -25
  12. data/lib/rudy/aws/ec2/instance.rb +1 -1
  13. data/lib/rudy/aws/ec2/volume.rb +2 -2
  14. data/lib/rudy/aws/sdb/error.rb +2 -1
  15. data/lib/rudy/cli.rb +10 -1
  16. data/lib/rudy/cli/aws/ec2/addresses.rb +1 -1
  17. data/lib/rudy/cli/aws/ec2/images.rb +3 -1
  18. data/lib/rudy/cli/aws/ec2/instances.rb +2 -2
  19. data/lib/rudy/cli/candy.rb +11 -0
  20. data/lib/rudy/cli/config.rb +25 -44
  21. data/lib/rudy/cli/machines.rb +30 -10
  22. data/lib/rudy/cli/routines.rb +67 -19
  23. data/lib/rudy/config.rb +30 -13
  24. data/lib/rudy/config/objects.rb +135 -10
  25. data/lib/rudy/disks.rb +8 -52
  26. data/lib/rudy/global.rb +9 -5
  27. data/lib/rudy/guidelines.rb +18 -0
  28. data/lib/rudy/huxtable.rb +29 -19
  29. data/lib/rudy/machines.rb +10 -7
  30. data/lib/rudy/mixins/hash.rb +25 -0
  31. data/lib/rudy/routines.rb +160 -10
  32. data/lib/rudy/routines/helper.rb +50 -0
  33. data/lib/rudy/routines/helpers/diskhelper.rb +44 -18
  34. data/lib/rudy/routines/helpers/scmhelper.rb +39 -0
  35. data/lib/rudy/routines/helpers/scripthelper.rb +86 -35
  36. data/lib/rudy/routines/helpers/userhelper.rb +37 -0
  37. data/lib/rudy/routines/passthrough.rb +36 -0
  38. data/lib/rudy/routines/release.rb +38 -22
  39. data/lib/rudy/routines/shutdown.rb +20 -49
  40. data/lib/rudy/routines/startup.rb +20 -47
  41. data/lib/rudy/scm.rb +75 -0
  42. data/lib/rudy/scm/git.rb +215 -0
  43. data/lib/rudy/scm/svn.rb +7 -6
  44. data/lib/rudy/utils.rb +12 -30
  45. data/lib/storable.rb +4 -1
  46. data/lib/sysinfo.rb +10 -0
  47. data/rudy.gemspec +21 -9
  48. data/test/01_mixins/10_hash_test.rb +25 -0
  49. data/test/{05_config → 10_config}/00_setup_test.rb +1 -1
  50. data/test/{05_config → 10_config}/30_machines_test.rb +1 -1
  51. data/test/15_scm/00_setup_test.rb +20 -0
  52. data/test/15_scm/20_git_test.rb +61 -0
  53. data/test/helper.rb +1 -1
  54. data/vendor/highline-1.5.1/Rakefile +3 -3
  55. metadata +41 -12
  56. data/bin/ird +0 -175
@@ -0,0 +1,50 @@
1
+
2
+
3
+ module Rudy
4
+ module Routines
5
+ module HelperBase
6
+ include Rudy::Huxtable
7
+
8
+ def execute_rbox_command(ret=nil, &command)
9
+ begin
10
+ ret = command.call
11
+ puts ' ' << ret.stdout.join("#{$/} ") if !ret.stdout.empty?
12
+ print_response(ret)
13
+ rescue Rye::CommandError => ex
14
+ print_response(ex)
15
+ exit 12 unless keep_going?
16
+ rescue Rye::CommandNotFound => ex
17
+ STDERR.puts " CommandNotFound: #{ex.message}".color(:red)
18
+ STDERR.puts ex.backtrace if Rudy.debug?
19
+ exit 12 unless keep_going?
20
+ end
21
+
22
+ ret
23
+ end
24
+
25
+ def keep_going?
26
+ Annoy.pose_question(" Keep going?\a ", /yes|y|ya|sure|you bet!/i, STDERR)
27
+ end
28
+
29
+ # Returns a formatted string for printing command info
30
+ def command_separator(cmd, user)
31
+ cmd ||= ""
32
+ cmd, user = cmd.to_s, user.to_s
33
+ prompt = user == "root" ? "#" : "$"
34
+ ("%s%s%s %s" % [$/, user, prompt, cmd.bright])
35
+ end
36
+
37
+ private
38
+ def print_response(rap)
39
+ colour = rap.exit_code != 0 ? :red : :normal
40
+ [:stderr].each do |sumpin|
41
+ next if rap.send(sumpin).empty?
42
+ STDERR.puts (" #{sumpin.upcase.to_s} " << '-'*38).color(colour).bright
43
+ STDERR.puts " " << rap.send(sumpin).join("#{$/} ").color(colour)
44
+ end
45
+ STDERR.puts " Exit code: #{rap.exit_code}".color(colour) if rap.exit_code != 0
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -2,8 +2,19 @@
2
2
 
3
3
  module Rudy; module Routines;
4
4
  module DiskHelper
5
+ include Rudy::Routines::HelperBase # TODO: use execute_rbox_command
5
6
  extend self
6
7
 
8
+ def disks?(routine)
9
+ (routine.is_a?(Caesars::Hash) && routine.disks &&
10
+ routine.disks.is_a?(Caesars::Hash) && !routine.disks.empty?)
11
+ end
12
+
13
+ def paths(routine)
14
+ return nil unless disks?(routine)
15
+ routine.disks.values.collect { |d| d.keys }.flatten
16
+ end
17
+
7
18
  def execute(routine, machine, rbox)
8
19
  return unless routine
9
20
  raise "Not a Rudy::Machine" unless machine.is_a?(Rudy::Machine)
@@ -12,13 +23,20 @@ module Rudy; module Routines;
12
23
  @machine = machine
13
24
  @rbox = rbox
14
25
 
15
- (routine.disks || {}).each_pair do |action, disks|
26
+ # We need to add mkfs since it's not enabled by default.
27
+ # We add it only to this instance we're using.
28
+ def @rbox.mkfs(*args); cmd('mkfs', args); end
29
+
30
+ return unless disks?(routine)
31
+
32
+ routine.disks.each_pair do |action, disks|
16
33
  unless DiskHelper.respond_to?(action)
17
34
  STDERR.puts %Q(DiskHelper: unknown action "#{action}")
18
35
  next
19
36
  end
20
37
  send(action, disks) # create, copy, destroy, ...
21
38
  end
39
+
22
40
  end
23
41
 
24
42
  def create(disks)
@@ -26,18 +44,23 @@ module Rudy; module Routines;
26
44
 
27
45
  disks.each_pair do |path, props|
28
46
  disk = Rudy::Disk.new(path, props[:size], props[:device], @machine.position)
29
- #disk = rdisk.get(disk.name)
47
+ olddisk = rdisk.get(disk.name)
48
+ if olddisk && olddisk.exists?
49
+ puts "Disk exists: #{olddisk.name}".color(:red)
50
+ return
51
+ end
52
+
30
53
  puts "Creating #{disk.name} "
31
54
 
32
- print "Creating volume... "
55
+ msg = "Creating volume... "
33
56
  disk.create
34
- Rudy::Utils.waiter(2, 60, STDOUT, "done", nil) {
57
+ Rudy::Utils.waiter(2, 60, STDOUT, msg) {
35
58
  disk.available?
36
59
  }
37
60
 
38
- print "Attaching #{disk.awsid} to #{@machine.awsid}... "
61
+ msg = "Attaching #{disk.awsid} to #{@machine.awsid}... "
39
62
  disk.attach(@machine.awsid)
40
- Rudy::Utils.waiter(2, 60, STDOUT, "done", nil) {
63
+ Rudy::Utils.waiter(2, 10, STDOUT, msg) {
41
64
  disk.attached?
42
65
  }
43
66
 
@@ -46,15 +69,19 @@ module Rudy; module Routines;
46
69
  # "No such file or directory while trying to determine filesystem size"
47
70
  sleep 2
48
71
 
72
+ # TODO: Cleanup. See ScriptHelper
49
73
  begin
50
74
  print "Creating ext3 filesystem for #{disk.device}... "
51
- @rbox.mkfs(:t, "ext3", :F, disk.device)
52
- @rbox.mkdir(:p, disk.path)
53
- puts "done"
75
+ execute_rbox_command {
76
+
77
+ @rbox.mkfs(:t, "ext3", :F, disk.device)
78
+ @rbox.mkdir(:p, disk.path)
79
+ puts "done"
54
80
 
55
- print "Mounting at #{disk.path}... "
81
+ print "Mounting at #{disk.path}... "
56
82
 
57
- @rbox.mount(:t, 'ext3', disk.device, disk.path)
83
+ @rbox.mount(:t, 'ext3', disk.device, disk.path)
84
+ }
58
85
  disk.mounted = true
59
86
  disk.save
60
87
  rescue Net::SSH::AuthenticationFailed, Net::SSH::HostKeyMismatch => ex
@@ -85,24 +112,23 @@ module Rudy; module Routines;
85
112
  puts "Destroying #{disk.name}"
86
113
 
87
114
  if disk.mounted?
88
- print "Unmounting #{disk.path}... "
89
- @rbox.umount(disk.path)
115
+ print "Unmounting #{disk.path}..."
116
+ execute_rbox_command { @rbox.umount(disk.path) }
117
+ puts " done"
90
118
  sleep 0.5
91
- puts "done"
92
119
  end
93
120
 
94
121
  if disk.attached?
95
- print "Detaching #{disk.awsid}... "
122
+ msg = "Detaching #{disk.awsid}..."
96
123
  disk.detach
97
- Rudy::Utils.waiter(2, 60, STDOUT, 'done', nil) {
124
+ Rudy::Utils.waiter(2, 60, STDOUT, msg) {
98
125
  disk.available?
99
126
  }
100
127
  sleep 0.5
101
128
  end
102
129
 
103
- print "Destroying metadata... "
130
+ puts "Destroying metadata... "
104
131
  disk.destroy
105
- puts "done"
106
132
 
107
133
  end
108
134
  end
@@ -0,0 +1,39 @@
1
+
2
+ module Rudy; module Routines;
3
+ module SCMHelper
4
+ include Rudy::Routines::HelperBase
5
+ extend self
6
+
7
+ # Does the routine config contain SCM routines?
8
+ # Raises Rudy::Error if there is malformed configuration.
9
+ def scm?(routine)
10
+ scmnames = SUPPORTED_SCM_NAMES & routine.keys # Find intersections.
11
+ return false if scmnames.empty? # Nothing to do.
12
+ scmnames.each do |scm|
13
+ routine[scm].values.each do |p| # Each SCM should have a
14
+ raise "Bad #{scm} config" if !p.kind_of?(Hash) # Hash config. Otherwise
15
+ end # it's misconfigured.
16
+ end
17
+ true
18
+ end
19
+
20
+ def create_scm_objects(routine)
21
+ return nil unless routine
22
+ scmnames = SUPPORTED_SCM_NAMES & routine.keys
23
+ vlist = []
24
+ # Look for scm config in the routine by checking all known scm types.
25
+ # For each one we'll create an instance of the appropriate SCM class.
26
+ scmnames.each do |scm|
27
+ routine[scm].each_pair do |user,params|
28
+ klass = eval "Rudy::SCM::#{scm.to_s.upcase}"
29
+ params[:user] = user
30
+ scm = klass.new(params)
31
+ scm.raise_early_exceptions # Raises exceptions for obvious problems.
32
+ vlist << scm
33
+ end
34
+ end
35
+ vlist
36
+ end
37
+
38
+ end
39
+ end; end
@@ -1,34 +1,69 @@
1
1
  require 'tempfile'
2
2
 
3
-
4
3
  module Rudy; module Routines;
5
-
4
+ #--
5
+ # TODO: Rename to ShellHelper
6
+ #++
6
7
  module ScriptHelper
8
+ include Rudy::Routines::HelperBase # TODO: use execute_rbox_command
7
9
  extend self
8
-
10
+
9
11
  @@script_types = [:after, :before, :after_local, :before_local]
10
12
  @@script_config_file = "rudy-config.yml"
11
13
 
12
14
  def before_local(routine, sconf, rbox)
15
+
16
+ # before_local generally doesn't take a user name like the remote
17
+ # before block so we add it here (unless the user did specify it)
18
+ routine[:before_local] = {
19
+ rbox.user.to_sym => routine.delete(:before_local)
20
+ } unless routine.has_key?(rbox.user.to_sym) # use routine[timing].deepest_point ?
13
21
  execute_command(:before_local, routine, sconf, 'localhost', rbox)
14
- end
22
+ end
23
+ def before_local?(routine); execute_command?(:before_local, routine); end
24
+
15
25
  def after_local(routine, sconf, rbox)
26
+ routine[:after_local] = { # See before_local note
27
+ rbox.user.to_sym => routine.delete(:after_local)
28
+ } unless routine.has_key?(rbox.user.to_sym)
16
29
  execute_command(:after_local, routine, sconf, 'localhost', rbox)
17
30
  end
18
-
31
+ def after_local?(routine); execute_command?(:after_local, routine); end
32
+
33
+
19
34
  def before(routine, sconf, machine, rbox)
20
35
  raise "ScriptHelper: Not a Rudy::Machine" unless machine.is_a?(Rudy::Machine)
21
36
  execute_command(:before, routine, sconf, machine.name, rbox)
22
37
  end
38
+ def before?(routine); execute_command?(:before, routine); end
39
+
23
40
  def after(routine, sconf, machine, rbox)
24
41
  raise "ScriptHelper: Not a Rudy::Machine" unless machine.is_a?(Rudy::Machine)
25
42
  execute_command(:after, routine, sconf, machine.name, rbox)
26
43
  end
44
+ def after?(routine); execute_command?(:after, routine); end
27
45
 
28
46
 
29
47
  private
30
48
 
31
- # * +timing+ is one of: after, before
49
+ # Does the routine have the requested script type?
50
+ # * +timing+ is one of: after, before, after_local, before_local
51
+ # * +routine+ a single routine hash (startup, shutdown, etc...)
52
+ # Prints notice to STDERR if there's an empty conf hash
53
+ def execute_command?(timing, routine)
54
+ hasconf = (routine.is_a?(Caesars::Hash) && routine.has_key?(timing))
55
+ return false unless hasconf
56
+ routine[timing].each_pair do |user,conf|
57
+ if conf.empty?
58
+ STDERR.puts "Empty #{timing} config for #{user}"
59
+ else
60
+ return true
61
+ end
62
+ end
63
+ false
64
+ end
65
+
66
+ # * +timing+ is one of: after, before, after_local, before_local
32
67
  # * +routine+ a single routine hash (startup, shutdown, etc...)
33
68
  # * +sconf+ is a config hash from machines config (ignored if nil)
34
69
  # * +hostname+ machine hostname that we're working on
@@ -47,48 +82,64 @@ module Rudy; module Routines;
47
82
  # add the method on for the instance of rbox we are using.
48
83
  def rbox.rm(*args); cmd('rm', args); end
49
84
 
50
-
51
- if routine.is_a?(Caesars::Hash) && routine.has_key?(timing)
52
- puts "Connecting to #{hostname}"
53
- begin
54
- rbox.connect
55
- rescue Net::SSH::AuthenticationFailed, Net::SSH::HostKeyMismatch => ex
56
- STDERR.puts "Error connecting: #{ex.message}".color(:red)
57
- STDERR.puts "Skipping scripts".color(:red)
58
- end
85
+ if execute_command?(timing, routine) # i.e. before_local?
59
86
 
60
87
  original_user = rbox.user
61
- scripts = [routine[timing]].flatten
62
- scripts.each do |script|
88
+ users = routine[timing] || {}
89
+ users.each_pair do |user, commands|
63
90
 
64
91
  begin
65
- user, command, *args = script.to_a.flatten.compact
66
92
  rbox.switch_user user # does nothing if it's the same user
67
- puts "Creating #{@@script_config_file}"
68
- rbox.safe = false
69
- puts rbox.echo("'#{sconf.to_hash.to_yaml}' > #{@@script_config_file}")
70
- rbox.safe = true
71
- rbox.chmod(600, @@script_config_file)
72
- puts %Q{Running (as #{user}): #{rbox.preview_command(command, args)}}
93
+ rbox.connect(false) # does nothing if already connected
94
+ rescue Net::SSH::AuthenticationFailed, Net::SSH::HostKeyMismatch => ex
95
+ STDERR.puts "Error connecting: #{ex.message}".color(:red)
96
+ STDERR.puts "Skipping user #{user}".color(:red)
97
+ next
98
+ end
73
99
 
74
- ret = rbox.send(command, args)
75
- if ret.exit_code > 0
76
- puts " Exit code: #{ret.exit_code}".color(:red)
77
- puts " STDERR: #{ret.stderr.join("#{$/} ")}".color(:red)
78
- puts " STDOUT: #{ret.stdout.join("#{$/} ")}".color(:red)
79
- else
80
- puts ' ' << ret.stdout.join("#{$/} ")
100
+ execute_rbox_command {
101
+ # We need to create the config file for every script,
102
+ # b/c the user may change and it would not be accessible.
103
+ # We turn off safe mode so we can write the config file via SSH.
104
+ # This will need to use SCP eventually; it is unsafe and error prone.
105
+ # TODO: Replace with rbox.upload. Make it safe again!
106
+ conf_str = StringIO.new
107
+ conf_str.puts sconf.to_hash.to_yaml
108
+ rbox.upload(conf_str, @@script_config_file)
109
+ rbox.chmod(600, @@script_config_file)
110
+ }
111
+
112
+ commands.each_pair do |command, calls|
113
+ # If a command is only referred to once and it has no arguments
114
+ # defined, we force it through by making an array with one element.
115
+ calls = [[]] if calls.empty?
116
+
117
+ # Force a CommandNotFound exception early
118
+ unless rbox.can?(command)
119
+ execute_rbox_command { rbox.send(command) }
120
+ next
121
+ end
122
+
123
+ # Execute the command for every set of arguments
124
+ calls.each do |args|
125
+ puts command_separator(rbox.preview_command(command, args), user)
126
+ execute_rbox_command { ret = rbox.send(command, args) }
81
127
  end
82
- rescue Rye::CommandNotFound => ex
83
- puts " CommandNotFound: #{ex.message}".color(:red)
84
128
  end
85
129
 
130
+ # I was gettings errors about script_config_file not existing. There
131
+ # might be a race condition when the rm command is called too quickly.
132
+ # It's also quite possible I'm off my rocker!
133
+ sleep 0.1
86
134
 
87
- rbox.rm(@@script_config_file)
135
+ rbox.cd # reset to home dir
136
+ #rbox.rm(@@script_config_file)
88
137
  end
138
+
139
+ # Return the borrowed rbox instance to the user it was provided with
89
140
  rbox.switch_user original_user
90
141
  else
91
- #puts "Nothing to do"
142
+ puts "Nothing to do"
92
143
  end
93
144
 
94
145
  tf.delete # delete local copy of script config
@@ -0,0 +1,37 @@
1
+
2
+ module Rudy; module Routines;
3
+ module UserHelper
4
+ include Rudy::Routines::HelperBase # TODO: use execute_rbox_command
5
+ extend self
6
+
7
+ def adduser?(routine)
8
+ (!routine.adduser.nil? && !routine.adduser.empty?)
9
+ end
10
+ def adduser(routine, machine, rbox)
11
+
12
+ # On Solaris, the user's home directory needs to be specified
13
+ # explicitly so we do it for linux too for fun.
14
+ homedir = rbox.guess_user_home(routine.adduser.to_s)
15
+ args = [:m, :d, homedir, :s, '/bin/bash', routine.adduser.to_s]
16
+ puts command_separator(rbox.preview_command(:useradd, args), routine.adduser.to_s)
17
+
18
+ # NOTE: We'll may to use platform specific code here.
19
+ # Linux has adduser and useradd commands:
20
+ # adduser can prompt for info which we don't want.
21
+ # useradd does not prompt (on Debian/Ubuntu at least).
22
+ # We need to specify bash b/c the default is /bin/sh
23
+ execute_rbox_command { rbox.useradd(args) }
24
+ end
25
+
26
+ def authorize?(routine)
27
+ (!routine.authorize.nil? && !routine.authorize.empty?)
28
+ end
29
+ def authorize(routine, machine, rbox)
30
+ puts command_separator(:authorize_keys_remote, routine.authorize)
31
+ execute_rbox_command { rbox.authorize_keys_remote(routine.authorize) }
32
+ end
33
+
34
+
35
+ end
36
+
37
+ end; end
@@ -0,0 +1,36 @@
1
+
2
+
3
+ module Rudy; module Routines;
4
+ class Passthrough < Rudy::Routines::Base
5
+
6
+ def init(*args)
7
+ @routine_name = args.first
8
+ @routine = fetch_routine_config(@routine_name)
9
+ end
10
+
11
+ # * +each_mach+ is an optional block which is executed between
12
+ # disk creation and the after scripts. The will receives two
13
+ # arguments: instances of Rudy::Machine and Rye::Box.
14
+ def execute(&each_mach)
15
+ routine_separator(@routine_name)
16
+ machines = []
17
+ generic_machine_runner(:list) do |machine|
18
+ puts $/ #, "[routine: #{@routine_name}]"
19
+ machines << machine
20
+ end
21
+ machines
22
+ end
23
+
24
+ # Called by generic_machine_runner
25
+ def raise_early_exceptions
26
+ raise Rudy::Error, "No routine name" unless @routine_name
27
+ raise NoRoutine, @routine_name unless @routine
28
+ rmach = Rudy::Machines.new
29
+ raise Rudy::PrivateKeyNotFound, root_keypairpath unless has_keypair?(:root)
30
+ raise MachineGroupNotDefined, current_machine_group unless known_machine_group?
31
+ raise MachineGroupNotRunning, current_machine_group unless rmach.running?
32
+ end
33
+
34
+ end
35
+
36
+ end; end