cheffish 0.1

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