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,118 @@
1
+ module Ironfan
2
+ #
3
+ # A cluster has many facets. Any setting applied here is merged with the facet
4
+ # at resolve time; if the facet explicitly sets any attributes they will win out.
5
+ #
6
+ class Cluster < Ironfan::ComputeBuilder
7
+ attr_reader :facets, :undefined_servers
8
+
9
+ def initialize(name, attrs={})
10
+ super(name.to_sym, attrs)
11
+ @facets = Mash.new
12
+ @chef_roles = []
13
+ environment :_default if environment.blank?
14
+ create_cluster_role
15
+ create_cluster_security_group unless attrs[:no_security_group]
16
+ end
17
+
18
+ def cluster
19
+ self
20
+ end
21
+
22
+ def cluster_name
23
+ name
24
+ end
25
+
26
+ # The auto-generated role for this cluster.
27
+ # Instance-evals the given block in the context of that role
28
+ #
29
+ # @example
30
+ # cluster_role do
31
+ # override_attributes({
32
+ # :time_machine => { :transition_speed => 88 },
33
+ # })
34
+ # end
35
+ #
36
+ # @return [Chef::Role] The auto-generated role for this facet.
37
+ def cluster_role(&block)
38
+ @cluster_role.instance_eval( &block ) if block_given?
39
+ @cluster_role
40
+ end
41
+
42
+ #
43
+ # Retrieve or define the given facet
44
+ #
45
+ # @param [String] facet_name -- name of the desired facet
46
+ # @param [Hash] attrs -- attributes to configure on the object
47
+ # @yield a block to execute in the context of the object
48
+ #
49
+ # @return [Ironfan::Facet]
50
+ #
51
+ def facet(facet_name, attrs={}, &block)
52
+ facet_name = facet_name.to_sym
53
+ @facets[facet_name] ||= Ironfan::Facet.new(self, facet_name)
54
+ @facets[facet_name].configure(attrs, &block)
55
+ @facets[facet_name]
56
+ end
57
+
58
+ def has_facet? facet_name
59
+ @facets.include?(facet_name)
60
+ end
61
+
62
+ def find_facet(facet_name)
63
+ @facets[facet_name] or raise("Facet '#{facet_name}' is not defined in cluster '#{cluster_name}'")
64
+ end
65
+
66
+ # All servers in this facet, sorted by facet name and index
67
+ #
68
+ # @return [Ironfan::ServerSlice] slice containing all servers
69
+ def servers
70
+ svrs = @facets.sort.map{|name, facet| facet.servers.to_a }
71
+ Ironfan::ServerSlice.new(self, svrs.flatten)
72
+ end
73
+
74
+ #
75
+ # A slice of a cluster:
76
+ #
77
+ # If +facet_name+ is nil, returns all servers.
78
+ # Otherwise, takes slice (given by +*args+) from the requested facet.
79
+ #
80
+ # @param [String] facet_name -- facet to slice (or nil for all in cluster)
81
+ # @param [Array, String] slice_indexes -- servers in that facet (or nil for all in facet).
82
+ # You must specify a facet if you use slice_indexes.
83
+ #
84
+ # @return [Ironfan::ServerSlice] the requested slice
85
+ def slice facet_name=nil, slice_indexes=nil
86
+ return Ironfan::ServerSlice.new(self, self.servers) if facet_name.nil?
87
+ find_facet(facet_name).slice(slice_indexes)
88
+ end
89
+
90
+ def to_s
91
+ "#{super[0..-3]} @facets=>#{@facets.keys.inspect}}>"
92
+ end
93
+
94
+ #
95
+ # Resolve:
96
+ #
97
+ def resolve!
98
+ facets.values.each(&:resolve!)
99
+ end
100
+
101
+ protected
102
+
103
+ # Create a security group named for the cluster
104
+ # that is friends with everything in the cluster
105
+ def create_cluster_security_group
106
+ clname = self.name # put it in scope
107
+ cloud.security_group(clname){ authorize_group(clname) }
108
+ end
109
+
110
+ # Creates a chef role named for the cluster
111
+ def create_cluster_role
112
+ @cluster_role_name = "#{name}_cluster"
113
+ @cluster_role = new_chef_role(@cluster_role_name, cluster)
114
+ role(@cluster_role_name, :own)
115
+ end
116
+
117
+ end
118
+ end
@@ -0,0 +1,153 @@
1
+ module Ironfan
2
+ #
3
+ # Base class allowing us to layer settings for facet over cluster
4
+ #
5
+ class ComputeBuilder < Ironfan::DslObject
6
+ attr_reader :cloud, :volumes, :chef_roles
7
+ has_keys :name, :bogosity, :environment
8
+ @@role_implications ||= Mash.new
9
+ @@run_list_rank ||= 0
10
+
11
+ def initialize(builder_name, attrs={})
12
+ super(attrs)
13
+ set :name, builder_name
14
+ @run_list_info = attrs[:run_list] || Mash.new
15
+ @volumes = Mash.new
16
+ end
17
+
18
+ # set the bogosity to a descriptive reason. Anything truthy implies bogusness
19
+ def bogus?
20
+ !! self.bogosity
21
+ end
22
+
23
+ # Magic method to produce cloud instance:
24
+ # * returns the cloud instance, creating it if necessary.
25
+ # * executes the block in the cloud's object context
26
+ #
27
+ # @example
28
+ # cloud do
29
+ # image_name 'maverick'
30
+ # security_group :nagios
31
+ # end
32
+ #
33
+ # # defines ec2-specific behavior
34
+ # cloud(:ec2) do
35
+ # public_ip '1.2.3.4'
36
+ # region 'us-east-1d'
37
+ # end
38
+ #
39
+ def cloud(cloud_provider=nil, attrs={}, &block)
40
+ raise "Only have ec2 so far" if cloud_provider && (cloud_provider != :ec2)
41
+ @cloud ||= Ironfan::Cloud::Ec2.new(self)
42
+ @cloud.configure(attrs, &block)
43
+ @cloud
44
+ end
45
+
46
+ # sugar for cloud(:ec2)
47
+ def ec2(attrs={}, &block)
48
+ cloud(:ec2, attrs, &block)
49
+ end
50
+
51
+ # Magic method to describe a volume
52
+ # * returns the named volume, creating it if necessary.
53
+ # * executes the block (if any) in the volume's context
54
+ #
55
+ # @example
56
+ # # a 1 GB volume at '/data' from the given snapshot
57
+ # volume(:data) do
58
+ # size 1
59
+ # mount_point '/data'
60
+ # snapshot_id 'snap-12345'
61
+ # end
62
+ #
63
+ # @param volume_name [String] an arbitrary handle -- you can use the device
64
+ # name, or a descriptive symbol.
65
+ # @param attrs [Hash] a hash of attributes to pass down.
66
+ #
67
+ def volume(volume_name, attrs={}, &block)
68
+ volumes[volume_name] ||= Ironfan::Volume.new(:parent => self, :name => volume_name)
69
+ volumes[volume_name].configure(attrs, &block)
70
+ volumes[volume_name]
71
+ end
72
+
73
+ def raid_group(rg_name, attrs={}, &block)
74
+ volumes[rg_name] ||= Ironfan::RaidGroup.new(:parent => self, :name => rg_name)
75
+ volumes[rg_name].configure(attrs, &block)
76
+ volumes[rg_name].sub_volumes.each do |sv_name|
77
+ volume(sv_name){ in_raid(rg_name) ; mountable(false) ; tags({}) }
78
+ end
79
+ volumes[rg_name]
80
+ end
81
+
82
+ def root_volume(attrs={}, &block)
83
+ volume(:root, attrs, &block)
84
+ end
85
+
86
+ #
87
+ # Adds the given role to the run list, and invokes any role_implications it
88
+ # implies (for instance, defining and applying the 'ssh' security group if
89
+ # the 'ssh' role is applied.)
90
+ #
91
+ # You can specify placement of `:first`, `:normal` (or nil) or `:last`; the
92
+ # final runlist is assembled as
93
+ #
94
+ # * run_list :first items -- cluster, then facet, then server
95
+ # * run_list :normal items -- cluster, then facet, then server
96
+ # * run_list :last items -- cluster, then facet, then server
97
+ #
98
+ # (see Ironfan::Server#combined_run_list for full details though)
99
+ #
100
+ def role(role_name, placement=nil)
101
+ add_to_run_list("role[#{role_name}]", placement)
102
+ self.instance_eval(&@@role_implications[role_name]) if @@role_implications[role_name]
103
+ end
104
+
105
+ # Add the given recipe to the run list. You can specify placement of
106
+ # `:first`, `:normal` (or nil) or `:last`; the final runlist is assembled as
107
+ #
108
+ # * run_list :first items -- cluster, then facet, then server
109
+ # * run_list :normal items -- cluster, then facet, then server
110
+ # * run_list :last items -- cluster, then facet, then server
111
+ #
112
+ # (see Ironfan::Server#combined_run_list for full details though)
113
+ #
114
+ def recipe(name, placement=nil)
115
+ add_to_run_list(name, placement)
116
+ end
117
+
118
+ # Roles and recipes for this element only.
119
+ #
120
+ # See Ironfan::Server#combined_run_list for run_list order resolution
121
+ def run_list
122
+ groups = run_list_groups
123
+ [ groups[:first], groups[:normal], groups[:last] ].flatten.compact.uniq
124
+ end
125
+
126
+ # run list elements grouped into :first, :normal and :last
127
+ def run_list_groups
128
+ @run_list_info.keys.sort_by{|item| @run_list_info[item][:rank] }.group_by{|item| @run_list_info[item][:placement] }
129
+ end
130
+
131
+ #
132
+ # Some roles imply aspects of the machine that have to exist at creation.
133
+ # For instance, on an ec2 machine you may wish the 'ssh' role to imply a
134
+ # security group explicity opening port 22.
135
+ #
136
+ # @param [String] role_name -- the role that triggers the block
137
+ # @yield block will be instance_eval'd in the object that calls 'role'
138
+ #
139
+ def self.role_implication(name, &block)
140
+ @@role_implications[name] = block
141
+ end
142
+
143
+ protected
144
+
145
+ def add_to_run_list(item, placement)
146
+ raise "run_list placement must be one of :first, :normal, :last or nil (also means :normal)" unless [:first, :last, :own, nil].include?(placement)
147
+ @@run_list_rank += 1
148
+ placement ||= :normal
149
+ @run_list_info[item] ||= { :rank => @@run_list_rank, :placement => placement }
150
+ end
151
+
152
+ end
153
+ end
@@ -0,0 +1,33 @@
1
+ module Ironfan
2
+
3
+ class Cluster
4
+ #
5
+ # **DEPRECATED**: This doesn't really work -- use +reverse_merge!+ instead
6
+ #
7
+ def use(*clusters)
8
+ ui.warn "The 'use' statement is deprecated #{caller.inspect}"
9
+ clusters.each do |c|
10
+ other_cluster = Ironfan.load_cluster(c)
11
+ reverse_merge! other_cluster
12
+ end
13
+ self
14
+ end
15
+
16
+ end
17
+
18
+ class Server
19
+ # **DEPRECATED**: Please use +fullname+ instead.
20
+ def chef_node_name name
21
+ ui.warn "[DEPRECATION] `chef_node_name` is deprecated. Please use `fullname` instead."
22
+ fullname name
23
+ end
24
+ end
25
+
26
+ class Cloud::Ec2
27
+ # **DEPRECATED**: Please use +public_ip+ instead.
28
+ def elastic_ip(*args, &block)
29
+ public_ip(*args, &block)
30
+ end
31
+ end
32
+
33
+ end
@@ -0,0 +1,177 @@
1
+ module Ironfan
2
+ class Cluster
3
+
4
+ def discover!
5
+ @aws_instance_hash = {}
6
+ discover_ironfan!
7
+ discover_chef_nodes!
8
+ discover_fog_servers! unless Ironfan.chef_config[:cloud] == false
9
+ discover_chef_clients!
10
+ discover_volumes!
11
+ end
12
+
13
+ def chef_clients
14
+ return @chef_clients if @chef_clients
15
+ @chef_clients = []
16
+
17
+ # Oh for fuck's sake -- the key used to index clients changed from
18
+ # 'clientname' in 0.10.4-and-prev to 'name' in 0.10.8. Rather than index
19
+ # both 'clientname' and 'name', they switched it -- so we have to fall
20
+ # back. FIXME: While the Opscode platform is 0.10.4 I have clientname
21
+ # first (sorry, people of the future). When it switches to 0.10.8 we'll
22
+ # reverse them (suck it people of the past).
23
+ # Also sometimes the server returns results that are nil on
24
+ # recently-expired clients, so that's annoying too.
25
+ clients, wtf, num = Chef::Search::Query.new.search(:client, "clientname:#{cluster_name}-*") ; clients.compact!
26
+ clients, wtf, num = Chef::Search::Query.new.search(:client, "name:#{cluster_name}-*") if clients.blank?
27
+ clients.each do |client_hsh|
28
+ next if client_hsh.nil?
29
+ # Return values from Chef::Search seem to be inconsistent across chef
30
+ # versions (sometimes a hash, sometimes an object). Fix if necessary.
31
+ client_hsh = Chef::ApiClient.json_create(client_hsh) unless client_hsh.is_a?(Chef::ApiClient)
32
+ @chef_clients.push( client_hsh )
33
+ end
34
+ @chef_clients
35
+ end
36
+
37
+ # returns client with the given name if in catalog, nil otherwise
38
+ def find_client(cl_name)
39
+ chef_clients.find{|ccl| ccl.name == cl_name }
40
+ end
41
+
42
+ def chef_nodes
43
+ return @chef_nodes if @chef_nodes
44
+ @chef_nodes = []
45
+ Chef::Search::Query.new.search(:node,"cluster_name:#{cluster_name}") do |n|
46
+ @chef_nodes.push(n) unless n.blank? || (n.cluster_name != cluster_name.to_s)
47
+ end
48
+ @chef_nodes
49
+ end
50
+
51
+ # returns node with the given name if in catalog, nil otherwise
52
+ def find_node(nd_name)
53
+ chef_nodes.find{|nd| nd.name == nd_name }
54
+ end
55
+
56
+ protected
57
+
58
+ def fog_servers
59
+ @fog_servers ||= Ironfan.fog_servers.select{|fs| fs.key_name == cluster_name.to_s && (fs.state != "terminated") }
60
+ end
61
+
62
+ # Walk the list of chef nodes and
63
+ # * vivify the server,
64
+ # * associate the chef node
65
+ # * if the chef node knows about its instance id, memorize that for lookup
66
+ # when we discover cloud instances.
67
+ def discover_chef_nodes!
68
+ chef_nodes.each do |chef_node|
69
+ if chef_node["cluster_name"] && chef_node["facet_name"] && chef_node["facet_index"]
70
+ cluster_name = chef_node["cluster_name"]
71
+ facet_name = chef_node["facet_name"]
72
+ facet_index = chef_node["facet_index"]
73
+ elsif chef_node.name
74
+ ( cluster_name, facet_name, facet_index ) = chef_node.name.split(/-/)
75
+ else
76
+ next
77
+ end
78
+ svr = Ironfan::Server.get(cluster_name, facet_name, facet_index)
79
+ svr.chef_node = chef_node
80
+ @aws_instance_hash[ chef_node.ec2.instance_id ] = svr if chef_node[:ec2] && chef_node.ec2.instance_id
81
+ end
82
+ end
83
+
84
+ # Walk the list of servers, asking each to discover its chef client.
85
+ def discover_chef_clients!
86
+ servers.each(&:chef_client)
87
+ end
88
+
89
+ # calling #servers vivifies each facet's Ironfan::Server instances
90
+ def discover_ironfan!
91
+ self.servers
92
+ end
93
+
94
+ def discover_fog_servers!
95
+ # If the fog server is tagged with cluster/facet/index, then try to
96
+ # locate the corresponding machine in the cluster def
97
+ # Otherwise, try to get to it through mapping the aws instance id
98
+ # to the chef node name found in the chef node
99
+ fog_servers.each do |fs|
100
+ if fs.tags["cluster"] && fs.tags["facet"] && fs.tags["index"] && fs.tags["cluster"] == cluster_name.to_s
101
+ svr = Ironfan::Server.get(fs.tags["cluster"], fs.tags["facet"], fs.tags["index"])
102
+ elsif @aws_instance_hash[fs.id]
103
+ svr = @aws_instance_hash[fs.id]
104
+ else
105
+ next
106
+ end
107
+
108
+ # If there already is a fog server there, then issue a warning and slap
109
+ # the just-discovered one onto a server with an arbitrary index, and
110
+ # mark both bogus
111
+ if existing_fs = svr.fog_server
112
+ if existing_fs.id != fs.id
113
+ ui.warn "Duplicate fog instance found for #{svr.fullname}: #{fs.id} and #{existing_fs.id}!!"
114
+ old_svr = svr
115
+ svr = old_svr.facet.server(1_000 + svr.facet_index.to_i)
116
+ old_svr.bogosity :duplicate
117
+ svr.bogosity :duplicate
118
+ end
119
+ end
120
+ svr.fog_server = fs
121
+ end
122
+ end
123
+
124
+ def discover_volumes!
125
+ servers.each(&:discover_volumes!)
126
+ end
127
+
128
+ def discover_addresses!
129
+ servers.each(&:discover_addresses!)
130
+ end
131
+
132
+ end # Ironfan::Cluster
133
+ end
134
+
135
+ module Ironfan
136
+
137
+ def self.fog_connection
138
+ @fog_connection ||= Fog::Compute.new({
139
+ :provider => 'AWS',
140
+ :aws_access_key_id => Chef::Config[:knife][:aws_access_key_id],
141
+ :aws_secret_access_key => Chef::Config[:knife][:aws_secret_access_key],
142
+ :region => Chef::Config[:knife][:region]
143
+ })
144
+ end
145
+
146
+ def self.fog_servers
147
+ return @fog_servers if @fog_servers
148
+ Chef::Log.debug("Using fog to catalog all servers")
149
+ @fog_servers = Ironfan.fog_connection.servers.all
150
+ end
151
+
152
+ def self.fog_addresses
153
+ return @fog_addresses if @fog_addresses
154
+ Chef::Log.debug("Using fog to catalog all addresses")
155
+ @fog_addresses = {}.tap{|hsh| Ironfan.fog_connection.addresses.each{|fa| hsh[fa.public_ip] = fa } }
156
+ end
157
+
158
+ def self.fog_volumes
159
+ @fog_volumes || fetch_fog_volumes
160
+ end
161
+
162
+ def self.fetch_fog_volumes
163
+ Chef::Log.debug("Using fog to catalog all volumes")
164
+ @fog_volumes = Ironfan.fog_connection.volumes
165
+ end
166
+
167
+ def self.fog_keypairs
168
+ return @fog_keypairs if @fog_keypairs
169
+ Chef::Log.debug("Using fog to catalog all keypairs")
170
+ @fog_keypairs = {}.tap{|hsh| Ironfan.fog_connection.key_pairs.each{|kp| hsh[kp.name] = kp } }
171
+ end
172
+
173
+ def safely *args, &block
174
+ Ironfan.safely(*args, &block)
175
+ end
176
+
177
+ end