cluster_chef 3.0.5

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 (46) hide show
  1. data/.gitignore +51 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +63 -0
  4. data/Gemfile +18 -0
  5. data/LICENSE +201 -0
  6. data/README.md +332 -0
  7. data/Rakefile +92 -0
  8. data/TODO.md +8 -0
  9. data/VERSION +1 -0
  10. data/chefignore +41 -0
  11. data/cluster_chef.gemspec +115 -0
  12. data/clusters/website_demo.rb +65 -0
  13. data/config/client.rb +59 -0
  14. data/lib/cluster_chef/chef_layer.rb +297 -0
  15. data/lib/cluster_chef/cloud.rb +409 -0
  16. data/lib/cluster_chef/cluster.rb +118 -0
  17. data/lib/cluster_chef/compute.rb +144 -0
  18. data/lib/cluster_chef/cookbook_munger/README.md.erb +47 -0
  19. data/lib/cluster_chef/cookbook_munger/licenses.yaml +16 -0
  20. data/lib/cluster_chef/cookbook_munger/metadata.rb.erb +23 -0
  21. data/lib/cluster_chef/cookbook_munger.rb +588 -0
  22. data/lib/cluster_chef/deprecated.rb +33 -0
  23. data/lib/cluster_chef/discovery.rb +158 -0
  24. data/lib/cluster_chef/dsl_object.rb +123 -0
  25. data/lib/cluster_chef/facet.rb +144 -0
  26. data/lib/cluster_chef/fog_layer.rb +134 -0
  27. data/lib/cluster_chef/private_key.rb +110 -0
  28. data/lib/cluster_chef/role_implications.rb +49 -0
  29. data/lib/cluster_chef/security_group.rb +103 -0
  30. data/lib/cluster_chef/server.rb +265 -0
  31. data/lib/cluster_chef/server_slice.rb +259 -0
  32. data/lib/cluster_chef/volume.rb +93 -0
  33. data/lib/cluster_chef.rb +137 -0
  34. data/notes/aws_console_screenshot.jpg +0 -0
  35. data/rspec.watchr +29 -0
  36. data/spec/cluster_chef/cluster_spec.rb +13 -0
  37. data/spec/cluster_chef/facet_spec.rb +70 -0
  38. data/spec/cluster_chef/server_slice_spec.rb +19 -0
  39. data/spec/cluster_chef/server_spec.rb +112 -0
  40. data/spec/cluster_chef_spec.rb +193 -0
  41. data/spec/spec_helper/dummy_chef.rb +25 -0
  42. data/spec/spec_helper.rb +50 -0
  43. data/spec/test_config.rb +20 -0
  44. data/tasks/chef_config.rb +38 -0
  45. data/tasks/jeweler_use_alt_branch.rb +47 -0
  46. metadata +227 -0
@@ -0,0 +1,158 @@
1
+ module ClusterChef
2
+ class Cluster
3
+
4
+ def discover!
5
+ @aws_instance_hash = {}
6
+ discover_cluster_chef!
7
+ discover_chef_nodes!
8
+ discover_fog_servers!
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
+ Chef::Search::Query.new.search(:client, "clientname:#{cluster_name}-*") do |client_hsh|
17
+ # Return values from Chef::Search seem to be inconsistent across chef
18
+ # versions (sometimes a hash, sometimes an object). Fix if necessary.
19
+ client_hsh = Chef::ApiClient.json_create(client_hsh) unless client_hsh.is_a?(Chef::ApiClient)
20
+ @chef_clients.push( client_hsh )
21
+ end
22
+ @chef_clients
23
+ end
24
+
25
+ # returns client with the given name if in catalog, nil otherwise
26
+ def find_client(cl_name)
27
+ chef_clients.find{|ccl| ccl.name == cl_name }
28
+ end
29
+
30
+ def chef_nodes
31
+ return @chef_nodes if @chef_nodes
32
+ @chef_nodes = []
33
+ Chef::Search::Query.new.search(:node,"cluster_name:#{cluster_name}") do |n|
34
+ @chef_nodes.push(n) unless n.blank? || (n.cluster_name != cluster_name.to_s)
35
+ end
36
+ @chef_nodes
37
+ end
38
+
39
+ # returns node with the given name if in catalog, nil otherwise
40
+ def find_node(nd_name)
41
+ chef_nodes.find{|nd| nd.name == nd_name }
42
+ end
43
+
44
+ protected
45
+
46
+ def fog_servers
47
+ @fog_servers ||= ClusterChef.fog_servers.select{|fs| fs.key_name == cluster_name.to_s && (fs.state != "terminated") }
48
+ end
49
+
50
+ # Walk the list of chef nodes and
51
+ # * vivify the server,
52
+ # * associate the chef node
53
+ # * if the chef node knows about its instance id, memorize that for lookup
54
+ # when we discover cloud instances.
55
+ def discover_chef_nodes!
56
+ chef_nodes.each do |chef_node|
57
+ if chef_node["cluster_name"] && chef_node["facet_name"] && chef_node["facet_index"]
58
+ cluster_name = chef_node["cluster_name"]
59
+ facet_name = chef_node["facet_name"]
60
+ facet_index = chef_node["facet_index"]
61
+ elsif chef_node.name
62
+ ( cluster_name, facet_name, facet_index ) = chef_node.name.split(/-/)
63
+ else
64
+ next
65
+ end
66
+ svr = ClusterChef::Server.get(cluster_name, facet_name, facet_index)
67
+ svr.chef_node = chef_node
68
+ @aws_instance_hash[ chef_node.ec2.instance_id ] = svr if chef_node[:ec2] && chef_node.ec2.instance_id
69
+ end
70
+ end
71
+
72
+ # Walk the list of servers, asking each to discover its chef client.
73
+ def discover_chef_clients!
74
+ servers.each(&:chef_client)
75
+ end
76
+
77
+ # calling #servers vivifies each facet's ClusterChef::Server instances
78
+ def discover_cluster_chef!
79
+ self.servers
80
+ end
81
+
82
+ def discover_fog_servers!
83
+ # If the fog server is tagged with cluster/facet/index, then try to
84
+ # locate the corresponding machine in the cluster def
85
+ # Otherwise, try to get to it through mapping the aws instance id
86
+ # to the chef node name found in the chef node
87
+ fog_servers.each do |fs|
88
+ if fs.tags["cluster"] && fs.tags["facet"] && fs.tags["index"] && fs.tags["cluster"] == cluster_name.to_s
89
+ svr = ClusterChef::Server.get(fs.tags["cluster"], fs.tags["facet"], fs.tags["index"])
90
+ elsif @aws_instance_hash[fs.id]
91
+ svr = @aws_instance_hash[fs.id]
92
+ else
93
+ next
94
+ end
95
+
96
+ # If there already is a fog server there, then issue a warning and slap
97
+ # the just-discovered one onto a server with an arbitrary index, and
98
+ # mark both bogus
99
+ if existing_fs = svr.fog_server
100
+ if existing_fs.id != fs.id
101
+ ui.warn "Duplicate fog instance found for #{svr.fullname}: #{fs.id} and #{existing_fs.id}!!"
102
+ old_svr = svr
103
+ svr = old_svr.facet.server(1_000 + svr.facet_index.to_i)
104
+ old_svr.bogosity :duplicate
105
+ svr.bogosity :duplicate
106
+ end
107
+ end
108
+ svr.fog_server = fs
109
+ end
110
+ end
111
+
112
+ def discover_volumes!
113
+ servers.each(&:discover_volumes!)
114
+ end
115
+
116
+ def discover_addresses!
117
+ servers.each(&:discover_addresses!)
118
+ end
119
+ end
120
+
121
+ def self.fog_connection
122
+ @fog_connection ||= Fog::Compute.new({
123
+ :provider => 'AWS',
124
+ :aws_access_key_id => Chef::Config[:knife][:aws_access_key_id],
125
+ :aws_secret_access_key => Chef::Config[:knife][:aws_secret_access_key],
126
+ :region => Chef::Config[:knife][:region]
127
+ })
128
+ end
129
+
130
+ def self.fog_servers
131
+ return @fog_servers if @fog_servers
132
+ Chef::Log.debug("Using fog to catalog all servers")
133
+ @fog_servers = ClusterChef.fog_connection.servers.all
134
+ end
135
+
136
+ def self.fog_volumes
137
+ return @fog_volumes if @fog_volumes
138
+ Chef::Log.debug("Using fog to catalog all volumes")
139
+ @fog_volumes ||= ClusterChef.fog_connection.volumes
140
+ end
141
+
142
+ def self.fog_addresses
143
+ return @fog_addresses if @fog_addresses
144
+ Chef::Log.debug("Using fog to catalog all addresses")
145
+ @fog_addresses = {}.tap{|hsh| ClusterChef.fog_connection.addresses.each{|fa| hsh[fa.public_ip] = fa } }
146
+ end
147
+
148
+ def self.fog_keypairs
149
+ return @fog_keypairs if @fog_keypairs
150
+ Chef::Log.debug("Using fog to catalog all keypairs")
151
+ @fog_keypairs = {}.tap{|hsh| ClusterChef.fog_connection.key_pairs.each{|kp| hsh[kp.name] = kp } }
152
+ end
153
+
154
+ def safely *args, &block
155
+ ClusterChef.safely(*args, &block)
156
+ end
157
+
158
+ end
@@ -0,0 +1,123 @@
1
+ Mash.class_eval do
2
+ def reverse_merge!(other_hash)
3
+ # stupid mash doesn't take a block arg, which breaks the implementation of
4
+ # reverse_merge!
5
+ other_hash.each_pair do |key, value|
6
+ key = convert_key(key)
7
+ regular_writer(key, convert_value(value)) unless has_key?(key)
8
+ end
9
+ self
10
+ end
11
+ def to_mash
12
+ self.dup
13
+ end unless method_defined?(:to_mash)
14
+ end
15
+
16
+ Hash.class_eval do
17
+ def to_mash
18
+ Mash.new(self)
19
+ end unless method_defined?(:to_mash)
20
+ end
21
+
22
+ module ClusterChef
23
+ #
24
+ # Provides magic methods, defined with has_keys
25
+ #
26
+ # @example
27
+ # class Mom < ClusterChef::DslObject
28
+ # has_keys(:college, :combat_boots, :fat, :so_fat)
29
+ # end
30
+ #
31
+ # class Person
32
+ # def momma &block
33
+ # @momma ||= Mom.new
34
+ # @momma.configure(&block) if block
35
+ # end
36
+ # end
37
+ #
38
+ # yo = Person.new
39
+ # yo.mamma.combat_boots :wears
40
+ # yo.momma do
41
+ # fat true
42
+ # so_fat 'When she sits around the house, she sits *AROUND* the house'
43
+ # end
44
+ #
45
+ class DslObject
46
+ class_attribute :keys
47
+ self.keys = []
48
+
49
+ def initialize(attrs={})
50
+ @settings = attrs.to_mash || Mash.new
51
+ end
52
+
53
+ #
54
+ # Defines DSL attributes
55
+ #
56
+ # @params [Array(String)] key_names DSL attribute names
57
+ #
58
+ # @example
59
+ # class Mom < ClusterChef::DslObject
60
+ # has_keys(:fat, :so_fat)
61
+ # end
62
+ # yer_mom = Mom.new
63
+ # yer_mom.fat :quite
64
+ #
65
+ def self.has_keys(*key_names)
66
+ key_names.map!(&:to_sym)
67
+ self.keys += key_names
68
+ self.keys.uniq!
69
+ key_names.each do |key|
70
+ next if method_defined?(key)
71
+ define_method(key){|*args| set(key, *args) }
72
+ end
73
+ end
74
+
75
+ #
76
+ # Sets the DSL attribute, unless the given value is nil.
77
+ #
78
+ def set(key, val=nil)
79
+ @settings[key.to_s] = val unless val.nil?
80
+ @settings[key.to_s]
81
+ end
82
+
83
+ def to_hash
84
+ @settings.to_hash
85
+ end
86
+
87
+ def to_mash
88
+ @settings.dup
89
+ end
90
+
91
+ def to_s
92
+ "<#{self.class} #{to_hash.inspect}>"
93
+ end
94
+
95
+ def reverse_merge!(hsh)
96
+ @settings.reverse_merge!(hsh.to_hash)
97
+ end
98
+
99
+ def configure(hsh={}, &block)
100
+ @settings.merge!(hsh.to_hash)
101
+ instance_eval(&block) if block
102
+ self
103
+ end
104
+
105
+ # delegate to the knife ui presenter
106
+ def ui() ClusterChef.ui ; end
107
+ # delegate to the knife ui presenter
108
+ def self.ui() ClusterChef.ui ; end
109
+
110
+ def step(desc, *style)
111
+ ui.info(" #{"%-15s" % (name.to_s+":")}\t#{ui.color(desc.to_s, *style)}")
112
+ end
113
+
114
+ # helper method for bombing out of a script
115
+ def die(*args) ClusterChef.die(*args) ; end
116
+
117
+ # helper method for turning exceptions into warnings
118
+ def safely(*args, &block) ClusterChef.safely(*args, &block) ; end
119
+
120
+ # helper method for debugging only
121
+ def dump(*args) args.each{|arg| Chef::Log.debug( arg.inspect ) } end
122
+ end
123
+ end
@@ -0,0 +1,144 @@
1
+ module ClusterChef
2
+ class Facet < ClusterChef::ComputeBuilder
3
+ attr_reader :cluster
4
+ has_keys :instances
5
+
6
+ def initialize cluster, facet_name, attrs={}
7
+ super(facet_name.to_sym, attrs)
8
+ @cluster = cluster
9
+ @servers = Mash.new
10
+ @chef_roles = []
11
+ @settings[:instances] ||= 0
12
+ create_facet_role
13
+ create_facet_security_group unless attrs[:no_security_group]
14
+ end
15
+
16
+ def cluster_name
17
+ cluster.name
18
+ end
19
+
20
+ def facet_name
21
+ name
22
+ end
23
+
24
+ # The auto-generated role for this facet.
25
+ # Instance-evals the given block in the context of that role,
26
+ #
27
+ # @example
28
+ # facet_role do
29
+ # override_attributes({
30
+ # :time_machine => { :transition_speed => 88 },
31
+ # })
32
+ # end
33
+ #
34
+ # @return [Chef::Role] The auto-generated role for this facet.
35
+ def facet_role(&block)
36
+ @facet_role.instance_eval( &block ) if block_given?
37
+ @facet_role
38
+ end
39
+
40
+ def assign_volume_ids(volume_name, *volume_ids)
41
+ volume_ids.flatten.zip(servers).each do |volume_id, server|
42
+ server.volume(volume_name){ volume_id(volume_id) } if server
43
+ end
44
+ end
45
+
46
+ #
47
+ # Retrieve or define the given server
48
+ #
49
+ # @param [Integer] idx -- the index of the desired server
50
+ # @param [Hash] attrs -- attributes to configure on the object
51
+ # @yield a block to execute in the context of the object
52
+ #
53
+ # @return [ClusterChef::Facet]
54
+ #
55
+ def server(idx, attrs={}, &block)
56
+ idx = idx.to_i
57
+ @servers[idx] ||= ClusterChef::Server.new(self, idx)
58
+ @servers[idx].configure(attrs, &block)
59
+ @servers[idx]
60
+ end
61
+
62
+ # if the server has been added to this facet or is in range
63
+ def has_server? idx
64
+ (idx.to_i < instances) || @servers.include?(idx.to_i)
65
+ end
66
+
67
+ #
68
+ # Slicing
69
+ #
70
+
71
+ # All servers in this facet
72
+ #
73
+ # @return [ClusterChef::ServerSlice] slice containing all servers
74
+ def servers
75
+ slice(indexes)
76
+ end
77
+
78
+ #
79
+ # A slice of servers from this facet, in index order
80
+ #
81
+ # If +slice_indexes+ is nil, returns all servers.
82
+ # Otherwise, takes slice (given by +*args+) from the requested facet.
83
+ #
84
+ # @param [Array, String] slice_indexes -- servers in that facet (or nil for all in facet).
85
+ #
86
+ # @return [ClusterChef::ServerSlice] the requested slice
87
+ def slice(slice_indexes=nil)
88
+ slice_indexes = self.indexes if slice_indexes.blank?
89
+ slice_indexes = indexes_from_intervals(slice_indexes) if slice_indexes.is_a?(String)
90
+ svrs = Array(slice_indexes).map(&:to_i).sort!.select{|idx| has_server?(idx) }.map{|idx| server(idx) }
91
+ ClusterChef::ServerSlice.new(self.cluster, svrs)
92
+ end
93
+
94
+ # all valid server indexes
95
+ def valid_indexes
96
+ (0 ... instances).to_a # note the '...'
97
+ end
98
+
99
+ # indexes in the 0...instances range plus bogus ones that showed up
100
+ # (probably from chef or fog)
101
+ def indexes
102
+ [@servers.keys, valid_indexes].flatten.compact.uniq.sort
103
+ end
104
+
105
+ #
106
+ # Resolve:
107
+ #
108
+ def resolve!
109
+ servers.each(&:resolve!)
110
+ end
111
+
112
+ protected
113
+
114
+ def create_facet_security_group
115
+ cloud.security_group("#{cluster_name}-#{facet_name}")
116
+ end
117
+
118
+ # Creates a chef role named for the facet
119
+ def create_facet_role
120
+ @facet_role_name = "#{cluster_name}_#{facet_name}"
121
+ @facet_role = new_chef_role(@facet_role_name, cluster, self)
122
+ role(@facet_role_name, :last)
123
+ end
124
+
125
+ #
126
+ # Given a string enumerating indexes to select returns a flat array of
127
+ # indexes. The indexes will be unique but in an arbitrary order.
128
+ #
129
+ # @example
130
+ # facet = ClusterChef::Facet.new('foo', 'bar')
131
+ # facet.indexes_from_intervals('1,2-3,8-9,7') # [1, 2, 3, 8, 9, 7]
132
+ # facet.indexes_from_intervals('1,3-5,4,7') # [1, 3, 4, 5, 7]
133
+ #
134
+ def indexes_from_intervals intervals
135
+ intervals.split(",").map do |term|
136
+ if term =~ /^(\d+)-(\d+)$/ then ($1.to_i .. $2.to_i).to_a
137
+ elsif term =~ /^(\d+)$/ then $1.to_i
138
+ else ui.warn("Bad interval: #{term}") ; nil
139
+ end
140
+ end.flatten.compact.uniq
141
+ end
142
+
143
+ end
144
+ end
@@ -0,0 +1,134 @@
1
+ module ClusterChef
2
+ #
3
+ # ClusterChef::Server methods that handle Fog action
4
+ #
5
+ Server.class_eval do
6
+
7
+ def fog_create_server
8
+ step(" creating cloud server", :green)
9
+ fog_description = fog_description_for_launch
10
+ Chef::Log.debug(JSON.pretty_generate(fog_description))
11
+ safely do
12
+ @fog_server = ClusterChef.fog_connection.servers.create(fog_description)
13
+ end
14
+ end
15
+
16
+ def fog_description_for_launch
17
+ {
18
+ :image_id => cloud.image_id,
19
+ :flavor_id => cloud.flavor,
20
+ #
21
+ :groups => cloud.security_groups.keys,
22
+ :key_name => cloud.keypair.to_s,
23
+ # Fog does not actually create tags when it creates a server.
24
+ :tags => {
25
+ :cluster => cluster_name,
26
+ :facet => facet_name,
27
+ :index => facet_index, },
28
+ :user_data => JSON.pretty_generate(cloud.user_data),
29
+ :block_device_mapping => block_device_mapping,
30
+ # :disable_api_termination => cloud.permanent,
31
+ # :instance_initiated_shutdown_behavior => instance_initiated_shutdown_behavior,
32
+ :availability_zone => self.default_availability_zone,
33
+ :monitoring => cloud.monitoring,
34
+ }
35
+ end
36
+
37
+ #
38
+ # Takes key-value pairs and idempotently sets those tags on the cloud machine
39
+ #
40
+ def fog_create_tags(fog_obj, desc, tags)
41
+ tags.each do |key, value|
42
+ next if fog_obj.tags[key] == value.to_s
43
+ Chef::Log.debug( "tagging #{key} = #{value} on #{desc}" )
44
+ safely do
45
+ ClusterChef.fog_connection.tags.create({
46
+ :key => key, :value => value.to_s, :resource_id => fog_obj.id })
47
+ end
48
+ end
49
+ end
50
+
51
+ def fog_address
52
+ address_str = self.cloud.public_ip or return
53
+ ClusterChef.fog_addresses[address_str]
54
+ end
55
+
56
+ def discover_volumes!
57
+ composite_volumes.each do |vol_name, vol|
58
+ my_vol = volumes[vol_name]
59
+ next if my_vol.fog_volume
60
+ my_vol.fog_volume = ClusterChef.fog_volumes.find do |fv|
61
+ ( # matches the explicit volume id
62
+ (vol.volume_id && (fv.id == vol.volume_id) ) ||
63
+ # OR this server's machine exists, and this volume is attached to
64
+ # it, and in the right place
65
+ ( fog_server && fv.server_id && vol.device &&
66
+ (fv.server_id == fog_server.id) &&
67
+ (fv.device.to_s == vol.device.to_s) ) ||
68
+ # OR this volume is tagged as belonging to this machine
69
+ ( fv.tags.present? &&
70
+ (fv.tags['server'] == self.fullname) &&
71
+ (fv.tags['device'] == vol.device.to_s) )
72
+ )
73
+ end
74
+ next unless my_vol.fog_volume
75
+ my_vol.volume_id(my_vol.fog_volume.id) unless my_vol.volume_id.present?
76
+ my_vol.availability_zone(my_vol.fog_volume.availability_zone) unless my_vol.availability_zone.present?
77
+ check_server_id_pairing(my_vol.fog_volume, my_vol.desc)
78
+ end
79
+ end
80
+
81
+ def attach_volumes
82
+ return unless in_cloud?
83
+ discover_volumes!
84
+ return if composite_volumes.empty?
85
+ step(" attaching volumes")
86
+ composite_volumes.each do |vol_name, vol|
87
+ next if vol.volume_id.blank? || (vol.attachable != :ebs)
88
+ if (not vol.in_cloud?) then Chef::Log.debug("Volume not found: #{vol.desc}") ; next ; end
89
+ if (vol.has_server?) then check_server_id_pairing(vol.fog_volume, vol.desc) ; next ; end
90
+ step(" - attaching #{vol.desc} -- #{vol.inspect}", :blue)
91
+ safely do
92
+ vol.fog_volume.device = vol.device
93
+ vol.fog_volume.server = fog_server
94
+ end
95
+ end
96
+ end
97
+
98
+ def associate_public_ip
99
+ address = self.cloud.public_ip
100
+ return unless self.in_cloud? && address
101
+ desc = "elastic ip #{address} for #{self.fullname}"
102
+ if (fog_address && fog_address.server_id) then check_server_id_pairing(fog_address, desc) ; return ; end
103
+ safely do
104
+ step(" assigning #{desc}", :blue)
105
+ ClusterChef.fog_connection.associate_address(self.fog_server.id, address)
106
+ end
107
+ end
108
+
109
+ def check_server_id_pairing thing, desc
110
+ return unless thing && thing.server_id && self.in_cloud?
111
+ type_of_thing = thing.class.to_s.gsub(/.*::/,"")
112
+ if thing.server_id != self.fog_server.id
113
+ ui.warn "#{type_of_thing} mismatch: #{desc} is on #{thing.server_id} not #{self.fog_server.id}: #{thing.inspect.gsub(/\s+/m,' ')}"
114
+ false
115
+ else
116
+ Chef::Log.debug("#{type_of_thing} paired: #{desc}")
117
+ true
118
+ end
119
+ end
120
+
121
+ end
122
+
123
+ class ServerSlice
124
+ def sync_keypairs
125
+ step("ensuring keypairs exist")
126
+ keypairs = servers.map{|svr| [svr.cluster.cloud.keypair, svr.cloud.keypair] }.flatten.map(&:to_s).reject(&:blank?).uniq
127
+ keypairs = keypairs - ClusterChef.fog_keypairs.keys
128
+ keypairs.each do |keypair_name|
129
+ keypair_obj = ClusterChef::Ec2Keypair.create!(keypair_name)
130
+ ClusterChef.fog_keypairs[keypair_name] = keypair_obj
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,110 @@
1
+ require 'fileutils'
2
+
3
+ module ClusterChef
4
+ #
5
+ # A private key -- chef client key, ssh key, etc.
6
+ #
7
+ # The key is a pro
8
+ class PrivateKey < ClusterChef::DslObject
9
+ attr_reader :name
10
+ attr_reader :proxy
11
+ attr_reader :on_update
12
+
13
+ #
14
+ # PrivateKey.new('bob')
15
+ #
16
+ # @yield a block, executed in caller's context, when the body is updated
17
+ # @yieldparam the updated body
18
+ def initialize(name, proxy=nil, &on_update)
19
+ super()
20
+ @name = name
21
+ @proxy = proxy
22
+ @on_update = on_update
23
+ end
24
+
25
+ def filename
26
+ File.join(key_dir, "#{name}.pem")
27
+ end
28
+
29
+ def save
30
+ return unless @body
31
+ if ClusterChef.chef_config[:dry_run]
32
+ Chef::Log.debug(" key #{name} - dry run, not writing out key")
33
+ return
34
+ end
35
+ ui.info( " key #{name} - writing to #{filename}" )
36
+ FileUtils.mkdir_p(File.dirname(filename))
37
+ File.open(filename, "w", 0600){|f| f.print( @body ) }
38
+ end
39
+
40
+ def load
41
+ return unless File.exists?(filename)
42
+ self.body = File.read(filename).chomp
43
+ end
44
+
45
+ def body=(content)
46
+ @body = content
47
+ on_update.call(content) if on_update
48
+ content
49
+ end
50
+
51
+ def self.create!(name, *args, &block)
52
+ obj = self.new(name, *args, &block)
53
+ obj.create_proxy!
54
+ obj
55
+ end
56
+
57
+ def to_s
58
+ [super[0..-2], @name, @proxy, @body.to_s[32..64], '...', @body.to_s[-60..-30]].join(" ").gsub(/[\r\n\t]+/,'') + '>'
59
+ end
60
+ end
61
+
62
+ class ChefClientKey < PrivateKey
63
+ def body
64
+ return @body if @body
65
+ if proxy && proxy.private_key && (not proxy.private_key.empty?)
66
+ @body = proxy.private_key
67
+ else
68
+ load
69
+ end
70
+ @body
71
+ end
72
+
73
+ def key_dir
74
+ Chef::Config.client_key_dir || '/tmp/client_keys'
75
+ end
76
+ end
77
+
78
+ class Ec2Keypair < PrivateKey
79
+ def body
80
+ return @body if @body
81
+ if proxy && proxy.private_key && (not proxy.private_key.empty?)
82
+ @body = proxy.private_key
83
+ else
84
+ load
85
+ end
86
+ @body
87
+ end
88
+
89
+ def create_proxy!
90
+ safely do
91
+ step(" key #{name} - creating", :green)
92
+ @proxy = ClusterChef.fog_connection.key_pairs.create(:name => name.to_s)
93
+ end
94
+ ClusterChef.fog_keypairs[name] = proxy
95
+ self.body = proxy.private_key
96
+ save
97
+ end
98
+
99
+ def key_dir
100
+ if Chef::Config.ec2_key_dir
101
+ return Chef::Config.ec2_key_dir
102
+ else
103
+ dir = "#{ENV['HOME']}/.chef/ec2_keys"
104
+ warn "Please set 'ec2_key_dir' in your knife.rb -- using #{dir} as a default"
105
+ dir
106
+ end
107
+ end
108
+ end
109
+
110
+ end