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