ironfan 3.1.0.rc1
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.
- data/.gitignore +51 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +130 -0
- data/Gemfile +26 -0
- data/LICENSE.md +201 -0
- data/README.md +328 -0
- data/Rakefile +104 -0
- data/TODO.md +16 -0
- data/VERSION +1 -0
- data/chefignore +41 -0
- data/cluster_chef-knife.gemspec +123 -0
- data/cluster_chef.gemspec +111 -0
- data/config/client.rb +59 -0
- data/config/proxy.pac +12 -0
- data/config/ubuntu10.04-ironfan.erb +157 -0
- data/config/ubuntu11.10-ironfan.erb +145 -0
- data/ironfan.gemspec +121 -0
- data/lib/chef/knife/bootstrap/ubuntu10.04-ironfan.erb +157 -0
- data/lib/chef/knife/bootstrap/ubuntu11.10-ironfan.erb +145 -0
- data/lib/chef/knife/cluster_bootstrap.rb +74 -0
- data/lib/chef/knife/cluster_kick.rb +94 -0
- data/lib/chef/knife/cluster_kill.rb +73 -0
- data/lib/chef/knife/cluster_launch.rb +164 -0
- data/lib/chef/knife/cluster_list.rb +50 -0
- data/lib/chef/knife/cluster_proxy.rb +126 -0
- data/lib/chef/knife/cluster_show.rb +61 -0
- data/lib/chef/knife/cluster_ssh.rb +141 -0
- data/lib/chef/knife/cluster_start.rb +40 -0
- data/lib/chef/knife/cluster_stop.rb +43 -0
- data/lib/chef/knife/cluster_sync.rb +77 -0
- data/lib/chef/knife/generic_command.rb +66 -0
- data/lib/chef/knife/knife_common.rb +195 -0
- data/lib/ironfan.rb +143 -0
- data/lib/ironfan/chef_layer.rb +299 -0
- data/lib/ironfan/cloud.rb +412 -0
- data/lib/ironfan/cluster.rb +118 -0
- data/lib/ironfan/compute.rb +153 -0
- data/lib/ironfan/deprecated.rb +33 -0
- data/lib/ironfan/discovery.rb +177 -0
- data/lib/ironfan/dsl_object.rb +124 -0
- data/lib/ironfan/facet.rb +144 -0
- data/lib/ironfan/fog_layer.rb +150 -0
- data/lib/ironfan/private_key.rb +130 -0
- data/lib/ironfan/role_implications.rb +58 -0
- data/lib/ironfan/security_group.rb +119 -0
- data/lib/ironfan/server.rb +281 -0
- data/lib/ironfan/server_slice.rb +260 -0
- data/lib/ironfan/volume.rb +157 -0
- data/spec/ironfan/cluster_spec.rb +13 -0
- data/spec/ironfan/facet_spec.rb +69 -0
- data/spec/ironfan/server_slice_spec.rb +19 -0
- data/spec/ironfan/server_spec.rb +112 -0
- data/spec/ironfan_spec.rb +193 -0
- data/spec/spec_helper.rb +50 -0
- data/spec/spec_helper/dummy_chef.rb +25 -0
- data/spec/test_config.rb +20 -0
- data/tasks/chef_config.rake +38 -0
- data/tasks/jeweler_use_alt_branch.rake +53 -0
- metadata +217 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Philip (flip) Kromer (<flip@infochimps.com>)
|
3
|
+
# Copyright:: Copyright (c) 2011 Infochimps, Inc
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require File.expand_path(File.dirname(__FILE__)+"/generic_command.rb")
|
20
|
+
|
21
|
+
class Chef
|
22
|
+
class Knife
|
23
|
+
class ClusterSync < Ironfan::Script
|
24
|
+
import_banner_and_options(Ironfan::Script)
|
25
|
+
|
26
|
+
option :cloud,
|
27
|
+
:long => "--[no-]cloud",
|
28
|
+
:description => "Sync to the cloud (default is yes, sync cloud; use --no-cloud to skip)",
|
29
|
+
:default => true,
|
30
|
+
:boolean => true
|
31
|
+
option :chef,
|
32
|
+
:long => "--[no-]chef",
|
33
|
+
:description => "Sync to the chef server (default is yes, sync chef; use --no-chef to skip)",
|
34
|
+
:default => true,
|
35
|
+
:boolean => true
|
36
|
+
option :sync_all,
|
37
|
+
:long => "--[no-]sync-all",
|
38
|
+
:description => "Sync, as best as possible, any defined node (even if it is missing from cloud or chef)",
|
39
|
+
:default => false,
|
40
|
+
:boolean => true
|
41
|
+
|
42
|
+
|
43
|
+
def relevant?(server)
|
44
|
+
if config[:sync_all]
|
45
|
+
not server.bogus?
|
46
|
+
else
|
47
|
+
server.created? && server.in_chef?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def perform_execution(target)
|
52
|
+
if config[:chef]
|
53
|
+
sync_to_chef target
|
54
|
+
else Chef::Log.debug("Skipping sync to chef") ; end
|
55
|
+
|
56
|
+
if config[:cloud] && target.any?(&:in_cloud?)
|
57
|
+
sync_to_cloud target
|
58
|
+
else Chef::Log.debug("Skipping sync to cloud") ; end
|
59
|
+
end
|
60
|
+
|
61
|
+
def sync_to_chef(target)
|
62
|
+
if config[:dry_run]
|
63
|
+
ui.info "(can't do a dry-run when syncing to chef -- skipping)"
|
64
|
+
return
|
65
|
+
end
|
66
|
+
ui.info "Syncing to Chef:"
|
67
|
+
target.sync_to_chef
|
68
|
+
end
|
69
|
+
|
70
|
+
def sync_to_cloud(target)
|
71
|
+
ui.info "Syncing to cloud:"
|
72
|
+
target.sync_to_cloud
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Philip (flip) Kromer (<flip@infochimps.com>)
|
3
|
+
# Copyright:: Copyright (c) 2011 Infochimps, Inc
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require File.expand_path(File.dirname(__FILE__)+"/knife_common.rb")
|
20
|
+
|
21
|
+
module Ironfan
|
22
|
+
class Script < Chef::Knife
|
23
|
+
include Ironfan::KnifeCommon
|
24
|
+
|
25
|
+
deps do
|
26
|
+
Ironfan::KnifeCommon.load_deps
|
27
|
+
end
|
28
|
+
|
29
|
+
option :dry_run,
|
30
|
+
:long => "--dry-run",
|
31
|
+
:description => "Don't really run, just use mock calls",
|
32
|
+
:boolean => true,
|
33
|
+
:default => false
|
34
|
+
option :yes,
|
35
|
+
:long => "--yes",
|
36
|
+
:description => "Skip confirmation prompts on risky actions.",
|
37
|
+
:boolean => true
|
38
|
+
|
39
|
+
def run
|
40
|
+
load_ironfan
|
41
|
+
die(banner) if @name_args.empty?
|
42
|
+
configure_dry_run
|
43
|
+
|
44
|
+
target = get_relevant_slice(* @name_args)
|
45
|
+
|
46
|
+
die("No nodes to #{sub_command}, exiting", 1) if target.empty?
|
47
|
+
|
48
|
+
ui.info(["\n",
|
49
|
+
ui.color("Running #{sub_command}", :cyan),
|
50
|
+
" on #{target.joined_names}..."].join())
|
51
|
+
unless config[:yes]
|
52
|
+
ui.info("")
|
53
|
+
confirm_execution(target)
|
54
|
+
end
|
55
|
+
#
|
56
|
+
perform_execution(target)
|
57
|
+
ui.info("")
|
58
|
+
ui.info "Finished! Current state:"
|
59
|
+
display(target)
|
60
|
+
end
|
61
|
+
|
62
|
+
def perform_execution(target)
|
63
|
+
target.send(sub_command)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'chef/knife'
|
2
|
+
|
3
|
+
module Ironfan
|
4
|
+
module KnifeCommon
|
5
|
+
|
6
|
+
def self.load_deps
|
7
|
+
require 'formatador'
|
8
|
+
require 'chef/node'
|
9
|
+
require 'chef/api_client'
|
10
|
+
require 'fog'
|
11
|
+
end
|
12
|
+
|
13
|
+
def load_ironfan
|
14
|
+
$LOAD_PATH << File.join(Chef::Config[:ironfan_path], '/lib') if Chef::Config[:ironfan_path]
|
15
|
+
require 'ironfan'
|
16
|
+
$stdout.sync = true
|
17
|
+
Ironfan.ui = self.ui
|
18
|
+
Ironfan.chef_config = self.config
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# A slice of a cluster:
|
23
|
+
#
|
24
|
+
# @param [String] cluster_name -- cluster to slice
|
25
|
+
# @param [String] facet_name -- facet to slice (or nil for all in cluster)
|
26
|
+
# @param [Array, String] slice_indexes -- servers in that facet (or nil for all in facet).
|
27
|
+
# You must specify a facet if you use slice_indexes.
|
28
|
+
#
|
29
|
+
# @return [Ironfan::ServerSlice] the requested slice
|
30
|
+
def get_slice(cluster_name, facet_name=nil, slice_indexes=nil)
|
31
|
+
if facet_name.nil? && slice_indexes.nil?
|
32
|
+
cluster_name, facet_name, slice_indexes = cluster_name.split(/[\s\-]/, 3)
|
33
|
+
end
|
34
|
+
ui.info("Inventorying servers in #{predicate_str(cluster_name, facet_name, slice_indexes)}")
|
35
|
+
cluster = Ironfan.load_cluster(cluster_name)
|
36
|
+
cluster.resolve!
|
37
|
+
cluster.discover!
|
38
|
+
cluster.slice(facet_name, slice_indexes)
|
39
|
+
end
|
40
|
+
|
41
|
+
def predicate_str(cluster_name, facet_name, slice_indexes)
|
42
|
+
[ "#{ui.color(cluster_name, :bold)} cluster",
|
43
|
+
(facet_name ? "#{ui.color(facet_name, :bold)} facet" : "#{ui.color("all", :bold)} facets"),
|
44
|
+
(slice_indexes ? "servers #{ui.color(slice_indexes, :bold)}" : "#{ui.color("all", :bold)} servers")
|
45
|
+
].join(', ')
|
46
|
+
end
|
47
|
+
|
48
|
+
# method to nodes should be filtered on
|
49
|
+
def relevant?(server)
|
50
|
+
server.exists?
|
51
|
+
end
|
52
|
+
|
53
|
+
# override in subclass to confirm risky actions
|
54
|
+
def confirm_execution(*args)
|
55
|
+
# pass
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Get a slice of nodes matching the given filter
|
60
|
+
#
|
61
|
+
# @example
|
62
|
+
# target = get_relevant_slice(* @name_args)
|
63
|
+
#
|
64
|
+
def get_relevant_slice( *predicate )
|
65
|
+
full_target = get_slice( *predicate )
|
66
|
+
display(full_target) do |svr|
|
67
|
+
rel = relevant?(svr)
|
68
|
+
{ :relevant? => (rel ? "[blue]#{rel}[reset]" : '-' ) }
|
69
|
+
end
|
70
|
+
full_target.select{|svr| relevant?(svr) }
|
71
|
+
end
|
72
|
+
|
73
|
+
# passes target to ClusterSlice#display, will show headings in server slice
|
74
|
+
# tables based on the --verbose flag
|
75
|
+
def display(target, display_style=nil, &block)
|
76
|
+
display_style ||= (config[:verbosity] == 0 ? :default : :expanded)
|
77
|
+
target.display(display_style, &block)
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Put Fog into mock mode if --dry_run
|
82
|
+
#
|
83
|
+
def configure_dry_run
|
84
|
+
if config[:dry_run]
|
85
|
+
Fog.mock!
|
86
|
+
Fog::Mock.delay = 0
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Show a pretty progress bar while we wait for a set of threads to finish.
|
91
|
+
def progressbar_for_threads(threads)
|
92
|
+
section "Waiting for servers:"
|
93
|
+
total = threads.length
|
94
|
+
remaining = threads.select(&:alive?)
|
95
|
+
start_time = Time.now
|
96
|
+
until remaining.empty?
|
97
|
+
remaining = remaining.select(&:alive?)
|
98
|
+
if config[:verbose]
|
99
|
+
ui.info "waiting: #{total - remaining.length} / #{total}, #{(Time.now - start_time).to_i}s"
|
100
|
+
sleep 5
|
101
|
+
else
|
102
|
+
Formatador.redisplay_progressbar(total - remaining.length, total, {:started_at => start_time })
|
103
|
+
sleep 1
|
104
|
+
end
|
105
|
+
end
|
106
|
+
# Collapse the threads
|
107
|
+
threads.each(&:join)
|
108
|
+
ui.info ''
|
109
|
+
end
|
110
|
+
|
111
|
+
def bootstrapper(server, hostname)
|
112
|
+
bootstrap = Chef::Knife::Bootstrap.new
|
113
|
+
bootstrap.config.merge!(config)
|
114
|
+
|
115
|
+
bootstrap.name_args = [ hostname ]
|
116
|
+
bootstrap.config[:node] = server
|
117
|
+
bootstrap.config[:run_list] = server.combined_run_list
|
118
|
+
bootstrap.config[:ssh_user] = config[:ssh_user] || server.cloud.ssh_user
|
119
|
+
bootstrap.config[:attribute] = config[:attribute]
|
120
|
+
bootstrap.config[:identity_file] = config[:identity_file] || server.cloud.ssh_identity_file
|
121
|
+
bootstrap.config[:distro] = config[:distro] || server.cloud.bootstrap_distro
|
122
|
+
bootstrap.config[:use_sudo] = true unless config[:use_sudo] == false
|
123
|
+
bootstrap.config[:chef_node_name] = server.fullname
|
124
|
+
bootstrap.config[:client_key] = server.client_key.body if server.client_key.body
|
125
|
+
|
126
|
+
bootstrap
|
127
|
+
end
|
128
|
+
|
129
|
+
def run_bootstrap(node, hostname)
|
130
|
+
bs = bootstrapper(node, hostname)
|
131
|
+
if config[:skip].to_s == 'true'
|
132
|
+
ui.info "Skipping: bootstrapp #{hostname} with #{JSON.pretty_generate(bs.config)}"
|
133
|
+
return
|
134
|
+
end
|
135
|
+
begin
|
136
|
+
bs.run
|
137
|
+
rescue StandardError => e
|
138
|
+
ui.warn e
|
139
|
+
ui.warn e.backtrace
|
140
|
+
ui.warn ""
|
141
|
+
ui.warn node.inspect
|
142
|
+
ui.warn ""
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
#
|
147
|
+
# Utilities
|
148
|
+
#
|
149
|
+
|
150
|
+
def sub_command
|
151
|
+
self.class.sub_command
|
152
|
+
end
|
153
|
+
|
154
|
+
def confirm_or_exit question, correct_answer
|
155
|
+
response = ui.ask_question(question)
|
156
|
+
unless response.chomp == correct_answer
|
157
|
+
die "I didn't think so.", "Aborting!", 1
|
158
|
+
end
|
159
|
+
ui.info("")
|
160
|
+
end
|
161
|
+
|
162
|
+
#
|
163
|
+
# Announce a new section of tasks
|
164
|
+
#
|
165
|
+
def section(desc, *style)
|
166
|
+
style = [:blue] if style.empty?
|
167
|
+
ui.info(ui.color(desc, *style))
|
168
|
+
end
|
169
|
+
|
170
|
+
def die *args
|
171
|
+
Ironfan.die(*args)
|
172
|
+
end
|
173
|
+
|
174
|
+
module ClassMethods
|
175
|
+
def sub_command
|
176
|
+
self.to_s.gsub(/^.*::/, '').gsub(/^Cluster/, '').downcase
|
177
|
+
end
|
178
|
+
|
179
|
+
def import_banner_and_options(klass, options={})
|
180
|
+
options[:except] ||= []
|
181
|
+
deps{ klass.load_deps }
|
182
|
+
klass.options.sort.each do |name, info|
|
183
|
+
next if options.include?(name) || options[:except].include?(name)
|
184
|
+
option name, info
|
185
|
+
end
|
186
|
+
banner "knife cluster #{sub_command} CLUSTER_NAME [FACET_NAME [INDEXES]] (options)"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
def self.included(base)
|
190
|
+
base.class_eval do
|
191
|
+
extend ClassMethods
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
data/lib/ironfan.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'chef/mash'
|
2
|
+
require 'chef/config'
|
3
|
+
#
|
4
|
+
require 'gorillib/metaprogramming/class_attribute'
|
5
|
+
require 'gorillib/hash/reverse_merge'
|
6
|
+
require 'gorillib/object/blank'
|
7
|
+
require 'gorillib/hash/compact'
|
8
|
+
require 'set'
|
9
|
+
|
10
|
+
require 'ironfan/dsl_object'
|
11
|
+
require 'ironfan/cloud'
|
12
|
+
require 'ironfan/security_group'
|
13
|
+
require 'ironfan/compute' # base class for machine attributes
|
14
|
+
require 'ironfan/facet' # similar machines within a cluster
|
15
|
+
require 'ironfan/cluster' # group of machines with a common mission
|
16
|
+
require 'ironfan/server' # realization of a specific facet
|
17
|
+
require 'ironfan/discovery' # pair servers with Fog and Chef objects
|
18
|
+
require 'ironfan/server_slice' # collection of server objects
|
19
|
+
require 'ironfan/volume' # configure external and internal volumes
|
20
|
+
require 'ironfan/private_key' # coordinate chef keys, cloud keypairs, etc
|
21
|
+
require 'ironfan/role_implications' # make roles trigger other actions (security groups, etc)
|
22
|
+
#
|
23
|
+
require 'ironfan/chef_layer' # interface to chef for server actions
|
24
|
+
require 'ironfan/fog_layer' # interface to fog for server actions
|
25
|
+
#
|
26
|
+
require 'ironfan/deprecated' # stuff slated to go away
|
27
|
+
|
28
|
+
module Ironfan
|
29
|
+
|
30
|
+
# path to search for cluster definition files
|
31
|
+
def self.cluster_path
|
32
|
+
return Chef::Config[:cluster_path] if Chef::Config[:cluster_path]
|
33
|
+
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?
|
34
|
+
cl_path = Chef::Config[:cookbook_path].map{|dir| File.expand_path('../clusters', dir) }.uniq
|
35
|
+
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"
|
36
|
+
Chef::Config[:cluster_path] = cl_path
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Delegates
|
41
|
+
def self.clusters
|
42
|
+
Chef::Config[:clusters] ||= Mash.new
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.ui=(ui) @ui = ui ; end
|
46
|
+
def self.ui() @ui ; end
|
47
|
+
|
48
|
+
def self.chef_config=(cc) @chef_config = cc ; end
|
49
|
+
def self.chef_config() @chef_config ; end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Defines a cluster with the given name.
|
53
|
+
#
|
54
|
+
# @example
|
55
|
+
# Ironfan.cluster 'demosimple' do
|
56
|
+
# cloud :ec2 do
|
57
|
+
# availability_zones ['us-east-1d']
|
58
|
+
# flavor "t1.micro"
|
59
|
+
# image_name "ubuntu-natty"
|
60
|
+
# end
|
61
|
+
# role :base_role
|
62
|
+
# role :chef_client
|
63
|
+
#
|
64
|
+
# facet :sandbox do
|
65
|
+
# instances 2
|
66
|
+
# role :nfs_client
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
#
|
71
|
+
def self.cluster(name, attrs={}, &block)
|
72
|
+
name = name.to_sym
|
73
|
+
cl = ( self.clusters[name] ||= Ironfan::Cluster.new(name, attrs) )
|
74
|
+
cl.configure(&block)
|
75
|
+
cl
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Return cluster if it's defined. Otherwise, search Ironfan.cluster_path
|
80
|
+
# for an eponymous file, load it, and return the cluster it defines.
|
81
|
+
#
|
82
|
+
# Raises an error if a matching file isn't found, or if loading that file
|
83
|
+
# doesn't define the requested cluster.
|
84
|
+
#
|
85
|
+
# @return [Ironfan::Cluster] the requested cluster
|
86
|
+
def self.load_cluster(cluster_name)
|
87
|
+
raise ArgumentError, "Please supply a cluster name" if cluster_name.to_s.empty?
|
88
|
+
return clusters[cluster_name] if clusters[cluster_name]
|
89
|
+
|
90
|
+
cluster_file = cluster_filenames[cluster_name] or die("Couldn't find a definition for #{cluster_name} in cluster_path: #{cluster_path.inspect}")
|
91
|
+
|
92
|
+
Chef::Log.info("Loading cluster #{cluster_file}")
|
93
|
+
|
94
|
+
require cluster_file
|
95
|
+
unless clusters[cluster_name] then die("#{cluster_file} was supposed to have the definition for the #{cluster_name} cluster, but didn't") end
|
96
|
+
|
97
|
+
clusters[cluster_name]
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# Map from cluster name to file name
|
102
|
+
#
|
103
|
+
# @return [Hash] map from cluster name to file name
|
104
|
+
def self.cluster_filenames
|
105
|
+
return @cluster_filenames if @cluster_filenames
|
106
|
+
@cluster_filenames = {}
|
107
|
+
cluster_path.each do |cp_dir|
|
108
|
+
Dir[ File.join(cp_dir, '*.rb') ].each do |filename|
|
109
|
+
cluster_name = File.basename(filename).gsub(/\.rb$/, '')
|
110
|
+
@cluster_filenames[cluster_name] ||= filename
|
111
|
+
end
|
112
|
+
end
|
113
|
+
@cluster_filenames
|
114
|
+
end
|
115
|
+
|
116
|
+
#
|
117
|
+
# Utility to die with an error message.
|
118
|
+
# If the last arg is an integer, use it as the exit code.
|
119
|
+
#
|
120
|
+
def self.die *strings
|
121
|
+
exit_code = strings.last.is_a?(Integer) ? strings.pop : -1
|
122
|
+
strings.each{|str| ui.warn str }
|
123
|
+
exit exit_code
|
124
|
+
end
|
125
|
+
|
126
|
+
#
|
127
|
+
# Utility to turn an error into a warning
|
128
|
+
#
|
129
|
+
# @example
|
130
|
+
# Ironfan.safely do
|
131
|
+
# Ironfan.fog_connection.associate_address(self.fog_server.id, address)
|
132
|
+
# end
|
133
|
+
#
|
134
|
+
def self.safely
|
135
|
+
begin
|
136
|
+
yield
|
137
|
+
rescue StandardError => boom
|
138
|
+
ui.info( boom )
|
139
|
+
Chef::Log.error( boom )
|
140
|
+
Chef::Log.error( boom.backtrace.join("\n") )
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|