ironfan 3.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/.gitignore +51 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +130 -0
  4. data/Gemfile +26 -0
  5. data/LICENSE.md +201 -0
  6. data/README.md +328 -0
  7. data/Rakefile +104 -0
  8. data/TODO.md +16 -0
  9. data/VERSION +1 -0
  10. data/chefignore +41 -0
  11. data/cluster_chef-knife.gemspec +123 -0
  12. data/cluster_chef.gemspec +111 -0
  13. data/config/client.rb +59 -0
  14. data/config/proxy.pac +12 -0
  15. data/config/ubuntu10.04-ironfan.erb +157 -0
  16. data/config/ubuntu11.10-ironfan.erb +145 -0
  17. data/ironfan.gemspec +121 -0
  18. data/lib/chef/knife/bootstrap/ubuntu10.04-ironfan.erb +157 -0
  19. data/lib/chef/knife/bootstrap/ubuntu11.10-ironfan.erb +145 -0
  20. data/lib/chef/knife/cluster_bootstrap.rb +74 -0
  21. data/lib/chef/knife/cluster_kick.rb +94 -0
  22. data/lib/chef/knife/cluster_kill.rb +73 -0
  23. data/lib/chef/knife/cluster_launch.rb +164 -0
  24. data/lib/chef/knife/cluster_list.rb +50 -0
  25. data/lib/chef/knife/cluster_proxy.rb +126 -0
  26. data/lib/chef/knife/cluster_show.rb +61 -0
  27. data/lib/chef/knife/cluster_ssh.rb +141 -0
  28. data/lib/chef/knife/cluster_start.rb +40 -0
  29. data/lib/chef/knife/cluster_stop.rb +43 -0
  30. data/lib/chef/knife/cluster_sync.rb +77 -0
  31. data/lib/chef/knife/generic_command.rb +66 -0
  32. data/lib/chef/knife/knife_common.rb +195 -0
  33. data/lib/ironfan.rb +143 -0
  34. data/lib/ironfan/chef_layer.rb +299 -0
  35. data/lib/ironfan/cloud.rb +412 -0
  36. data/lib/ironfan/cluster.rb +118 -0
  37. data/lib/ironfan/compute.rb +153 -0
  38. data/lib/ironfan/deprecated.rb +33 -0
  39. data/lib/ironfan/discovery.rb +177 -0
  40. data/lib/ironfan/dsl_object.rb +124 -0
  41. data/lib/ironfan/facet.rb +144 -0
  42. data/lib/ironfan/fog_layer.rb +150 -0
  43. data/lib/ironfan/private_key.rb +130 -0
  44. data/lib/ironfan/role_implications.rb +58 -0
  45. data/lib/ironfan/security_group.rb +119 -0
  46. data/lib/ironfan/server.rb +281 -0
  47. data/lib/ironfan/server_slice.rb +260 -0
  48. data/lib/ironfan/volume.rb +157 -0
  49. data/spec/ironfan/cluster_spec.rb +13 -0
  50. data/spec/ironfan/facet_spec.rb +69 -0
  51. data/spec/ironfan/server_slice_spec.rb +19 -0
  52. data/spec/ironfan/server_spec.rb +112 -0
  53. data/spec/ironfan_spec.rb +193 -0
  54. data/spec/spec_helper.rb +50 -0
  55. data/spec/spec_helper/dummy_chef.rb +25 -0
  56. data/spec/test_config.rb +20 -0
  57. data/tasks/chef_config.rake +38 -0
  58. data/tasks/jeweler_use_alt_branch.rake +53 -0
  59. 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