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,51 @@
1
+ require 'cheffish'
2
+ require 'chef/resource/lwrp_base'
3
+
4
+ class Chef::Resource::ChefUser < Chef::Resource::LWRPBase
5
+ self.resource_name = 'chef_user'
6
+
7
+ actions :create, :delete, :nothing
8
+ default_action :create
9
+
10
+ # Grab environment from with_environment
11
+ def initialize(*args)
12
+ super
13
+ chef_server Cheffish.enclosing_chef_server
14
+ end
15
+
16
+ # Client attributes
17
+ attribute :name, :kind_of => String, :regex => Cheffish::NAME_REGEX, :name_attribute => true
18
+ attribute :admin, :kind_of => [TrueClass, FalseClass]
19
+ attribute :email, :kind_of => String
20
+ attribute :external_authentication_uid
21
+ attribute :recovery_authentication_enabled, :kind_of => [TrueClass, FalseClass]
22
+ attribute :password, :kind_of => String # Hmm. There is no way to idempotentize this.
23
+ #attribute :salt # TODO server doesn't support sending or receiving these, but it's the only way to backup / restore a user
24
+ #attribute :hashed_password
25
+ #attribute :hash_type
26
+
27
+ # Input key
28
+ attribute :source_key # String or OpenSSL::PKey::*
29
+ attribute :source_key_path, :kind_of => String
30
+ attribute :source_key_pass_phrase
31
+
32
+ # Output public key (if so desired)
33
+ attribute :output_key_path, :kind_of => String
34
+ attribute :output_key_format, :kind_of => Symbol, :default => :openssh, :equal_to => [ :pem, :der, :openssh ]
35
+
36
+ # If this is set, client is not patchy
37
+ attribute :complete, :kind_of => [TrueClass, FalseClass]
38
+
39
+ attribute :raw_json, :kind_of => Hash
40
+ attribute :chef_server, :kind_of => Hash
41
+
42
+ # Proc that runs just before the resource executes. Called with (resource)
43
+ def before(&block)
44
+ block ? @before = block : @before
45
+ end
46
+
47
+ # Proc that runs after the resource completes. Called with (resource, json, private_key, public_key)
48
+ def after(&block)
49
+ block ? @after = block : @after
50
+ end
51
+ end
@@ -0,0 +1,6 @@
1
+
2
+
3
+ class Chef::Resource::InParallel < Chef::Resource
4
+ def initialize(&block)
5
+ end
6
+ end
@@ -0,0 +1,39 @@
1
+ require 'openssl/cipher'
2
+ require 'chef/resource/lwrp_base'
3
+
4
+ class Chef::Resource::PrivateKey < Chef::Resource::LWRPBase
5
+ self.resource_name = 'private_key'
6
+
7
+ actions :create, :delete, :regenerate, :nothing
8
+ default_action :create
9
+
10
+ # Path to private key. Set to :none to create the key in memory and not on disk.
11
+ attribute :path, :kind_of => [ String, Symbol ], :name_attribute => true
12
+ attribute :format, :kind_of => Symbol, :default => :pem, :equal_to => [ :pem, :der ]
13
+ attribute :type, :kind_of => Symbol, :default => :rsa, :equal_to => [ :rsa, :dsa ] # TODO support :ec
14
+ # These specify an optional public_key you can spit out if you want.
15
+ attribute :public_key_path, :kind_of => String
16
+ attribute :public_key_format, :kind_of => Symbol, :default => :openssh, :equal_to => [ :openssh, :pem, :der ]
17
+ # Specify this if you want to copy another private key but give it a different format / password
18
+ attribute :source_key
19
+ attribute :source_key_path, :kind_of => String
20
+ attribute :source_key_pass_phrase
21
+
22
+ # RSA and DSA
23
+ attribute :size, :kind_of => Integer, :default => 2048
24
+
25
+ # RSA-only
26
+ attribute :exponent, :kind_of => Integer # For RSA
27
+
28
+ # PEM-only
29
+ attribute :pass_phrase, :kind_of => String
30
+ attribute :cipher, :kind_of => String, :default => 'DES-EDE3-CBC', :equal_to => OpenSSL::Cipher.ciphers
31
+
32
+ # Set this to regenerate the key if it does not have the desired characteristics (like size, type, etc.)
33
+ attribute :regenerate_if_different, :kind_of => [TrueClass, FalseClass]
34
+
35
+ # Proc that runs after the resource completes. Called with (resource, private_key)
36
+ def after(&block)
37
+ block ? @after = block : @after
38
+ end
39
+ end
@@ -0,0 +1,16 @@
1
+ require 'openssl/cipher'
2
+ require 'chef/resource/lwrp_base'
3
+
4
+ class Chef::Resource::PublicKey < Chef::Resource::LWRPBase
5
+ self.resource_name = 'public_key'
6
+
7
+ actions :create, :delete, :nothing
8
+ default_action :create
9
+
10
+ attribute :path, :kind_of => String, :name_attribute => true
11
+ attribute :format, :kind_of => Symbol, :default => :openssh, :equal_to => [ :pem, :der, :openssh ]
12
+
13
+ attribute :source_key
14
+ attribute :source_key_path, :kind_of => String
15
+ attribute :source_key_pass_phrase
16
+ end
@@ -0,0 +1,245 @@
1
+ require 'chef/run_list/run_list_item'
2
+ require 'cheffish/inline_resource'
3
+
4
+ module Cheffish
5
+ NAME_REGEX = /^[.\-[:alnum:]_]+$/
6
+
7
+ @@enclosing_data_bag = nil
8
+ def self.enclosing_data_bag
9
+ @@enclosing_data_bag
10
+ end
11
+ def self.enclosing_data_bag=(name)
12
+ @@enclosing_data_bag = name
13
+ end
14
+
15
+ @@enclosing_environment = nil
16
+ def self.enclosing_environment
17
+ @@enclosing_environment
18
+ end
19
+ def self.enclosing_environment=(name)
20
+ @@enclosing_environment = name
21
+ end
22
+
23
+ @@enclosing_data_bag_item_encryption = nil
24
+ def self.enclosing_data_bag_item_encryption
25
+ @@enclosing_data_bag_item_encryption
26
+ end
27
+ def self.enclosing_data_bag_item_encryption=(options)
28
+ @@enclosing_data_bag_item_encryption = options
29
+ end
30
+
31
+ def self.inline_resource(provider, &block)
32
+ InlineResource.new(provider).instance_eval(&block)
33
+ end
34
+
35
+ @@enclosing_chef_server = nil
36
+ def self.enclosing_chef_server
37
+ @@enclosing_chef_server || {
38
+ :chef_server_url => Chef::Config[:chef_server_url],
39
+ :options => {
40
+ :client_name => Chef::Config[:node_name],
41
+ :signing_key_filename => Chef::Config[:client_key]
42
+ }
43
+ }
44
+ end
45
+ def self.enclosing_chef_server=(chef_server)
46
+ @@enclosing_chef_server = chef_server
47
+ end
48
+
49
+ def self.reset
50
+ @@enclosing_data_bag = nil
51
+ @@enclosing_environment = nil
52
+ @@enclosing_data_bag_item_encryption = nil
53
+ @@enclosing_chef_server = nil
54
+ end
55
+
56
+ NOT_PASSED=Object.new
57
+
58
+ def self.node_attributes(klass)
59
+ klass.class_eval do
60
+ attribute :name, :kind_of => String, :regex => Cheffish::NAME_REGEX, :name_attribute => true
61
+ attribute :chef_environment, :kind_of => String, :regex => Cheffish::NAME_REGEX
62
+ attribute :run_list, :kind_of => Array # We should let them specify it as a series of parameters too
63
+ attribute :default_attributes, :kind_of => Hash
64
+ attribute :normal_attributes, :kind_of => Hash
65
+ attribute :override_attributes, :kind_of => Hash
66
+ attribute :automatic_attributes, :kind_of => Hash
67
+
68
+ # Specifies that this is a complete specification for the environment (i.e. attributes you don't specify will be
69
+ # reset to their defaults)
70
+ attribute :complete, :kind_of => [TrueClass, FalseClass]
71
+
72
+ attribute :raw_json, :kind_of => Hash
73
+ attribute :chef_server, :kind_of => Hash
74
+
75
+ # default 'ip_address', '127.0.0.1'
76
+ # default [ 'pushy', 'port' ], '9000'
77
+ # default 'ip_addresses' do |existing_value|
78
+ # (existing_value || []) + [ '127.0.0.1' ]
79
+ # end
80
+ # default 'ip_address', :delete
81
+ attr_accessor :default_modifiers
82
+ def default(attribute_path, value=Cheffish.NOT_PASSED, &block)
83
+ @default_modifiers ||= []
84
+ if value != Cheffish.NOT_PASSED
85
+ @default_modifiers << [ attribute_path, value ]
86
+ elsif block
87
+ @default_modifiers << [ attribute_path, block ]
88
+ else
89
+ raise "default requires either a value or a block"
90
+ end
91
+ end
92
+
93
+ # normal 'ip_address', '127.0.0.1'
94
+ # normal [ 'pushy', 'port' ], '9000'
95
+ # normal 'ip_addresses' do |existing_value|
96
+ # (existing_value || []) + [ '127.0.0.1' ]
97
+ # end
98
+ # normal 'ip_address', :delete
99
+ attr_accessor :normal_modifiers
100
+ def normal(attribute_path, value=NOT_PASSED, &block)
101
+ @normal_modifiers ||= []
102
+ if value != NOT_PASSED
103
+ @normal_modifiers << [ attribute_path, value ]
104
+ elsif block
105
+ @normal_modifiers << [ attribute_path, block ]
106
+ else
107
+ raise "normal requires either a value or a block"
108
+ end
109
+ end
110
+
111
+ # override 'ip_address', '127.0.0.1'
112
+ # override [ 'pushy', 'port' ], '9000'
113
+ # override 'ip_addresses' do |existing_value|
114
+ # (existing_value || []) + [ '127.0.0.1' ]
115
+ # end
116
+ # override 'ip_address', :delete
117
+ attr_accessor :override_modifiers
118
+ def override(attribute_path, value=NOT_PASSED, &block)
119
+ @override_modifiers ||= []
120
+ if value != NOT_PASSED
121
+ @override_modifiers << [ attribute_path, value ]
122
+ elsif block
123
+ @override_modifiers << [ attribute_path, block ]
124
+ else
125
+ raise "override requires either a value or a block"
126
+ end
127
+ end
128
+
129
+ # automatic 'ip_address', '127.0.0.1'
130
+ # automatic [ 'pushy', 'port' ], '9000'
131
+ # automatic 'ip_addresses' do |existing_value|
132
+ # (existing_value || []) + [ '127.0.0.1' ]
133
+ # end
134
+ # automatic 'ip_address', :delete
135
+ attr_accessor :automatic_modifiers
136
+ def automatic(attribute_path, value=NOT_PASSED, &block)
137
+ @automatic_modifiers ||= []
138
+ if value != NOT_PASSED
139
+ @automatic_modifiers << [ attribute_path, value ]
140
+ elsif block
141
+ @automatic_modifiers << [ attribute_path, block ]
142
+ else
143
+ raise "automatic requires either a value or a block"
144
+ end
145
+ end
146
+
147
+ # Patchy tags
148
+ # tag 'webserver', 'apache', 'myenvironment'
149
+ def tag(*tags)
150
+ normal 'tags' do |existing_tags|
151
+ existing_tags ||= []
152
+ tags.each do |tag|
153
+ if !existing_tags.include?(tag.to_s)
154
+ existing_tags << tag.to_s
155
+ end
156
+ end
157
+ existing_tags
158
+ end
159
+ end
160
+ def remove_tag(*tags)
161
+ normal 'tags' do |existing_tags|
162
+ if existing_tags
163
+ tags.each do |tag|
164
+ existing_tags.delete(tag.to_s)
165
+ end
166
+ end
167
+ existing_tags
168
+ end
169
+ end
170
+
171
+ # NON-patchy tags
172
+ # tags :a, :b, :c # removes all other tags
173
+ def tags(*tags)
174
+ if tags.size == 0
175
+ normal('tags')
176
+ else
177
+ tags = tags[0] if tags.size == 1 && tags[0].kind_of?(Array)
178
+ normal 'tags', tags.map { |tag| tag.to_s }
179
+ end
180
+ end
181
+
182
+
183
+ alias :attributes :normal_attributes
184
+ alias :attribute :normal
185
+
186
+ # Order matters--if two things here are in the wrong order, they will be flipped in the run list
187
+ # recipe 'apache', 'mysql'
188
+ # recipe 'recipe@version'
189
+ # recipe 'recipe'
190
+ # role ''
191
+ attr_accessor :run_list_modifiers
192
+ attr_accessor :run_list_removers
193
+ def recipe(*recipes)
194
+ if recipes.size == 0
195
+ raise ArgumentError, "At least one recipe must be specified"
196
+ end
197
+ @run_list_modifiers ||= []
198
+ @run_list_modifiers += recipes.map { |recipe| Chef::RunList::RunListItem.new("recipe[#{recipe}]") }
199
+ end
200
+ def role(*roles)
201
+ if roles.size == 0
202
+ raise ArgumentError, "At least one role must be specified"
203
+ end
204
+ @run_list_modifiers ||= []
205
+ @run_list_modifiers += roles.map { |role| Chef::RunList::RunListItem.new("role[#{role}]") }
206
+ end
207
+ def remove_recipe(*recipes)
208
+ if recipes.size == 0
209
+ raise ArgumentError, "At least one recipe must be specified"
210
+ end
211
+ @run_list_removers ||= []
212
+ @run_list_removers += recipes.map { |recipe| Chef::RunList::RunListItem.new("recipe[#{recipe}]") }
213
+ end
214
+ def remove_role(*roles)
215
+ if roles.size == 0
216
+ raise ArgumentError, "At least one role must be specified"
217
+ end
218
+ @run_list_removers ||= []
219
+ @run_list_removers += roles.map { |recipe| Chef::RunList::RunListItem.new("role[#{role}]") }
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+ # Include all recipe objects so require 'cheffish' brings in the whole recipe DSL
226
+
227
+ require 'cheffish/recipe_dsl'
228
+ require 'chef/resource/chef_client'
229
+ require 'chef/resource/chef_data_bag'
230
+ require 'chef/resource/chef_data_bag_item'
231
+ require 'chef/resource/chef_environment'
232
+ require 'chef/resource/chef_node'
233
+ require 'chef/resource/chef_role'
234
+ require 'chef/resource/chef_user'
235
+ require 'chef/resource/private_key'
236
+ require 'chef/resource/public_key'
237
+ require 'chef/provider/chef_client'
238
+ require 'chef/provider/chef_data_bag'
239
+ require 'chef/provider/chef_data_bag_item'
240
+ require 'chef/provider/chef_environment'
241
+ require 'chef/provider/chef_node'
242
+ require 'chef/provider/chef_role'
243
+ require 'chef/provider/chef_user'
244
+ require 'chef/provider/private_key'
245
+ require 'chef/provider/public_key'
@@ -0,0 +1,120 @@
1
+ require 'cheffish/key_formatter'
2
+ require 'cheffish/chef_provider_base'
3
+
4
+ class Cheffish::ActorProviderBase < Cheffish::ChefProviderBase
5
+
6
+ def create_actor
7
+ if new_resource.before
8
+ new_resource.before.call(new_resource)
9
+ end
10
+
11
+ # Create or update the client/user
12
+ current_public_key = new_json['public_key']
13
+ differences = json_differences(current_json, new_json)
14
+ if current_resource_exists?
15
+ # Update the actor if it's different
16
+ if differences.size > 0
17
+ description = [ "update #{actor_type} #{new_resource.name} at #{rest.url}" ] + differences
18
+ converge_by description do
19
+ result = rest.put("#{actor_type}s/#{new_resource.name}", normalize_for_put(new_json))
20
+ current_public_key, current_public_key_format = Cheffish::KeyFormatter.decode(result['public_key']) if result['public_key']
21
+ end
22
+ end
23
+ else
24
+ # Create the actor if it's missing
25
+ if !new_public_key
26
+ raise "You must specify a public key to create a #{actor_type}! Use the private_key resource to create a key, and pass it in with source_key_path."
27
+ end
28
+ description = [ "create #{actor_type} #{new_resource.name} at #{rest.url}" ] + differences
29
+ converge_by description do
30
+ result = rest.post("#{actor_type}s", normalize_for_post(new_json))
31
+ current_public_key, current_public_key_format = Cheffish::KeyFormatter.decode(result['public_key']) if result['public_key']
32
+ end
33
+ end
34
+
35
+ # Write out the public key
36
+ if new_resource.output_key_path
37
+ # TODO use inline_resource
38
+ key_content = Cheffish::KeyFormatter.encode(current_public_key, { :format => new_resource.output_key_format })
39
+ if !current_resource.output_key_path
40
+ action = 'create'
41
+ elsif key_content != IO.read(current_resource.output_key_path)
42
+ action = 'overwrite'
43
+ else
44
+ action = nil
45
+ end
46
+ if action
47
+ converge_by "#{action} public key #{new_resource.output_key_path}" do
48
+ IO.write(new_resource.output_key_path, key_content)
49
+ end
50
+ end
51
+ # TODO permissions?
52
+ end
53
+
54
+ if new_resource.after
55
+ new_resource.after.call(self, new_json, server_private_key, server_public_key)
56
+ end
57
+ end
58
+
59
+ def delete_actor
60
+ if current_resource_exists?
61
+ converge_by "delete #{actor_type} #{new_resource.name} at #{rest.url}" do
62
+ rest.delete("#{actor_type}s/#{new_resource.name}")
63
+ Chef::Log.info("#{new_resource} deleted #{actor_type} #{new_resource.name} at #{rest.url}")
64
+ end
65
+ end
66
+ if current_resource.output_key_path
67
+ converge_by "delete public key #{current_resource.output_key_path}" do
68
+ ::File.unlink(current_resource.output_key_path)
69
+ end
70
+ end
71
+ end
72
+
73
+ def new_public_key
74
+ @new_public_key ||= begin
75
+ if new_resource.source_key
76
+ if new_resource.source_key.is_a?(String)
77
+ Cheffish::KeyFormatter.decode(new_resource.source_key)
78
+ elsif new_resource.source_key.private?
79
+ new_resource.source_key.public_key
80
+ else
81
+ new_resource.source_key
82
+ end
83
+ elsif new_resource.source_key_path
84
+ source_key, source_key_format = Cheffish::KeyFormatter.decode(IO.read(new_resource.source_key_path), new_resource.source_key_pass_phrase, new_resource.source_key_path)
85
+ if source_key.private?
86
+ source_key.public_key
87
+ else
88
+ source_key
89
+ end
90
+ else
91
+ nil
92
+ end
93
+ end
94
+ end
95
+
96
+ def augment_new_json(json)
97
+ if new_public_key
98
+ json['public_key'] = new_public_key.to_pem
99
+ end
100
+ json
101
+ end
102
+
103
+ def load_current_resource
104
+ begin
105
+ json = rest.get("#{actor_type}s/#{new_resource.name}")
106
+ @current_resource = json_to_resource(json)
107
+ rescue Net::HTTPServerException => e
108
+ if e.response.code == "404"
109
+ @current_resource = not_found_resource
110
+ else
111
+ raise
112
+ end
113
+ end
114
+
115
+ if new_resource.output_key_path && ::File.exist?(new_resource.output_key_path)
116
+ current_resource.output_key_path = new_resource.output_key_path
117
+ end
118
+ end
119
+
120
+ end