forked from infochimps-labs/ironfan
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathironfan.rb
210 lines (187 loc) · 6.11 KB
/
ironfan.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
require 'ironfan/requirements'
module Ironfan
@@clusters ||= Hash.new
# path to search for cluster definition files
def self.cluster_path
return Array(Chef::Config[:cluster_path]) if Chef::Config[:cluster_path]
raise "Holy smokes, you have no cookbook_path or cluster_path set up. Follow chef's directions for creating a knife.rb." if Chef::Config[:cookbook_path].blank?
cl_path = Chef::Config[:cookbook_path].map{|dir| File.expand_path('../clusters', dir) }.uniq
ui.warn "No cluster path set. Taking a wild guess that #{cl_path.inspect} is \nreasonable based on your cookbook_path -- but please set cluster_path in your knife.rb"
Chef::Config[:cluster_path] = cl_path
end
#
# Delegates
def self.clusters
@@clusters
end
def self.ui=(ui) @ui = ui ; end
def self.ui() @ui ; end
def self.chef_config=(cc) @chef_config = cc ; end
def self.chef_config() @chef_config ; end
# execute against multiple targets in parallel
def self.parallel(targets)
raise 'missing block' unless block_given?
results = []
[targets].flatten.each_with_index.map do |target, idx|
sleep(0.25) # avoid hammering with simultaneous requests
Thread.new(target) do |target|
results[idx] = safely(target.inspect) do
yield target
end
end
end.each(&:join) # wait for all the blocks to return
results
end
#
# Defines a cluster with the given name.
#
# @example
# Ironfan.cluster 'demosimple' do
# cloud :ec2 do
# availability_zones ['us-east-1d']
# flavor "t1.micro"
# image_name "ubuntu-natty"
# end
# role :base_role
# role :chef_client
#
# facet :sandbox do
# instances 2
# role :nfs_client
# end
# end
#
#
def self.cluster(name, attrs={}, &block)
name = name.to_sym
# If this is being called as Ironfan.cluster('foo') with no additional arguments,
# return the cached cluster object if it exists
if @@clusters[name] and attrs.empty? and not block_given?
return @@clusters[name]
else # Otherwise we're being asked to (re)initialize and cache a cluster definition
cl = Ironfan::Dsl::Cluster.new(:name => name)
cl.receive!(attrs, &block)
@@clusters[name] = cl.resolve
end
end
#
# Return cluster if it's defined. Otherwise, search Ironfan.cluster_path
# for an eponymous file, load it, and return the cluster it defines.
#
# Raises an error if a matching file isn't found, or if loading that file
# doesn't define the requested cluster.
#
# @return [Ironfan::Cluster] the requested cluster
def self.load_cluster(name)
name = name.to_sym
raise ArgumentError, "Please supply a cluster name" if name.to_s.empty?
return @@clusters[name] if @@clusters[name]
cluster_file = cluster_filenames[name] or raise("Couldn't find a definition for #{name} in cluster_path: #{cluster_path.inspect}")
Chef::Log.info("Loading cluster #{cluster_file}")
require cluster_file
unless @@clusters[name] then die("#{cluster_file} was supposed to have the definition for the #{name} cluster, but didn't") end
@@clusters[name]
end
#
# Map from cluster name to file name
#
# @return [Hash] map from cluster name to file name
def self.cluster_filenames
return @cluster_filenames if @cluster_filenames
@cluster_filenames = {}
cluster_path.each do |cp_dir|
Dir[ File.join(cp_dir, '*.rb') ].each do |filename|
cluster_name = File.basename(filename).gsub(/\.rb$/, '')
@cluster_filenames[cluster_name.to_sym] ||= filename
end
end
@cluster_filenames
end
#
# Utility to die with an error message.
# If the last arg is an integer, use it as the exit code.
#
def self.die *strings
exit_code = strings.last.is_a?(Integer) ? strings.pop : -1
strings.each{|str| ui.warn str }
exit exit_code
end
#
# Utility to turn an error into a warning
#
# @example
# Ironfan.safely do
# Ironfan.fog_connection.associate_address(self.fog_server.id, address)
# end
#
def self.safely(info="")
begin
yield
rescue StandardError => err
ui.warn("Error running #{info}:")
ui.warn(err)
Chef::Log.error( err )
Chef::Log.error( err.backtrace.join("\n") )
return err
end
end
#
# Utility to retry a flaky operation three times, with ascending wait times
#
# FIXME: Add specs to test the rescue here. It's a PITA to debug naturally or
#
# Manual test:
# bundle exec ruby -e "require 'chef'; require 'ironfan'; Ironfan.tell_you_thrice { p 'hah'; raise 'hell' }"
def self.tell_you_thrice(options={})
options = { name: "problem",
error_class: StandardError,
retries: 3,
multiplier: 3 }.merge!(options)
try = 0
message = ''
begin
try += 1
yield
rescue options[:error_class] => err
raise unless try < options[:retries]
pause_for = options[:multiplier] * try
Chef::Log.debug "Caught error (was #{err.inspect}). Sleeping #{pause_for} seconds."
sleep pause_for
retry
end
end
#
# Utility to show a step of the overall process
#
def self.step(name, desc, *style)
ui.info(" #{"%-15s" % (name.to_s+":")}\t#{ui.color(desc.to_s, *style)}")
end
def self.substep(name, desc, color = :gray)
step(name, " - #{desc}", color) if (verbosity >= 1 or color != :gray)
end
def self.verbosity
chef_config[:verbosity].to_i
end
# Output a TODO to the logs if you've switched on pestering
def self.todo(*args)
Chef::Log.debug(*args) if Chef::Config[:show_todo]
end
#
# Utility to do mock out a step during a dry-run
#
def self.unless_dry_run
if dry_run?
ui.info(" ... but not really")
return nil
else
yield
end
end
def self.dry_run?
chef_config[:dry_run]
end
# Intentionally skipping an implied step
def self.noop(source,method,*params)
# Chef::Log.debug("#{method} is a no-op for #{source} -- skipping (#{params.join(',')})")
end
end