rudy 0.6.8 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.txt +15 -2
- data/README.rdoc +30 -23
- data/Rakefile +5 -5
- data/Rudyfile +87 -66
- data/bin/rudy +120 -167
- data/bin/rudy-ec2 +17 -13
- data/bin/rudy-s3 +6 -4
- data/bin/rudy-sdb +5 -4
- data/lib/annoy.rb +1 -1
- data/lib/console.rb +1 -3
- data/lib/rudy.rb +11 -25
- data/lib/rudy/aws/ec2/instance.rb +1 -1
- data/lib/rudy/aws/ec2/volume.rb +2 -2
- data/lib/rudy/aws/sdb/error.rb +2 -1
- data/lib/rudy/cli.rb +10 -1
- data/lib/rudy/cli/aws/ec2/addresses.rb +1 -1
- data/lib/rudy/cli/aws/ec2/images.rb +3 -1
- data/lib/rudy/cli/aws/ec2/instances.rb +2 -2
- data/lib/rudy/cli/candy.rb +11 -0
- data/lib/rudy/cli/config.rb +25 -44
- data/lib/rudy/cli/machines.rb +30 -10
- data/lib/rudy/cli/routines.rb +67 -19
- data/lib/rudy/config.rb +30 -13
- data/lib/rudy/config/objects.rb +135 -10
- data/lib/rudy/disks.rb +8 -52
- data/lib/rudy/global.rb +9 -5
- data/lib/rudy/guidelines.rb +18 -0
- data/lib/rudy/huxtable.rb +29 -19
- data/lib/rudy/machines.rb +10 -7
- data/lib/rudy/mixins/hash.rb +25 -0
- data/lib/rudy/routines.rb +160 -10
- data/lib/rudy/routines/helper.rb +50 -0
- data/lib/rudy/routines/helpers/diskhelper.rb +44 -18
- data/lib/rudy/routines/helpers/scmhelper.rb +39 -0
- data/lib/rudy/routines/helpers/scripthelper.rb +86 -35
- data/lib/rudy/routines/helpers/userhelper.rb +37 -0
- data/lib/rudy/routines/passthrough.rb +36 -0
- data/lib/rudy/routines/release.rb +38 -22
- data/lib/rudy/routines/shutdown.rb +20 -49
- data/lib/rudy/routines/startup.rb +20 -47
- data/lib/rudy/scm.rb +75 -0
- data/lib/rudy/scm/git.rb +215 -0
- data/lib/rudy/scm/svn.rb +7 -6
- data/lib/rudy/utils.rb +12 -30
- data/lib/storable.rb +4 -1
- data/lib/sysinfo.rb +10 -0
- data/rudy.gemspec +21 -9
- data/test/01_mixins/10_hash_test.rb +25 -0
- data/test/{05_config → 10_config}/00_setup_test.rb +1 -1
- data/test/{05_config → 10_config}/30_machines_test.rb +1 -1
- data/test/15_scm/00_setup_test.rb +20 -0
- data/test/15_scm/20_git_test.rb +61 -0
- data/test/helper.rb +1 -1
- data/vendor/highline-1.5.1/Rakefile +3 -3
- metadata +41 -12
- 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
|
-
|
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
|
-
|
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
|
-
|
55
|
+
msg = "Creating volume... "
|
33
56
|
disk.create
|
34
|
-
Rudy::Utils.waiter(2, 60, STDOUT,
|
57
|
+
Rudy::Utils.waiter(2, 60, STDOUT, msg) {
|
35
58
|
disk.available?
|
36
59
|
}
|
37
60
|
|
38
|
-
|
61
|
+
msg = "Attaching #{disk.awsid} to #{@machine.awsid}... "
|
39
62
|
disk.attach(@machine.awsid)
|
40
|
-
Rudy::Utils.waiter(2,
|
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
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
81
|
+
print "Mounting at #{disk.path}... "
|
56
82
|
|
57
|
-
|
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
|
-
|
122
|
+
msg = "Detaching #{disk.awsid}..."
|
96
123
|
disk.detach
|
97
|
-
Rudy::Utils.waiter(2, 60, STDOUT,
|
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
|
-
|
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
|
-
#
|
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
|
-
|
62
|
-
|
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
|
-
|
68
|
-
|
69
|
-
puts
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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.
|
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
|
-
|
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
|