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.
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