cheffish 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/README.md +4 -0
  4. data/Rakefile +23 -0
  5. data/lib/chef/provider/chef_client.rb +44 -0
  6. data/lib/chef/provider/chef_data_bag.rb +50 -0
  7. data/lib/chef/provider/chef_data_bag_item.rb +273 -0
  8. data/lib/chef/provider/chef_environment.rb +78 -0
  9. data/lib/chef/provider/chef_node.rb +82 -0
  10. data/lib/chef/provider/chef_role.rb +79 -0
  11. data/lib/chef/provider/chef_user.rb +48 -0
  12. data/lib/chef/provider/private_key.rb +160 -0
  13. data/lib/chef/provider/public_key.rb +83 -0
  14. data/lib/chef/resource/chef_client.rb +44 -0
  15. data/lib/chef/resource/chef_data_bag.rb +18 -0
  16. data/lib/chef/resource/chef_data_bag_item.rb +114 -0
  17. data/lib/chef/resource/chef_environment.rb +71 -0
  18. data/lib/chef/resource/chef_node.rb +18 -0
  19. data/lib/chef/resource/chef_role.rb +104 -0
  20. data/lib/chef/resource/chef_user.rb +51 -0
  21. data/lib/chef/resource/in_parallel.rb +6 -0
  22. data/lib/chef/resource/private_key.rb +39 -0
  23. data/lib/chef/resource/public_key.rb +16 -0
  24. data/lib/cheffish.rb +245 -0
  25. data/lib/cheffish/actor_provider_base.rb +120 -0
  26. data/lib/cheffish/chef_provider_base.rb +222 -0
  27. data/lib/cheffish/cheffish_server_api.rb +21 -0
  28. data/lib/cheffish/inline_resource.rb +88 -0
  29. data/lib/cheffish/key_formatter.rb +93 -0
  30. data/lib/cheffish/recipe_dsl.rb +98 -0
  31. data/lib/cheffish/version.rb +4 -0
  32. data/spec/integration/chef_client_spec.rb +48 -0
  33. data/spec/integration/chef_node_spec.rb +75 -0
  34. data/spec/integration/chef_user_spec.rb +48 -0
  35. data/spec/integration/private_key_spec.rb +356 -0
  36. data/spec/integration/recipe_dsl_spec.rb +29 -0
  37. data/spec/support/key_support.rb +29 -0
  38. data/spec/support/spec_support.rb +148 -0
  39. metadata +124 -0
@@ -0,0 +1,222 @@
1
+ require 'chef/config'
2
+ require 'chef/run_list'
3
+ require 'cheffish/cheffish_server_api'
4
+
5
+ module Cheffish
6
+ class ChefProviderBase < Chef::Provider::LWRPBase
7
+ def rest
8
+ @rest ||= CheffishServerAPI.new(new_resource.chef_server)
9
+ end
10
+
11
+ def current_resource_exists?
12
+ Array(current_resource.action) != [ :delete ]
13
+ end
14
+
15
+ def not_found_resource
16
+ resource = resource_class.new(new_resource.name)
17
+ resource.action :delete
18
+ resource
19
+ end
20
+
21
+ def normalize_for_put(json)
22
+ data_handler.normalize_for_put(json, fake_entry)
23
+ end
24
+
25
+ def normalize_for_post(json)
26
+ data_handler.normalize_for_post(json, fake_entry)
27
+ end
28
+
29
+ def new_json
30
+ @new_json ||= begin
31
+ if new_resource.complete
32
+ result = normalize(resource_to_json(new_resource))
33
+ else
34
+ # If resource is incomplete, use current json to fill any holes
35
+ result = current_json.merge(resource_to_json(new_resource))
36
+ end
37
+ augment_new_json(result)
38
+ end
39
+ end
40
+
41
+ # Meant to be overridden
42
+ def augment_new_json(json)
43
+ json
44
+ end
45
+
46
+ def current_json
47
+ @current_json ||= begin
48
+ result = normalize(resource_to_json(current_resource))
49
+ result = augment_current_json(result)
50
+ result
51
+ end
52
+ end
53
+
54
+ # Meant to be overridden
55
+ def augment_current_json(json)
56
+ json
57
+ end
58
+
59
+ def resource_to_json(resource)
60
+ json = resource.raw_json || {}
61
+ keys.each do |json_key, resource_key|
62
+ value = resource.send(resource_key)
63
+ json[json_key] = value if value
64
+ end
65
+ json
66
+ end
67
+
68
+ def json_to_resource(json)
69
+ resource = resource_class.new(new_resource.name)
70
+ keys.each do |json_key, resource_key|
71
+ resource.send(resource_key, json[json_key])
72
+ end
73
+ resource
74
+ end
75
+
76
+ def normalize(json)
77
+ data_handler.normalize(json, fake_entry)
78
+ end
79
+
80
+ def json_differences(old_json, new_json, print_values=true, name = '', result = nil)
81
+ result ||= []
82
+ json_differences_internal(old_json, new_json, print_values, name, result)
83
+ result
84
+ end
85
+
86
+ def json_differences_internal(old_json, new_json, print_values, name, result)
87
+ if old_json.kind_of?(Hash) && new_json.kind_of?(Hash)
88
+ removed_keys = old_json.keys.inject({}) { |hash, key| hash[key] = true; hash }
89
+ new_json.each_pair do |new_key, new_value|
90
+ if old_json.has_key?(new_key)
91
+ removed_keys.delete(new_key)
92
+ if new_value != old_json[new_key]
93
+ json_differences_internal(old_json[new_key], new_value, print_values, name == '' ? new_key : "#{name}.#{new_key}", result)
94
+ end
95
+ else
96
+ if print_values
97
+ result << "add #{name == '' ? new_key : "#{name}.#{new_key}"} = #{new_value.inspect}"
98
+ else
99
+ result << "add #{name == '' ? new_key : "#{name}.#{new_key}"}"
100
+ end
101
+ end
102
+ end
103
+ removed_keys.keys.each do |removed_key|
104
+ result << "remove #{name == '' ? removed_key : "#{name}.#{removed_key}"}"
105
+ end
106
+ else
107
+ old_json = old_json.to_s if old_json.kind_of?(Symbol)
108
+ new_json = new_json.to_s if new_json.kind_of?(Symbol)
109
+ if old_json != new_json
110
+ if print_values
111
+ result << "update #{name} from #{old_json.inspect} to #{new_json.inspect}"
112
+ else
113
+ result << "update #{name}"
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ def apply_modifiers(modifiers, json)
120
+ return json if !modifiers || modifiers.size == 0
121
+
122
+ # If the attributes have nothing, set them to {} so we have something to add to
123
+ if json
124
+ json = Marshal.load(Marshal.dump(json)) # Deep copy
125
+ else
126
+ json = {}
127
+ end
128
+
129
+ modifiers.each do |path, value|
130
+ path = [path] if !path.kind_of?(Array)
131
+ path = path.map { |path_part| path_part.to_s }
132
+ parent = path[0..-2].inject(json) { |hash, path_part| hash ? hash[path_part] : nil }
133
+ existing_value = parent ? parent[path[-1]] : nil
134
+
135
+ if value.is_a?(Proc)
136
+ value = value.call(existing_value)
137
+ end
138
+ if value == :delete
139
+ parent.delete(path[-1]) if parent
140
+ # TODO clean up parent chain if hash is completely emptied
141
+ else
142
+ if !parent
143
+ # Create parent if necessary
144
+ parent = path[0..-2].inject(json) do |hash, path_part|
145
+ hash[path_part] = {} if !hash[path_part]
146
+ hash[path_part]
147
+ end
148
+ end
149
+ parent[path[-1]] = value
150
+ end
151
+ end
152
+ json
153
+ end
154
+
155
+ def apply_run_list_modifiers(add_to_run_list, delete_from_run_list, run_list)
156
+ return run_list if (!add_to_run_list || add_to_run_list.size == 0) && (!delete_from_run_list || !delete_from_run_list.size)
157
+ delete_from_run_list ||= []
158
+ add_to_run_list ||= []
159
+
160
+ run_list = Chef::RunList.new(*run_list)
161
+
162
+ result = []
163
+ add_to_run_list_index = 0
164
+ run_list_index = 0
165
+ while run_list_index < run_list.run_list_items.size do
166
+ # See if the desired run list has this item
167
+ found_desired = add_to_run_list.index { |item| same_run_list_item(item, run_list[run_list_index]) }
168
+ if found_desired
169
+ # If so, copy all items up to that desired run list (to preserve order).
170
+ # If a run list item is out of order (run_list = X, B, Y, A, Z and desired = A, B)
171
+ # then this will give us X, A, B. When A is found later, nothing will be copied
172
+ # because found_desired will be less than add_to_run_list_index. The result will
173
+ # be X, A, B, Y, Z.
174
+ if found_desired >= add_to_run_list_index
175
+ result += add_to_run_list[add_to_run_list_index..found_desired].map { |item| item.to_s }
176
+ add_to_run_list_index = found_desired+1
177
+ end
178
+ else
179
+ # If not, just copy it in
180
+ unless delete_from_run_list.index { |item| same_run_list_item(item, run_list[run_list_index]) }
181
+ result << run_list[run_list_index].to_s
182
+ end
183
+ end
184
+ run_list_index += 1
185
+ end
186
+
187
+ # Copy any remaining desired items at the end
188
+ result += add_to_run_list[add_to_run_list_index..-1].map { |item| item.to_s }
189
+ result
190
+ end
191
+
192
+ def same_run_list_item(a, b)
193
+ a_name = a.name
194
+ b_name = b.name
195
+ # Handle "a::default" being the same as "a"
196
+ if a.type == :recipe && a_name =~ /(.+)::default$/
197
+ a_name = $1
198
+ elsif b.type == :recipe && b_name =~ /(.+)::default$/
199
+ b_name = $1
200
+ end
201
+
202
+ a_name == b_name && a.type == b.type # We want to replace things with same name and different version
203
+ end
204
+
205
+ private
206
+
207
+ # Needed to be able to use DataHandler classes
208
+ def fake_entry
209
+ FakeEntry.new("#{new_resource.send(keys.values.first)}.json")
210
+ end
211
+
212
+ class FakeEntry
213
+ def initialize(name, parent = nil)
214
+ @name = name
215
+ @parent = parent
216
+ end
217
+
218
+ attr_reader :name
219
+ attr_reader :parent
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,21 @@
1
+ require 'chef/http'
2
+ require 'chef/http/authenticator'
3
+ require 'chef/http/cookie_manager'
4
+ require 'chef/http/decompressor'
5
+ require 'chef/http/json_input'
6
+ require 'chef/http/json_output'
7
+
8
+ module Cheffish
9
+ # Just like ServerAPI, except it does not default the server URL or options
10
+ class CheffishServerAPI < Chef::HTTP
11
+ def initialize(chef_server)
12
+ super(chef_server[:chef_server_url], chef_server[:options] || {})
13
+ end
14
+
15
+ use Chef::HTTP::JSONInput
16
+ use Chef::HTTP::JSONOutput
17
+ use Chef::HTTP::CookieManager
18
+ use Chef::HTTP::Decompressor
19
+ use Chef::HTTP::Authenticator
20
+ end
21
+ end
@@ -0,0 +1,88 @@
1
+ module Cheffish
2
+ class InlineResource
3
+ def initialize(provider)
4
+ @provider = provider
5
+ end
6
+
7
+ attr_reader :provider
8
+
9
+ def run_context
10
+ provider.run_context
11
+ end
12
+
13
+ def method_missing(method_symbol, *args, &block)
14
+ # Stolen ruthlessly from Chef's chef/dsl/recipe.rb
15
+
16
+ # Checks the new platform => short_name => resource mapping initially
17
+ # then fall back to the older approach (Chef::Resource.const_get) for
18
+ # backward compatibility
19
+ resource_class = Chef::Resource.resource_for_node(method_symbol, provider.run_context.node)
20
+
21
+ super unless resource_class
22
+ raise ArgumentError, "You must supply a name when declaring a #{method_symbol} resource" unless args.size > 0
23
+
24
+ # If we have a resource like this one, we want to steal its state
25
+ args << run_context
26
+ resource = resource_class.new(*args)
27
+ resource.source_line = caller[0]
28
+ resource.load_prior_resource
29
+ resource.cookbook_name = provider.cookbook_name
30
+ resource.recipe_name = @recipe_name
31
+ resource.params = @params
32
+ # Determine whether this resource is being created in the context of an enclosing Provider
33
+ resource.enclosing_provider = provider.is_a?(Chef::Provider) ? provider : nil
34
+ # Evaluate resource attribute DSL
35
+ resource.instance_eval(&block) if block
36
+
37
+ # Run optional resource hook
38
+ resource.after_created
39
+
40
+ # Do NOT put this in the resource collection.
41
+ #run_context.resource_collection.insert(resource)
42
+
43
+ # Instead, run the action directly.
44
+ Array(resource.action).each do |action|
45
+ resource.updated_by_last_action(false)
46
+ run_provider_action(resource.provider_for_action(action))
47
+ provider.new_resource.updated_by_last_action(true) if resource.updated_by_last_action?
48
+ end
49
+ resource
50
+ end
51
+
52
+ # Do Chef::Provider.run_action, but without events
53
+ def run_provider_action(inline_provider)
54
+ if !inline_provider.whyrun_supported?
55
+ raise "#{inline_provider} is not why-run-safe. Only why-run-safe resources are supported in inline_resource."
56
+ end
57
+
58
+ # Blatantly ripped off from chef/provider run_action
59
+
60
+ # TODO: it would be preferable to get the action to be executed in the
61
+ # constructor...
62
+
63
+ # user-defined LWRPs may include unsafe load_current_resource methods that cannot be run in whyrun mode
64
+ inline_provider.load_current_resource
65
+ inline_provider.define_resource_requirements
66
+ inline_provider.process_resource_requirements
67
+
68
+ # user-defined providers including LWRPs may
69
+ # not include whyrun support - if they don't support it
70
+ # we can't execute any actions while we're running in
71
+ # whyrun mode. Instead we 'fake' whyrun by documenting that
72
+ # we can't execute the action.
73
+ # in non-whyrun mode, this will still cause the action to be
74
+ # executed normally.
75
+ if inline_provider.whyrun_supported? && !inline_provider.requirements.action_blocked?(@action)
76
+ inline_provider.send("action_#{inline_provider.action}")
77
+ elsif !inline_provider.whyrun_mode?
78
+ inline_provider.send("action_#{inline_provider.action}")
79
+ end
80
+
81
+ if inline_provider.resource_updated?
82
+ inline_provider.new_resource.updated_by_last_action(true)
83
+ end
84
+
85
+ inline_provider.cleanup_after_converge
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,93 @@
1
+ require 'openssl'
2
+ require 'net/ssh'
3
+ require 'etc'
4
+ require 'socket'
5
+ require 'digest/md5'
6
+
7
+ module Cheffish
8
+ class KeyFormatter
9
+ # Returns nil or key, format
10
+ def self.decode(str, pass_phrase=nil, filename='')
11
+ key_format = {}
12
+ key_format[:format] = format_of(str)
13
+
14
+ case key_format[:format]
15
+ when :openssh
16
+ key = decode_openssh_key(str, filename)
17
+ else
18
+ begin
19
+ key = OpenSSL::PKey.read(str) { pass_phrase }
20
+ rescue
21
+ return nil
22
+ end
23
+ end
24
+
25
+ key_format[:type] = type_of(key)
26
+ key_format[:size] = size_of(key)
27
+ key_format[:pass_phrase] = pass_phrase if pass_phrase
28
+ # TODO cipher, exponent
29
+
30
+ [key, key_format]
31
+ end
32
+
33
+ def self.encode(key, key_format)
34
+ format = key_format[:format] || :pem
35
+ case format
36
+ when :openssh
37
+ encode_openssh_key(key)
38
+ when :pem
39
+ if key_format[:pass_phrase]
40
+ cipher = key_format[:cipher] || 'DES-EDE3-CBC'
41
+ key.to_pem(OpenSSL::Cipher.new(cipher), key_format[:pass_phrase])
42
+ else
43
+ key.to_pem
44
+ end
45
+ when :der
46
+ key.to_der
47
+ when :fingerprint
48
+ hexes = Digest::MD5.hexdigest(key.to_der)
49
+ # Put : between every pair of hexes
50
+ hexes.scan(/../).join(':')
51
+ else
52
+ raise "Unrecognized key format #{format}"
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def self.encode_openssh_key(key)
59
+ # TODO there really isn't a method somewhere in net/ssh or openssl that does this??
60
+ type = key.ssh_type
61
+ data = [ key.to_blob ].pack('m0')
62
+ "#{type} #{data} #{Etc.getlogin}@#{Socket.gethostname}"
63
+ end
64
+
65
+ def self.decode_openssh_key(str, filename='')
66
+ Net::SSH::KeyFactory.load_data_public_key(str, filename)
67
+ end
68
+
69
+ def self.format_of(key_contents)
70
+ if key_contents.start_with?('-----BEGIN ')
71
+ :pem
72
+ elsif key_contents.start_with?('ssh-rsa ') || key_contents.start_with?('ssh-dss ')
73
+ :openssh
74
+ else
75
+ :der
76
+ end
77
+ end
78
+
79
+ def self.type_of(key)
80
+ case key.class
81
+ when OpenSSL::PKey::RSA
82
+ :rsa
83
+ when OpenSSL::PKey::DSA
84
+ :dsa
85
+ end
86
+ end
87
+
88
+ def self.size_of(key)
89
+ # TODO DSA -- this is RSA only
90
+ key.n.num_bytes * 8
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,98 @@
1
+ require 'cheffish'
2
+
3
+ require 'chef_zero/server'
4
+ require 'chef/chef_fs/chef_fs_data_store'
5
+ require 'chef/chef_fs/config'
6
+
7
+ class Chef
8
+ class Recipe
9
+ def with_chef_data_bag(name)
10
+ old_enclosing_data_bag = Cheffish.enclosing_data_bag
11
+ Cheffish.enclosing_data_bag = name
12
+ if block_given?
13
+ begin
14
+ yield
15
+ ensure
16
+ Cheffish.enclosing_data_bag = old_enclosing_data_bag
17
+ end
18
+ end
19
+ end
20
+
21
+ def with_chef_environment(name)
22
+ old_enclosing_environment = Cheffish.enclosing_environment
23
+ Cheffish.enclosing_environment = name
24
+ if block_given?
25
+ begin
26
+ yield
27
+ ensure
28
+ Cheffish.enclosing_environment = old_enclosing_environment
29
+ end
30
+ end
31
+ end
32
+
33
+ def with_chef_data_bag_item_encryption(encryption_options)
34
+ old_enclosing_data_bag_item_encryption = Cheffish.enclosing_data_bag_item_encryption
35
+ Cheffish.enclosing_data_bag_item_encryption = encryption_options
36
+ if block_given?
37
+ begin
38
+ yield
39
+ ensure
40
+ Cheffish.enclosing_data_bag_item_encryption = old_enclosing_data_bag_item_encryption
41
+ end
42
+ end
43
+ end
44
+
45
+ def with_chef_server(server_url, options = {})
46
+ old_enclosing_chef_server = Cheffish.enclosing_chef_server
47
+ Cheffish.enclosing_chef_server = { :chef_server_url => server_url, :options => options }
48
+ if block_given?
49
+ begin
50
+ yield
51
+ ensure
52
+ Cheffish.enclosing_chef_server = old_enclosing_chef_server
53
+ end
54
+ end
55
+ end
56
+
57
+ def with_chef_local_server(options, &block)
58
+ options[:host] ||= '127.0.0.1'
59
+ options[:log_level] ||= Chef::Log.level
60
+ options[:port] ||= 8900
61
+
62
+ # Create the data store chef-zero will use
63
+ options[:data_store] ||= begin
64
+ if !options[:chef_repo_path]
65
+ raise "chef_repo_path must be specified to with_chef_local_server"
66
+ end
67
+
68
+ # Ensure all paths are given
69
+ %w(acl client cookbook container data_bag environment group node role).each do |type|
70
+ options["#{type}_path".to_sym] ||= begin
71
+ if options[:chef_repo_path].kind_of?(String)
72
+ Chef::Config.path_join(options[:chef_repo_path], "#{type}s")
73
+ else
74
+ options[:chef_repo_path].map { |path| Chef::Config.path_join(path, "#{type}s")}
75
+ end
76
+ end
77
+ # Work around issue in earlier versions of ChefFS where it expects strings for these
78
+ # instead of symbols
79
+ options["#{type}_path"] = options["#{type}_path".to_sym]
80
+ end
81
+
82
+ chef_fs = Chef::ChefFS::Config.new(options).local_fs
83
+ chef_fs.write_pretty_json = true
84
+ Chef::ChefFS::ChefFSDataStore.new(chef_fs)
85
+ end
86
+
87
+ # Start the chef-zero server
88
+ Chef::Log.info("Starting chef-zero on port #{options[:port]} with repository at #{options[:data_store].chef_fs.fs_description}")
89
+ chef_zero_server = ChefZero::Server.new(options)
90
+ chef_zero_server.start_background
91
+
92
+ @@local_servers ||= []
93
+ @@local_servers << chef_zero_server
94
+
95
+ with_chef_server(chef_zero_server.url, &block)
96
+ end
97
+ end
98
+ end