chef-zero 4.3.0 → 4.3.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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +201 -201
  3. data/README.md +155 -155
  4. data/Rakefile +31 -31
  5. data/bin/chef-zero +100 -100
  6. data/lib/chef_zero.rb +10 -10
  7. data/lib/chef_zero/chef_data/acl_path.rb +139 -139
  8. data/lib/chef_zero/chef_data/cookbook_data.rb +240 -240
  9. data/lib/chef_zero/chef_data/data_normalizer.rb +208 -207
  10. data/lib/chef_zero/chef_data/default_creator.rb +446 -446
  11. data/lib/chef_zero/data_store/data_already_exists_error.rb +29 -29
  12. data/lib/chef_zero/data_store/data_error.rb +31 -31
  13. data/lib/chef_zero/data_store/data_not_found_error.rb +28 -28
  14. data/lib/chef_zero/data_store/default_facade.rb +149 -149
  15. data/lib/chef_zero/data_store/interface_v1.rb +67 -67
  16. data/lib/chef_zero/data_store/interface_v2.rb +18 -18
  17. data/lib/chef_zero/data_store/memory_store.rb +33 -33
  18. data/lib/chef_zero/data_store/memory_store_v2.rb +155 -155
  19. data/lib/chef_zero/data_store/raw_file_store.rb +147 -147
  20. data/lib/chef_zero/data_store/v1_to_v2_adapter.rb +142 -142
  21. data/lib/chef_zero/data_store/v2_to_v1_adapter.rb +107 -107
  22. data/lib/chef_zero/endpoints/acl_endpoint.rb +38 -38
  23. data/lib/chef_zero/endpoints/acls_endpoint.rb +29 -29
  24. data/lib/chef_zero/endpoints/actor_endpoint.rb +94 -94
  25. data/lib/chef_zero/endpoints/actors_endpoint.rb +64 -64
  26. data/lib/chef_zero/endpoints/authenticate_user_endpoint.rb +31 -31
  27. data/lib/chef_zero/endpoints/container_endpoint.rb +22 -22
  28. data/lib/chef_zero/endpoints/containers_endpoint.rb +13 -13
  29. data/lib/chef_zero/endpoints/cookbook_endpoint.rb +39 -39
  30. data/lib/chef_zero/endpoints/cookbook_version_endpoint.rb +119 -119
  31. data/lib/chef_zero/endpoints/cookbooks_base.rb +65 -65
  32. data/lib/chef_zero/endpoints/cookbooks_endpoint.rb +19 -19
  33. data/lib/chef_zero/endpoints/data_bag_endpoint.rb +45 -45
  34. data/lib/chef_zero/endpoints/data_bag_item_endpoint.rb +25 -25
  35. data/lib/chef_zero/endpoints/data_bags_endpoint.rb +23 -23
  36. data/lib/chef_zero/endpoints/environment_cookbook_endpoint.rb +24 -24
  37. data/lib/chef_zero/endpoints/environment_cookbook_versions_endpoint.rb +123 -123
  38. data/lib/chef_zero/endpoints/environment_cookbooks_endpoint.rb +22 -22
  39. data/lib/chef_zero/endpoints/environment_endpoint.rb +33 -33
  40. data/lib/chef_zero/endpoints/environment_nodes_endpoint.rb +23 -23
  41. data/lib/chef_zero/endpoints/environment_recipes_endpoint.rb +22 -22
  42. data/lib/chef_zero/endpoints/environment_role_endpoint.rb +36 -36
  43. data/lib/chef_zero/endpoints/file_store_file_endpoint.rb +22 -22
  44. data/lib/chef_zero/endpoints/group_endpoint.rb +20 -20
  45. data/lib/chef_zero/endpoints/groups_endpoint.rb +13 -13
  46. data/lib/chef_zero/endpoints/license_endpoint.rb +25 -25
  47. data/lib/chef_zero/endpoints/node_endpoint.rb +17 -17
  48. data/lib/chef_zero/endpoints/node_identifiers_endpoint.rb +22 -22
  49. data/lib/chef_zero/endpoints/not_found_endpoint.rb +11 -11
  50. data/lib/chef_zero/endpoints/organization_association_request_endpoint.rb +22 -22
  51. data/lib/chef_zero/endpoints/organization_association_requests_endpoint.rb +30 -30
  52. data/lib/chef_zero/endpoints/organization_authenticate_user_endpoint.rb +26 -26
  53. data/lib/chef_zero/endpoints/organization_endpoint.rb +46 -46
  54. data/lib/chef_zero/endpoints/organization_user_base.rb +15 -15
  55. data/lib/chef_zero/endpoints/organization_user_endpoint.rb +26 -26
  56. data/lib/chef_zero/endpoints/organization_users_endpoint.rb +43 -43
  57. data/lib/chef_zero/endpoints/organization_validator_key_endpoint.rb +20 -20
  58. data/lib/chef_zero/endpoints/organizations_endpoint.rb +62 -62
  59. data/lib/chef_zero/endpoints/policies_endpoint.rb +151 -151
  60. data/lib/chef_zero/endpoints/principal_endpoint.rb +42 -42
  61. data/lib/chef_zero/endpoints/rest_list_endpoint.rb +42 -42
  62. data/lib/chef_zero/endpoints/rest_object_endpoint.rb +63 -63
  63. data/lib/chef_zero/endpoints/role_endpoint.rb +16 -16
  64. data/lib/chef_zero/endpoints/role_environments_endpoint.rb +14 -14
  65. data/lib/chef_zero/endpoints/sandbox_endpoint.rb +27 -27
  66. data/lib/chef_zero/endpoints/sandboxes_endpoint.rb +50 -50
  67. data/lib/chef_zero/endpoints/search_endpoint.rb +194 -194
  68. data/lib/chef_zero/endpoints/searches_endpoint.rb +18 -18
  69. data/lib/chef_zero/endpoints/server_api_version_endpoint.rb +14 -14
  70. data/lib/chef_zero/endpoints/system_recovery_endpoint.rb +30 -30
  71. data/lib/chef_zero/endpoints/user_association_request_endpoint.rb +40 -40
  72. data/lib/chef_zero/endpoints/user_association_requests_count_endpoint.rb +19 -19
  73. data/lib/chef_zero/endpoints/user_association_requests_endpoint.rb +19 -19
  74. data/lib/chef_zero/endpoints/user_organizations_endpoint.rb +22 -22
  75. data/lib/chef_zero/endpoints/version_endpoint.rb +12 -12
  76. data/lib/chef_zero/log.rb +7 -7
  77. data/lib/chef_zero/rest_base.rb +242 -242
  78. data/lib/chef_zero/rest_error_response.rb +11 -11
  79. data/lib/chef_zero/rest_request.rb +69 -69
  80. data/lib/chef_zero/rest_router.rb +45 -45
  81. data/lib/chef_zero/rspec.rb +308 -308
  82. data/lib/chef_zero/server.rb +642 -642
  83. data/lib/chef_zero/socketless_server_map.rb +92 -92
  84. data/lib/chef_zero/solr/query/binary_operator.rb +52 -52
  85. data/lib/chef_zero/solr/query/phrase.rb +23 -23
  86. data/lib/chef_zero/solr/query/range_query.rb +46 -46
  87. data/lib/chef_zero/solr/query/regexpable_query.rb +29 -29
  88. data/lib/chef_zero/solr/query/subquery.rb +37 -37
  89. data/lib/chef_zero/solr/query/term.rb +45 -45
  90. data/lib/chef_zero/solr/query/unary_operator.rb +43 -43
  91. data/lib/chef_zero/solr/solr_doc.rb +53 -53
  92. data/lib/chef_zero/solr/solr_parser.rb +203 -203
  93. data/lib/chef_zero/version.rb +3 -3
  94. data/spec/run_oc_pedant.rb +63 -63
  95. data/spec/search_spec.rb +32 -32
  96. data/spec/server_spec.rb +92 -92
  97. data/spec/socketless_server_map_spec.rb +76 -76
  98. data/spec/support/oc_pedant.rb +132 -132
  99. data/spec/support/stickywicket.pem +27 -27
  100. metadata +3 -3
@@ -1,642 +1,642 @@
1
- #
2
- # Author:: John Keiser (<jkeiser@opscode.com>)
3
- # Copyright:: Copyright (c) 2012 Opscode, Inc.
4
- # License:: Apache License, Version 2.0
5
- #
6
- # Licensed under the Apache License, Version 2.0 (the "License");
7
- # you may not use this file except in compliance with the License.
8
- # You may obtain a copy of the License at
9
- #
10
- # http://www.apache.org/licenses/LICENSE-2.0
11
- #
12
- # Unless required by applicable law or agreed to in writing, software
13
- # distributed under the License is distributed on an "AS IS" BASIS,
14
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
- # See the License for the specific language governing permissions and
16
- # limitations under the License.
17
- #
18
-
19
- require 'openssl'
20
- require 'open-uri'
21
- require 'rubygems'
22
- require 'timeout'
23
- require 'stringio'
24
-
25
- require 'rack'
26
- require 'webrick'
27
- require 'webrick/https'
28
-
29
- require 'chef_zero'
30
- require 'chef_zero/socketless_server_map'
31
- require 'chef_zero/chef_data/cookbook_data'
32
- require 'chef_zero/chef_data/acl_path'
33
- require 'chef_zero/rest_router'
34
- require 'chef_zero/data_store/memory_store_v2'
35
- require 'chef_zero/data_store/v1_to_v2_adapter'
36
- require 'chef_zero/data_store/default_facade'
37
- require 'chef_zero/version'
38
-
39
- require 'chef_zero/endpoints/rest_list_endpoint'
40
- require 'chef_zero/endpoints/authenticate_user_endpoint'
41
- require 'chef_zero/endpoints/acls_endpoint'
42
- require 'chef_zero/endpoints/acl_endpoint'
43
- require 'chef_zero/endpoints/actors_endpoint'
44
- require 'chef_zero/endpoints/actor_endpoint'
45
- require 'chef_zero/endpoints/cookbooks_endpoint'
46
- require 'chef_zero/endpoints/cookbook_endpoint'
47
- require 'chef_zero/endpoints/cookbook_version_endpoint'
48
- require 'chef_zero/endpoints/containers_endpoint'
49
- require 'chef_zero/endpoints/container_endpoint'
50
- require 'chef_zero/endpoints/data_bags_endpoint'
51
- require 'chef_zero/endpoints/data_bag_endpoint'
52
- require 'chef_zero/endpoints/data_bag_item_endpoint'
53
- require 'chef_zero/endpoints/groups_endpoint'
54
- require 'chef_zero/endpoints/group_endpoint'
55
- require 'chef_zero/endpoints/environment_endpoint'
56
- require 'chef_zero/endpoints/environment_cookbooks_endpoint'
57
- require 'chef_zero/endpoints/environment_cookbook_endpoint'
58
- require 'chef_zero/endpoints/environment_cookbook_versions_endpoint'
59
- require 'chef_zero/endpoints/environment_nodes_endpoint'
60
- require 'chef_zero/endpoints/environment_recipes_endpoint'
61
- require 'chef_zero/endpoints/environment_role_endpoint'
62
- require 'chef_zero/endpoints/license_endpoint'
63
- require 'chef_zero/endpoints/node_endpoint'
64
- require 'chef_zero/endpoints/node_identifiers_endpoint'
65
- require 'chef_zero/endpoints/organizations_endpoint'
66
- require 'chef_zero/endpoints/organization_endpoint'
67
- require 'chef_zero/endpoints/organization_association_requests_endpoint'
68
- require 'chef_zero/endpoints/organization_association_request_endpoint'
69
- require 'chef_zero/endpoints/organization_authenticate_user_endpoint'
70
- require 'chef_zero/endpoints/organization_users_endpoint'
71
- require 'chef_zero/endpoints/organization_user_endpoint'
72
- require 'chef_zero/endpoints/organization_validator_key_endpoint'
73
- require 'chef_zero/endpoints/principal_endpoint'
74
- require 'chef_zero/endpoints/policies_endpoint'
75
- require 'chef_zero/endpoints/role_endpoint'
76
- require 'chef_zero/endpoints/role_environments_endpoint'
77
- require 'chef_zero/endpoints/sandboxes_endpoint'
78
- require 'chef_zero/endpoints/sandbox_endpoint'
79
- require 'chef_zero/endpoints/searches_endpoint'
80
- require 'chef_zero/endpoints/search_endpoint'
81
- require 'chef_zero/endpoints/system_recovery_endpoint'
82
- require 'chef_zero/endpoints/user_association_requests_endpoint'
83
- require 'chef_zero/endpoints/user_association_requests_count_endpoint'
84
- require 'chef_zero/endpoints/user_association_request_endpoint'
85
- require 'chef_zero/endpoints/user_organizations_endpoint'
86
- require 'chef_zero/endpoints/file_store_file_endpoint'
87
- require 'chef_zero/endpoints/not_found_endpoint'
88
- require 'chef_zero/endpoints/version_endpoint'
89
- require 'chef_zero/endpoints/server_api_version_endpoint'
90
-
91
- module ChefZero
92
- class Server
93
-
94
- DEFAULT_OPTIONS = {
95
- :host => '127.0.0.1',
96
- :port => 8889,
97
- :log_level => :info,
98
- :generate_real_keys => true,
99
- :single_org => 'chef',
100
- :ssl => false
101
- }.freeze
102
-
103
- GLOBAL_ENDPOINTS = [
104
- '/license',
105
- '/version',
106
- '/server_api_version'
107
- ]
108
-
109
- def initialize(options = {})
110
- @options = DEFAULT_OPTIONS.merge(options)
111
- if @options[:single_org] && !@options.has_key?(:osc_compat)
112
- @options[:osc_compat] = true
113
- end
114
- @options.freeze
115
- ChefZero::Log.level = @options[:log_level].to_sym
116
- @app = nil
117
- end
118
-
119
- # @return [Hash]
120
- attr_reader :options
121
-
122
- # @return [Integer]
123
- def port
124
- if @port
125
- @port
126
- elsif !options[:port].respond_to?(:each)
127
- options[:port]
128
- else
129
- raise "port cannot be determined until server is started"
130
- end
131
- end
132
-
133
- # @return [WEBrick::HTTPServer]
134
- attr_reader :server
135
-
136
- include ChefZero::Endpoints
137
-
138
- #
139
- # The URL for this Chef Zero server. If the given host is an IPV6 address,
140
- # it is escaped in brackets according to RFC-2732.
141
- #
142
- # @see http://www.ietf.org/rfc/rfc2732.txt RFC-2732
143
- #
144
- # @return [String]
145
- #
146
- def url
147
- sch = @options[:ssl] ? 'https' : 'http'
148
- @url ||= if @options[:host].include?(':')
149
- URI("#{sch}://[#{@options[:host]}]:#{port}").to_s
150
- else
151
- URI("#{sch}://#{@options[:host]}:#{port}").to_s
152
- end
153
- end
154
-
155
- def local_mode_url
156
- raise "Port not yet set, cannot generate URL" unless port.kind_of?(Integer)
157
- "chefzero://localhost:#{port}"
158
- end
159
-
160
-
161
- #
162
- # The data store for this server (default is in-memory).
163
- #
164
- # @return [ChefZero::DataStore]
165
- #
166
- def data_store
167
- @data_store ||= begin
168
- result = @options[:data_store] || DataStore::DefaultFacade.new(DataStore::MemoryStoreV2.new, options[:single_org], options[:osc_compat])
169
- if options[:single_org]
170
-
171
- if !result.respond_to?(:interface_version) || result.interface_version == 1
172
- result = ChefZero::DataStore::V1ToV2Adapter.new(result, options[:single_org])
173
- result = ChefZero::DataStore::DefaultFacade.new(result, options[:single_org], options[:osc_compat])
174
- end
175
-
176
- else
177
- if !result.respond_to?(:interface_version) || result.interface_version == 1
178
- raise "Multi-org not supported by data store #{result}!"
179
- end
180
- end
181
-
182
- result
183
- end
184
- end
185
-
186
- #
187
- # Boolean method to determine if real Public/Private keys should be
188
- # generated.
189
- #
190
- # @return [Boolean]
191
- # true if real keys should be created, false otherwise
192
- #
193
- def generate_real_keys?
194
- !!@options[:generate_real_keys]
195
- end
196
-
197
- #
198
- # Start a Chef Zero server in the current thread. You can stop this server
199
- # by canceling the current thread.
200
- #
201
- # @param [Boolean|IO] publish
202
- # publish the server information to the publish parameter or to STDOUT if it's "true"
203
- #
204
- # @return [nil]
205
- # this method will block the main thread until interrupted
206
- #
207
- def start(publish = true)
208
- publish = publish[:publish] if publish.is_a?(Hash) # Legacy API
209
-
210
- if publish
211
- output = publish.respond_to?(:puts) ? publish : STDOUT
212
- output.puts <<-EOH.gsub(/^ {10}/, '')
213
- >> Starting Chef Zero (v#{ChefZero::VERSION})...
214
- EOH
215
- end
216
-
217
- thread = start_background
218
-
219
- if publish
220
- output = publish.respond_to?(:puts) ? publish : STDOUT
221
- output.puts <<-EOH.gsub(/^ {10}/, '')
222
- >> WEBrick (v#{WEBrick::VERSION}) on Rack (v#{Rack.release}) is listening at #{url}
223
- >> Press CTRL+C to stop
224
-
225
- EOH
226
- end
227
-
228
- %w[INT TERM].each do |signal|
229
- Signal.trap(signal) do
230
- puts "\n>> Stopping Chef Zero..."
231
- @server.shutdown
232
- end
233
- end
234
-
235
- # Move the background process to the main thread
236
- thread.join
237
- end
238
-
239
- #
240
- # Start a Chef Zero server in a forked process. This method returns the PID
241
- # to the forked process.
242
- #
243
- # @param [Fixnum] wait
244
- # the number of seconds to wait for the server to start
245
- #
246
- # @return [Thread]
247
- # the thread the background process is running in
248
- #
249
- def start_background(wait = 5)
250
- @server = WEBrick::HTTPServer.new(
251
- :DoNotListen => true,
252
- :AccessLog => [],
253
- :Logger => WEBrick::Log.new(StringIO.new, 7),
254
- :SSLEnable => options[:ssl],
255
- :SSLCertName => [ [ 'CN', WEBrick::Utils::getservername ] ],
256
- :StartCallback => proc {
257
- @running = true
258
- }
259
- )
260
- ENV['HTTPS'] = 'on' if options[:ssl]
261
- @server.mount('/', Rack::Handler::WEBrick, app)
262
-
263
- # Pick a port
264
- if options[:port].respond_to?(:each)
265
- options[:port].each do |port|
266
- begin
267
- @server.listen(options[:host], port)
268
- @port = port
269
- break
270
- rescue Errno::EADDRINUSE
271
- ChefZero::Log.info("Port #{port} in use: #{$!}")
272
- end
273
- end
274
- if !@port
275
- raise Errno::EADDRINUSE, "No port in :port range #{options[:port]} is available"
276
- end
277
- else
278
- @server.listen(options[:host], options[:port])
279
- @port = options[:port]
280
- end
281
-
282
- # Start the server in the background
283
- @thread = Thread.new do
284
- begin
285
- Thread.current.abort_on_exception = true
286
- @server.start
287
- ensure
288
- @port = nil
289
- @running = false
290
- end
291
- end
292
-
293
- # Do not return until the web server is genuinely started.
294
- while !@running && @thread.alive?
295
- sleep(0.01)
296
- end
297
-
298
- SocketlessServerMap.instance.register_port(@port, self)
299
-
300
- @thread
301
- end
302
-
303
- def start_socketless
304
- @port = SocketlessServerMap.instance.register_no_listen_server(self)
305
- end
306
-
307
- def handle_socketless_request(request_env)
308
- app.call(request_env)
309
- end
310
-
311
- #
312
- # Boolean method to determine if the server is currently ready to accept
313
- # requests. This method will attempt to make an HTTP request against the
314
- # server. If this method returns true, you are safe to make a request.
315
- #
316
- # @return [Boolean]
317
- # true if the server is accepting requests, false otherwise
318
- #
319
- def running?
320
- !@server.nil? && @running && @server.status == :Running
321
- end
322
-
323
- #
324
- # Gracefully stop the Chef Zero server.
325
- #
326
- # @param [Fixnum] wait
327
- # the number of seconds to wait before raising force-terminating the
328
- # server
329
- #
330
- def stop(wait = 5)
331
- if @running
332
- @server.shutdown if @server
333
- @thread.join(wait) if @thread
334
- end
335
- rescue Timeout::Error
336
- if @thread
337
- ChefZero::Log.error("Chef Zero did not stop within #{wait} seconds! Killing...")
338
- @thread.kill
339
- SocketlessServerMap.deregister(port)
340
- end
341
- ensure
342
- @server = nil
343
- @thread = nil
344
- end
345
-
346
- def gen_key_pair
347
- if generate_real_keys?
348
- private_key = OpenSSL::PKey::RSA.new(2048)
349
- public_key = private_key.public_key.to_s
350
- public_key.sub!(/^-----BEGIN RSA PUBLIC KEY-----/, '-----BEGIN PUBLIC KEY-----')
351
- public_key.sub!(/-----END RSA PUBLIC KEY-----(\s+)$/, '-----END PUBLIC KEY-----\1')
352
- [private_key.to_s, public_key]
353
- else
354
- [PRIVATE_KEY, PUBLIC_KEY]
355
- end
356
- end
357
-
358
- def on_request(&block)
359
- @on_request_proc = block
360
- end
361
-
362
- def on_response(&block)
363
- @on_response_proc = block
364
- end
365
-
366
- # Load data in a nice, friendly form:
367
- # {
368
- # 'roles' => {
369
- # 'desert' => '{ "description": "Hot and dry"' },
370
- # 'rainforest' => { "description" => 'Wet and humid' }
371
- # },
372
- # 'cookbooks' => {
373
- # 'apache2-1.0.1' => {
374
- # 'templates' => { 'default' => { 'blah.txt' => 'hi' }}
375
- # 'recipes' => { 'default.rb' => 'template "blah.txt"' }
376
- # 'metadata.rb' => 'depends "mysql"'
377
- # },
378
- # 'apache2-1.2.0' => {
379
- # 'templates' => { 'default' => { 'blah.txt' => 'lo' }}
380
- # 'recipes' => { 'default.rb' => 'template "blah.txt"' }
381
- # 'metadata.rb' => 'depends "mysql"'
382
- # },
383
- # 'mysql' => {
384
- # 'recipes' => { 'default.rb' => 'file { contents "hi" }' },
385
- # 'metadata.rb' => 'version "1.0.0"'
386
- # }
387
- # }
388
- # }
389
- def load_data(contents, org_name = nil)
390
- org_name ||= options[:single_org]
391
- if org_name.nil? && contents.keys != [ 'users' ]
392
- raise "Must pass an org name to load_data or run in single_org mode"
393
- end
394
-
395
- %w(clients containers environments groups nodes roles sandboxes).each do |data_type|
396
- if contents[data_type]
397
- dejsonize_children(contents[data_type]).each_pair do |name, data|
398
- data_store.set(['organizations', org_name, data_type, name], data, :create)
399
- end
400
- end
401
- end
402
-
403
- if contents['users']
404
- dejsonize_children(contents['users']).each_pair do |name, data|
405
- if options[:osc_compat]
406
- data_store.set(['organizations', org_name, 'users', name], data, :create)
407
- else
408
- # Create the user and put them in the org
409
- data_store.set(['users', name], data, :create)
410
- if org_name
411
- data_store.set(['organizations', org_name, 'users', name], '{}', :create)
412
- end
413
- end
414
- end
415
- end
416
-
417
- if contents['members']
418
- contents['members'].each do |name|
419
- data_store.set(['organizations', org_name, 'users', name], '{}', :create)
420
- end
421
- end
422
-
423
- if contents['invites']
424
- contents['invites'].each do |name|
425
- data_store.set(['organizations', org_name, 'association_requests', name], '{}', :create)
426
- end
427
- end
428
-
429
- if contents['acls']
430
- dejsonize_children(contents['acls']).each do |path, acl|
431
- path = [ 'organizations', org_name ] + path.split('/')
432
- path = ChefData::AclPath.get_acl_data_path(path)
433
- ChefZero::RSpec.server.data_store.set(path, acl)
434
- end
435
- end
436
-
437
- if contents['data']
438
- contents['data'].each_pair do |key, data_bag|
439
- data_store.create_dir(['organizations', org_name, 'data'], key, :recursive)
440
- dejsonize_children(data_bag).each do |item_name, item|
441
- data_store.set(['organizations', org_name, 'data', key, item_name], item, :create)
442
- end
443
- end
444
- end
445
-
446
- if contents['cookbooks']
447
- contents['cookbooks'].each_pair do |name_version, cookbook|
448
- if name_version =~ /(.+)-(\d+\.\d+\.\d+)$/
449
- cookbook_data = ChefData::CookbookData.to_hash(cookbook, $1, $2)
450
- else
451
- cookbook_data = ChefData::CookbookData.to_hash(cookbook, name_version)
452
- end
453
- raise "No version specified" if !cookbook_data[:version]
454
- data_store.create_dir(['organizations', org_name, 'cookbooks'], cookbook_data[:cookbook_name], :recursive)
455
- data_store.set(['organizations', org_name, 'cookbooks', cookbook_data[:cookbook_name], cookbook_data[:version]], FFI_Yajl::Encoder.encode(cookbook_data, :pretty => true), :create)
456
- cookbook_data.values.each do |files|
457
- next unless files.is_a? Array
458
- files.each do |file|
459
- data_store.set(['organizations', org_name, 'file_store', 'checksums', file[:checksum]], get_file(cookbook, file[:path]), :create)
460
- end
461
- end
462
- end
463
- end
464
- end
465
-
466
- def clear_data
467
- data_store.clear
468
- end
469
-
470
- def request_handler(&block)
471
- @request_handler = block
472
- end
473
-
474
- def to_s
475
- "#<#{self.class} #{url}>"
476
- end
477
-
478
- def inspect
479
- "#<#{self.class} @url=#{url.inspect}>"
480
- end
481
-
482
- private
483
-
484
- def open_source_endpoints
485
- result = if options[:osc_compat]
486
- # OSC-only
487
- [
488
- [ "/organizations/*/users", ActorsEndpoint.new(self) ],
489
- [ "/organizations/*/users/*", ActorEndpoint.new(self) ],
490
- [ "/organizations/*/authenticate_user", OrganizationAuthenticateUserEndpoint.new(self) ],
491
- ]
492
- else
493
- # EC-only
494
- [
495
- [ "/organizations/*/users", OrganizationUsersEndpoint.new(self) ],
496
- [ "/organizations/*/users/*", OrganizationUserEndpoint.new(self) ],
497
- [ "/users", ActorsEndpoint.new(self, 'username') ],
498
- [ "/users/*", ActorEndpoint.new(self, 'username') ],
499
- [ "/users/*/_acl", AclsEndpoint.new(self) ],
500
- [ "/users/*/_acl/*", AclEndpoint.new(self) ],
501
- [ "/users/*/association_requests", UserAssociationRequestsEndpoint.new(self) ],
502
- [ "/users/*/association_requests/count", UserAssociationRequestsCountEndpoint.new(self) ],
503
- [ "/users/*/association_requests/*", UserAssociationRequestEndpoint.new(self) ],
504
- [ "/users/*/organizations", UserOrganizationsEndpoint.new(self) ],
505
- [ "/authenticate_user", AuthenticateUserEndpoint.new(self) ],
506
- [ "/system_recovery", SystemRecoveryEndpoint.new(self) ],
507
- [ "/license", LicenseEndpoint.new(self) ],
508
-
509
- [ "/organizations", OrganizationsEndpoint.new(self) ],
510
- [ "/organizations/*", OrganizationEndpoint.new(self) ],
511
- [ "/organizations/*/_validator_key", OrganizationValidatorKeyEndpoint.new(self) ],
512
- [ "/organizations/*/association_requests", OrganizationAssociationRequestsEndpoint.new(self) ],
513
- [ "/organizations/*/association_requests/*", OrganizationAssociationRequestEndpoint.new(self) ],
514
- [ "/organizations/*/containers", ContainersEndpoint.new(self) ],
515
- [ "/organizations/*/containers/*", ContainerEndpoint.new(self) ],
516
- [ "/organizations/*/groups", GroupsEndpoint.new(self) ],
517
- [ "/organizations/*/groups/*", GroupEndpoint.new(self) ],
518
- [ "/organizations/*/organization/_acl", AclsEndpoint.new(self) ],
519
- [ "/organizations/*/organizations/_acl", AclsEndpoint.new(self) ],
520
- [ "/organizations/*/*/*/_acl", AclsEndpoint.new(self) ],
521
- [ "/organizations/*/organization/_acl/*", AclEndpoint.new(self) ],
522
- [ "/organizations/*/organizations/_acl/*", AclEndpoint.new(self) ],
523
- [ "/organizations/*/*/*/_acl/*", AclEndpoint.new(self) ]
524
- ]
525
- end
526
- result + [
527
- # Both
528
- [ "/organizations/*/clients", ActorsEndpoint.new(self) ],
529
- [ "/organizations/*/clients/*", ActorEndpoint.new(self) ],
530
- [ "/organizations/*/cookbooks", CookbooksEndpoint.new(self) ],
531
- [ "/organizations/*/cookbooks/*", CookbookEndpoint.new(self) ],
532
- [ "/organizations/*/cookbooks/*/*", CookbookVersionEndpoint.new(self) ],
533
- [ "/organizations/*/data", DataBagsEndpoint.new(self) ],
534
- [ "/organizations/*/data/*", DataBagEndpoint.new(self) ],
535
- [ "/organizations/*/data/*/*", DataBagItemEndpoint.new(self) ],
536
- [ "/organizations/*/environments", RestListEndpoint.new(self) ],
537
- [ "/organizations/*/environments/*", EnvironmentEndpoint.new(self) ],
538
- [ "/organizations/*/environments/*/cookbooks", EnvironmentCookbooksEndpoint.new(self) ],
539
- [ "/organizations/*/environments/*/cookbooks/*", EnvironmentCookbookEndpoint.new(self) ],
540
- [ "/organizations/*/environments/*/cookbook_versions", EnvironmentCookbookVersionsEndpoint.new(self) ],
541
- [ "/organizations/*/environments/*/nodes", EnvironmentNodesEndpoint.new(self) ],
542
- [ "/organizations/*/environments/*/recipes", EnvironmentRecipesEndpoint.new(self) ],
543
- [ "/organizations/*/environments/*/roles/*", EnvironmentRoleEndpoint.new(self) ],
544
- [ "/organizations/*/nodes", RestListEndpoint.new(self) ],
545
- [ "/organizations/*/nodes/*", NodeEndpoint.new(self) ],
546
- [ "/organizations/*/nodes/*/_identifiers", NodeIdentifiersEndpoint.new(self) ],
547
- [ "/organizations/*/policies/*/*", PoliciesEndpoint.new(self) ],
548
- [ "/organizations/*/principals/*", PrincipalEndpoint.new(self) ],
549
- [ "/organizations/*/roles", RestListEndpoint.new(self) ],
550
- [ "/organizations/*/roles/*", RoleEndpoint.new(self) ],
551
- [ "/organizations/*/roles/*/environments", RoleEnvironmentsEndpoint.new(self) ],
552
- [ "/organizations/*/roles/*/environments/*", EnvironmentRoleEndpoint.new(self) ],
553
- [ "/organizations/*/sandboxes", SandboxesEndpoint.new(self) ],
554
- [ "/organizations/*/sandboxes/*", SandboxEndpoint.new(self) ],
555
- [ "/organizations/*/search", SearchesEndpoint.new(self) ],
556
- [ "/organizations/*/search/*", SearchEndpoint.new(self) ],
557
- [ "/version", VersionEndpoint.new(self) ],
558
- [ "/server_api_version", ServerAPIVersionEndpoint.new(self) ],
559
-
560
- # Internal
561
- [ "/organizations/*/file_store/**", FileStoreFileEndpoint.new(self) ]
562
- ]
563
- end
564
-
565
- def global_endpoint?(ep)
566
- GLOBAL_ENDPOINTS.any? do |g_ep|
567
- ep.start_with?(g_ep)
568
- end
569
- end
570
-
571
- def app
572
- return @app if @app
573
- router = RestRouter.new(open_source_endpoints)
574
- router.not_found = NotFoundEndpoint.new
575
-
576
- if options[:single_org]
577
- rest_base_prefix = [ 'organizations', options[:single_org] ]
578
- else
579
- rest_base_prefix = []
580
- end
581
- @app = proc do |env|
582
- begin
583
- prefix = global_endpoint?(env['PATH_INFO']) ? [] : rest_base_prefix
584
-
585
- request = RestRequest.new(env, prefix)
586
- if @on_request_proc
587
- @on_request_proc.call(request)
588
- end
589
- response = nil
590
- if @request_handler
591
- response = @request_handler.call(request)
592
- end
593
- unless response
594
- response = router.call(request)
595
- end
596
- if @on_response_proc
597
- @on_response_proc.call(request, response)
598
- end
599
-
600
- # Insert Server header
601
- response[1]['Server'] = 'chef-zero'
602
-
603
- # Add CORS header
604
- response[1]['Access-Control-Allow-Origin'] = '*'
605
-
606
- # Puma expects the response to be an array (chunked responses). Since
607
- # we are statically generating data, we won't ever have said chunked
608
- # response, so fake it.
609
- response[-1] = Array(response[-1])
610
-
611
- response
612
- rescue
613
- if options[:log_level] == :debug
614
- STDERR.puts "Request Error: #{$!}"
615
- STDERR.puts $!.backtrace.join("\n")
616
- end
617
- end
618
- end
619
- @app
620
- end
621
-
622
- def dejsonize_children(hash)
623
- result = {}
624
- hash.each_pair do |key, value|
625
- result[key] = dejsonize(value)
626
- end
627
- result
628
- end
629
-
630
- def dejsonize(value)
631
- value.is_a?(Hash) ? FFI_Yajl::Encoder.encode(value, :pretty => true) : value
632
- end
633
-
634
- def get_file(directory, path)
635
- value = directory
636
- path.split('/').each do |part|
637
- value = value[part]
638
- end
639
- value
640
- end
641
- end
642
- end
1
+ #
2
+ # Author:: John Keiser (<jkeiser@opscode.com>)
3
+ # Copyright:: Copyright (c) 2012 Opscode, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'openssl'
20
+ require 'open-uri'
21
+ require 'rubygems'
22
+ require 'timeout'
23
+ require 'stringio'
24
+
25
+ require 'rack'
26
+ require 'webrick'
27
+ require 'webrick/https'
28
+
29
+ require 'chef_zero'
30
+ require 'chef_zero/socketless_server_map'
31
+ require 'chef_zero/chef_data/cookbook_data'
32
+ require 'chef_zero/chef_data/acl_path'
33
+ require 'chef_zero/rest_router'
34
+ require 'chef_zero/data_store/memory_store_v2'
35
+ require 'chef_zero/data_store/v1_to_v2_adapter'
36
+ require 'chef_zero/data_store/default_facade'
37
+ require 'chef_zero/version'
38
+
39
+ require 'chef_zero/endpoints/rest_list_endpoint'
40
+ require 'chef_zero/endpoints/authenticate_user_endpoint'
41
+ require 'chef_zero/endpoints/acls_endpoint'
42
+ require 'chef_zero/endpoints/acl_endpoint'
43
+ require 'chef_zero/endpoints/actors_endpoint'
44
+ require 'chef_zero/endpoints/actor_endpoint'
45
+ require 'chef_zero/endpoints/cookbooks_endpoint'
46
+ require 'chef_zero/endpoints/cookbook_endpoint'
47
+ require 'chef_zero/endpoints/cookbook_version_endpoint'
48
+ require 'chef_zero/endpoints/containers_endpoint'
49
+ require 'chef_zero/endpoints/container_endpoint'
50
+ require 'chef_zero/endpoints/data_bags_endpoint'
51
+ require 'chef_zero/endpoints/data_bag_endpoint'
52
+ require 'chef_zero/endpoints/data_bag_item_endpoint'
53
+ require 'chef_zero/endpoints/groups_endpoint'
54
+ require 'chef_zero/endpoints/group_endpoint'
55
+ require 'chef_zero/endpoints/environment_endpoint'
56
+ require 'chef_zero/endpoints/environment_cookbooks_endpoint'
57
+ require 'chef_zero/endpoints/environment_cookbook_endpoint'
58
+ require 'chef_zero/endpoints/environment_cookbook_versions_endpoint'
59
+ require 'chef_zero/endpoints/environment_nodes_endpoint'
60
+ require 'chef_zero/endpoints/environment_recipes_endpoint'
61
+ require 'chef_zero/endpoints/environment_role_endpoint'
62
+ require 'chef_zero/endpoints/license_endpoint'
63
+ require 'chef_zero/endpoints/node_endpoint'
64
+ require 'chef_zero/endpoints/node_identifiers_endpoint'
65
+ require 'chef_zero/endpoints/organizations_endpoint'
66
+ require 'chef_zero/endpoints/organization_endpoint'
67
+ require 'chef_zero/endpoints/organization_association_requests_endpoint'
68
+ require 'chef_zero/endpoints/organization_association_request_endpoint'
69
+ require 'chef_zero/endpoints/organization_authenticate_user_endpoint'
70
+ require 'chef_zero/endpoints/organization_users_endpoint'
71
+ require 'chef_zero/endpoints/organization_user_endpoint'
72
+ require 'chef_zero/endpoints/organization_validator_key_endpoint'
73
+ require 'chef_zero/endpoints/principal_endpoint'
74
+ require 'chef_zero/endpoints/policies_endpoint'
75
+ require 'chef_zero/endpoints/role_endpoint'
76
+ require 'chef_zero/endpoints/role_environments_endpoint'
77
+ require 'chef_zero/endpoints/sandboxes_endpoint'
78
+ require 'chef_zero/endpoints/sandbox_endpoint'
79
+ require 'chef_zero/endpoints/searches_endpoint'
80
+ require 'chef_zero/endpoints/search_endpoint'
81
+ require 'chef_zero/endpoints/system_recovery_endpoint'
82
+ require 'chef_zero/endpoints/user_association_requests_endpoint'
83
+ require 'chef_zero/endpoints/user_association_requests_count_endpoint'
84
+ require 'chef_zero/endpoints/user_association_request_endpoint'
85
+ require 'chef_zero/endpoints/user_organizations_endpoint'
86
+ require 'chef_zero/endpoints/file_store_file_endpoint'
87
+ require 'chef_zero/endpoints/not_found_endpoint'
88
+ require 'chef_zero/endpoints/version_endpoint'
89
+ require 'chef_zero/endpoints/server_api_version_endpoint'
90
+
91
+ module ChefZero
92
+ class Server
93
+
94
+ DEFAULT_OPTIONS = {
95
+ :host => '127.0.0.1',
96
+ :port => 8889,
97
+ :log_level => :info,
98
+ :generate_real_keys => true,
99
+ :single_org => 'chef',
100
+ :ssl => false
101
+ }.freeze
102
+
103
+ GLOBAL_ENDPOINTS = [
104
+ '/license',
105
+ '/version',
106
+ '/server_api_version'
107
+ ]
108
+
109
+ def initialize(options = {})
110
+ @options = DEFAULT_OPTIONS.merge(options)
111
+ if @options[:single_org] && !@options.has_key?(:osc_compat)
112
+ @options[:osc_compat] = true
113
+ end
114
+ @options.freeze
115
+ ChefZero::Log.level = @options[:log_level].to_sym
116
+ @app = nil
117
+ end
118
+
119
+ # @return [Hash]
120
+ attr_reader :options
121
+
122
+ # @return [Integer]
123
+ def port
124
+ if @port
125
+ @port
126
+ elsif !options[:port].respond_to?(:each)
127
+ options[:port]
128
+ else
129
+ raise "port cannot be determined until server is started"
130
+ end
131
+ end
132
+
133
+ # @return [WEBrick::HTTPServer]
134
+ attr_reader :server
135
+
136
+ include ChefZero::Endpoints
137
+
138
+ #
139
+ # The URL for this Chef Zero server. If the given host is an IPV6 address,
140
+ # it is escaped in brackets according to RFC-2732.
141
+ #
142
+ # @see http://www.ietf.org/rfc/rfc2732.txt RFC-2732
143
+ #
144
+ # @return [String]
145
+ #
146
+ def url
147
+ sch = @options[:ssl] ? 'https' : 'http'
148
+ @url ||= if @options[:host].include?(':')
149
+ URI("#{sch}://[#{@options[:host]}]:#{port}").to_s
150
+ else
151
+ URI("#{sch}://#{@options[:host]}:#{port}").to_s
152
+ end
153
+ end
154
+
155
+ def local_mode_url
156
+ raise "Port not yet set, cannot generate URL" unless port.kind_of?(Integer)
157
+ "chefzero://localhost:#{port}"
158
+ end
159
+
160
+
161
+ #
162
+ # The data store for this server (default is in-memory).
163
+ #
164
+ # @return [ChefZero::DataStore]
165
+ #
166
+ def data_store
167
+ @data_store ||= begin
168
+ result = @options[:data_store] || DataStore::DefaultFacade.new(DataStore::MemoryStoreV2.new, options[:single_org], options[:osc_compat])
169
+ if options[:single_org]
170
+
171
+ if !result.respond_to?(:interface_version) || result.interface_version == 1
172
+ result = ChefZero::DataStore::V1ToV2Adapter.new(result, options[:single_org])
173
+ result = ChefZero::DataStore::DefaultFacade.new(result, options[:single_org], options[:osc_compat])
174
+ end
175
+
176
+ else
177
+ if !result.respond_to?(:interface_version) || result.interface_version == 1
178
+ raise "Multi-org not supported by data store #{result}!"
179
+ end
180
+ end
181
+
182
+ result
183
+ end
184
+ end
185
+
186
+ #
187
+ # Boolean method to determine if real Public/Private keys should be
188
+ # generated.
189
+ #
190
+ # @return [Boolean]
191
+ # true if real keys should be created, false otherwise
192
+ #
193
+ def generate_real_keys?
194
+ !!@options[:generate_real_keys]
195
+ end
196
+
197
+ #
198
+ # Start a Chef Zero server in the current thread. You can stop this server
199
+ # by canceling the current thread.
200
+ #
201
+ # @param [Boolean|IO] publish
202
+ # publish the server information to the publish parameter or to STDOUT if it's "true"
203
+ #
204
+ # @return [nil]
205
+ # this method will block the main thread until interrupted
206
+ #
207
+ def start(publish = true)
208
+ publish = publish[:publish] if publish.is_a?(Hash) # Legacy API
209
+
210
+ if publish
211
+ output = publish.respond_to?(:puts) ? publish : STDOUT
212
+ output.puts <<-EOH.gsub(/^ {10}/, '')
213
+ >> Starting Chef Zero (v#{ChefZero::VERSION})...
214
+ EOH
215
+ end
216
+
217
+ thread = start_background
218
+
219
+ if publish
220
+ output = publish.respond_to?(:puts) ? publish : STDOUT
221
+ output.puts <<-EOH.gsub(/^ {10}/, '')
222
+ >> WEBrick (v#{WEBrick::VERSION}) on Rack (v#{Rack.release}) is listening at #{url}
223
+ >> Press CTRL+C to stop
224
+
225
+ EOH
226
+ end
227
+
228
+ %w[INT TERM].each do |signal|
229
+ Signal.trap(signal) do
230
+ puts "\n>> Stopping Chef Zero..."
231
+ @server.shutdown
232
+ end
233
+ end
234
+
235
+ # Move the background process to the main thread
236
+ thread.join
237
+ end
238
+
239
+ #
240
+ # Start a Chef Zero server in a forked process. This method returns the PID
241
+ # to the forked process.
242
+ #
243
+ # @param [Fixnum] wait
244
+ # the number of seconds to wait for the server to start
245
+ #
246
+ # @return [Thread]
247
+ # the thread the background process is running in
248
+ #
249
+ def start_background(wait = 5)
250
+ @server = WEBrick::HTTPServer.new(
251
+ :DoNotListen => true,
252
+ :AccessLog => [],
253
+ :Logger => WEBrick::Log.new(StringIO.new, 7),
254
+ :SSLEnable => options[:ssl],
255
+ :SSLCertName => [ [ 'CN', WEBrick::Utils::getservername ] ],
256
+ :StartCallback => proc {
257
+ @running = true
258
+ }
259
+ )
260
+ ENV['HTTPS'] = 'on' if options[:ssl]
261
+ @server.mount('/', Rack::Handler::WEBrick, app)
262
+
263
+ # Pick a port
264
+ if options[:port].respond_to?(:each)
265
+ options[:port].each do |port|
266
+ begin
267
+ @server.listen(options[:host], port)
268
+ @port = port
269
+ break
270
+ rescue Errno::EADDRINUSE
271
+ ChefZero::Log.info("Port #{port} in use: #{$!}")
272
+ end
273
+ end
274
+ if !@port
275
+ raise Errno::EADDRINUSE, "No port in :port range #{options[:port]} is available"
276
+ end
277
+ else
278
+ @server.listen(options[:host], options[:port])
279
+ @port = options[:port]
280
+ end
281
+
282
+ # Start the server in the background
283
+ @thread = Thread.new do
284
+ begin
285
+ Thread.current.abort_on_exception = true
286
+ @server.start
287
+ ensure
288
+ @port = nil
289
+ @running = false
290
+ end
291
+ end
292
+
293
+ # Do not return until the web server is genuinely started.
294
+ while !@running && @thread.alive?
295
+ sleep(0.01)
296
+ end
297
+
298
+ SocketlessServerMap.instance.register_port(@port, self)
299
+
300
+ @thread
301
+ end
302
+
303
+ def start_socketless
304
+ @port = SocketlessServerMap.instance.register_no_listen_server(self)
305
+ end
306
+
307
+ def handle_socketless_request(request_env)
308
+ app.call(request_env)
309
+ end
310
+
311
+ #
312
+ # Boolean method to determine if the server is currently ready to accept
313
+ # requests. This method will attempt to make an HTTP request against the
314
+ # server. If this method returns true, you are safe to make a request.
315
+ #
316
+ # @return [Boolean]
317
+ # true if the server is accepting requests, false otherwise
318
+ #
319
+ def running?
320
+ !@server.nil? && @running && @server.status == :Running
321
+ end
322
+
323
+ #
324
+ # Gracefully stop the Chef Zero server.
325
+ #
326
+ # @param [Fixnum] wait
327
+ # the number of seconds to wait before raising force-terminating the
328
+ # server
329
+ #
330
+ def stop(wait = 5)
331
+ if @running
332
+ @server.shutdown if @server
333
+ @thread.join(wait) if @thread
334
+ end
335
+ rescue Timeout::Error
336
+ if @thread
337
+ ChefZero::Log.error("Chef Zero did not stop within #{wait} seconds! Killing...")
338
+ @thread.kill
339
+ SocketlessServerMap.deregister(port)
340
+ end
341
+ ensure
342
+ @server = nil
343
+ @thread = nil
344
+ end
345
+
346
+ def gen_key_pair
347
+ if generate_real_keys?
348
+ private_key = OpenSSL::PKey::RSA.new(2048)
349
+ public_key = private_key.public_key.to_s
350
+ public_key.sub!(/^-----BEGIN RSA PUBLIC KEY-----/, '-----BEGIN PUBLIC KEY-----')
351
+ public_key.sub!(/-----END RSA PUBLIC KEY-----(\s+)$/, '-----END PUBLIC KEY-----\1')
352
+ [private_key.to_s, public_key]
353
+ else
354
+ [PRIVATE_KEY, PUBLIC_KEY]
355
+ end
356
+ end
357
+
358
+ def on_request(&block)
359
+ @on_request_proc = block
360
+ end
361
+
362
+ def on_response(&block)
363
+ @on_response_proc = block
364
+ end
365
+
366
+ # Load data in a nice, friendly form:
367
+ # {
368
+ # 'roles' => {
369
+ # 'desert' => '{ "description": "Hot and dry"' },
370
+ # 'rainforest' => { "description" => 'Wet and humid' }
371
+ # },
372
+ # 'cookbooks' => {
373
+ # 'apache2-1.0.1' => {
374
+ # 'templates' => { 'default' => { 'blah.txt' => 'hi' }}
375
+ # 'recipes' => { 'default.rb' => 'template "blah.txt"' }
376
+ # 'metadata.rb' => 'depends "mysql"'
377
+ # },
378
+ # 'apache2-1.2.0' => {
379
+ # 'templates' => { 'default' => { 'blah.txt' => 'lo' }}
380
+ # 'recipes' => { 'default.rb' => 'template "blah.txt"' }
381
+ # 'metadata.rb' => 'depends "mysql"'
382
+ # },
383
+ # 'mysql' => {
384
+ # 'recipes' => { 'default.rb' => 'file { contents "hi" }' },
385
+ # 'metadata.rb' => 'version "1.0.0"'
386
+ # }
387
+ # }
388
+ # }
389
+ def load_data(contents, org_name = nil)
390
+ org_name ||= options[:single_org]
391
+ if org_name.nil? && contents.keys != [ 'users' ]
392
+ raise "Must pass an org name to load_data or run in single_org mode"
393
+ end
394
+
395
+ %w(clients containers environments groups nodes roles sandboxes).each do |data_type|
396
+ if contents[data_type]
397
+ dejsonize_children(contents[data_type]).each_pair do |name, data|
398
+ data_store.set(['organizations', org_name, data_type, name], data, :create)
399
+ end
400
+ end
401
+ end
402
+
403
+ if contents['users']
404
+ dejsonize_children(contents['users']).each_pair do |name, data|
405
+ if options[:osc_compat]
406
+ data_store.set(['organizations', org_name, 'users', name], data, :create)
407
+ else
408
+ # Create the user and put them in the org
409
+ data_store.set(['users', name], data, :create)
410
+ if org_name
411
+ data_store.set(['organizations', org_name, 'users', name], '{}', :create)
412
+ end
413
+ end
414
+ end
415
+ end
416
+
417
+ if contents['members']
418
+ contents['members'].each do |name|
419
+ data_store.set(['organizations', org_name, 'users', name], '{}', :create)
420
+ end
421
+ end
422
+
423
+ if contents['invites']
424
+ contents['invites'].each do |name|
425
+ data_store.set(['organizations', org_name, 'association_requests', name], '{}', :create)
426
+ end
427
+ end
428
+
429
+ if contents['acls']
430
+ dejsonize_children(contents['acls']).each do |path, acl|
431
+ path = [ 'organizations', org_name ] + path.split('/')
432
+ path = ChefData::AclPath.get_acl_data_path(path)
433
+ ChefZero::RSpec.server.data_store.set(path, acl)
434
+ end
435
+ end
436
+
437
+ if contents['data']
438
+ contents['data'].each_pair do |key, data_bag|
439
+ data_store.create_dir(['organizations', org_name, 'data'], key, :recursive)
440
+ dejsonize_children(data_bag).each do |item_name, item|
441
+ data_store.set(['organizations', org_name, 'data', key, item_name], item, :create)
442
+ end
443
+ end
444
+ end
445
+
446
+ if contents['cookbooks']
447
+ contents['cookbooks'].each_pair do |name_version, cookbook|
448
+ if name_version =~ /(.+)-(\d+\.\d+\.\d+)$/
449
+ cookbook_data = ChefData::CookbookData.to_hash(cookbook, $1, $2)
450
+ else
451
+ cookbook_data = ChefData::CookbookData.to_hash(cookbook, name_version)
452
+ end
453
+ raise "No version specified" if !cookbook_data[:version]
454
+ data_store.create_dir(['organizations', org_name, 'cookbooks'], cookbook_data[:cookbook_name], :recursive)
455
+ data_store.set(['organizations', org_name, 'cookbooks', cookbook_data[:cookbook_name], cookbook_data[:version]], FFI_Yajl::Encoder.encode(cookbook_data, :pretty => true), :create)
456
+ cookbook_data.values.each do |files|
457
+ next unless files.is_a? Array
458
+ files.each do |file|
459
+ data_store.set(['organizations', org_name, 'file_store', 'checksums', file[:checksum]], get_file(cookbook, file[:path]), :create)
460
+ end
461
+ end
462
+ end
463
+ end
464
+ end
465
+
466
+ def clear_data
467
+ data_store.clear
468
+ end
469
+
470
+ def request_handler(&block)
471
+ @request_handler = block
472
+ end
473
+
474
+ def to_s
475
+ "#<#{self.class} #{url}>"
476
+ end
477
+
478
+ def inspect
479
+ "#<#{self.class} @url=#{url.inspect}>"
480
+ end
481
+
482
+ private
483
+
484
+ def open_source_endpoints
485
+ result = if options[:osc_compat]
486
+ # OSC-only
487
+ [
488
+ [ "/organizations/*/users", ActorsEndpoint.new(self) ],
489
+ [ "/organizations/*/users/*", ActorEndpoint.new(self) ],
490
+ [ "/organizations/*/authenticate_user", OrganizationAuthenticateUserEndpoint.new(self) ],
491
+ ]
492
+ else
493
+ # EC-only
494
+ [
495
+ [ "/organizations/*/users", OrganizationUsersEndpoint.new(self) ],
496
+ [ "/organizations/*/users/*", OrganizationUserEndpoint.new(self) ],
497
+ [ "/users", ActorsEndpoint.new(self, 'username') ],
498
+ [ "/users/*", ActorEndpoint.new(self, 'username') ],
499
+ [ "/users/*/_acl", AclsEndpoint.new(self) ],
500
+ [ "/users/*/_acl/*", AclEndpoint.new(self) ],
501
+ [ "/users/*/association_requests", UserAssociationRequestsEndpoint.new(self) ],
502
+ [ "/users/*/association_requests/count", UserAssociationRequestsCountEndpoint.new(self) ],
503
+ [ "/users/*/association_requests/*", UserAssociationRequestEndpoint.new(self) ],
504
+ [ "/users/*/organizations", UserOrganizationsEndpoint.new(self) ],
505
+ [ "/authenticate_user", AuthenticateUserEndpoint.new(self) ],
506
+ [ "/system_recovery", SystemRecoveryEndpoint.new(self) ],
507
+ [ "/license", LicenseEndpoint.new(self) ],
508
+
509
+ [ "/organizations", OrganizationsEndpoint.new(self) ],
510
+ [ "/organizations/*", OrganizationEndpoint.new(self) ],
511
+ [ "/organizations/*/_validator_key", OrganizationValidatorKeyEndpoint.new(self) ],
512
+ [ "/organizations/*/association_requests", OrganizationAssociationRequestsEndpoint.new(self) ],
513
+ [ "/organizations/*/association_requests/*", OrganizationAssociationRequestEndpoint.new(self) ],
514
+ [ "/organizations/*/containers", ContainersEndpoint.new(self) ],
515
+ [ "/organizations/*/containers/*", ContainerEndpoint.new(self) ],
516
+ [ "/organizations/*/groups", GroupsEndpoint.new(self) ],
517
+ [ "/organizations/*/groups/*", GroupEndpoint.new(self) ],
518
+ [ "/organizations/*/organization/_acl", AclsEndpoint.new(self) ],
519
+ [ "/organizations/*/organizations/_acl", AclsEndpoint.new(self) ],
520
+ [ "/organizations/*/*/*/_acl", AclsEndpoint.new(self) ],
521
+ [ "/organizations/*/organization/_acl/*", AclEndpoint.new(self) ],
522
+ [ "/organizations/*/organizations/_acl/*", AclEndpoint.new(self) ],
523
+ [ "/organizations/*/*/*/_acl/*", AclEndpoint.new(self) ]
524
+ ]
525
+ end
526
+ result + [
527
+ # Both
528
+ [ "/organizations/*/clients", ActorsEndpoint.new(self) ],
529
+ [ "/organizations/*/clients/*", ActorEndpoint.new(self) ],
530
+ [ "/organizations/*/cookbooks", CookbooksEndpoint.new(self) ],
531
+ [ "/organizations/*/cookbooks/*", CookbookEndpoint.new(self) ],
532
+ [ "/organizations/*/cookbooks/*/*", CookbookVersionEndpoint.new(self) ],
533
+ [ "/organizations/*/data", DataBagsEndpoint.new(self) ],
534
+ [ "/organizations/*/data/*", DataBagEndpoint.new(self) ],
535
+ [ "/organizations/*/data/*/*", DataBagItemEndpoint.new(self) ],
536
+ [ "/organizations/*/environments", RestListEndpoint.new(self) ],
537
+ [ "/organizations/*/environments/*", EnvironmentEndpoint.new(self) ],
538
+ [ "/organizations/*/environments/*/cookbooks", EnvironmentCookbooksEndpoint.new(self) ],
539
+ [ "/organizations/*/environments/*/cookbooks/*", EnvironmentCookbookEndpoint.new(self) ],
540
+ [ "/organizations/*/environments/*/cookbook_versions", EnvironmentCookbookVersionsEndpoint.new(self) ],
541
+ [ "/organizations/*/environments/*/nodes", EnvironmentNodesEndpoint.new(self) ],
542
+ [ "/organizations/*/environments/*/recipes", EnvironmentRecipesEndpoint.new(self) ],
543
+ [ "/organizations/*/environments/*/roles/*", EnvironmentRoleEndpoint.new(self) ],
544
+ [ "/organizations/*/nodes", RestListEndpoint.new(self) ],
545
+ [ "/organizations/*/nodes/*", NodeEndpoint.new(self) ],
546
+ [ "/organizations/*/nodes/*/_identifiers", NodeIdentifiersEndpoint.new(self) ],
547
+ [ "/organizations/*/policies/*/*", PoliciesEndpoint.new(self) ],
548
+ [ "/organizations/*/principals/*", PrincipalEndpoint.new(self) ],
549
+ [ "/organizations/*/roles", RestListEndpoint.new(self) ],
550
+ [ "/organizations/*/roles/*", RoleEndpoint.new(self) ],
551
+ [ "/organizations/*/roles/*/environments", RoleEnvironmentsEndpoint.new(self) ],
552
+ [ "/organizations/*/roles/*/environments/*", EnvironmentRoleEndpoint.new(self) ],
553
+ [ "/organizations/*/sandboxes", SandboxesEndpoint.new(self) ],
554
+ [ "/organizations/*/sandboxes/*", SandboxEndpoint.new(self) ],
555
+ [ "/organizations/*/search", SearchesEndpoint.new(self) ],
556
+ [ "/organizations/*/search/*", SearchEndpoint.new(self) ],
557
+ [ "/version", VersionEndpoint.new(self) ],
558
+ [ "/server_api_version", ServerAPIVersionEndpoint.new(self) ],
559
+
560
+ # Internal
561
+ [ "/organizations/*/file_store/**", FileStoreFileEndpoint.new(self) ]
562
+ ]
563
+ end
564
+
565
+ def global_endpoint?(ep)
566
+ GLOBAL_ENDPOINTS.any? do |g_ep|
567
+ ep.start_with?(g_ep)
568
+ end
569
+ end
570
+
571
+ def app
572
+ return @app if @app
573
+ router = RestRouter.new(open_source_endpoints)
574
+ router.not_found = NotFoundEndpoint.new
575
+
576
+ if options[:single_org]
577
+ rest_base_prefix = [ 'organizations', options[:single_org] ]
578
+ else
579
+ rest_base_prefix = []
580
+ end
581
+ @app = proc do |env|
582
+ begin
583
+ prefix = global_endpoint?(env['PATH_INFO']) ? [] : rest_base_prefix
584
+
585
+ request = RestRequest.new(env, prefix)
586
+ if @on_request_proc
587
+ @on_request_proc.call(request)
588
+ end
589
+ response = nil
590
+ if @request_handler
591
+ response = @request_handler.call(request)
592
+ end
593
+ unless response
594
+ response = router.call(request)
595
+ end
596
+ if @on_response_proc
597
+ @on_response_proc.call(request, response)
598
+ end
599
+
600
+ # Insert Server header
601
+ response[1]['Server'] = 'chef-zero'
602
+
603
+ # Add CORS header
604
+ response[1]['Access-Control-Allow-Origin'] = '*'
605
+
606
+ # Puma expects the response to be an array (chunked responses). Since
607
+ # we are statically generating data, we won't ever have said chunked
608
+ # response, so fake it.
609
+ response[-1] = Array(response[-1])
610
+
611
+ response
612
+ rescue
613
+ if options[:log_level] == :debug
614
+ STDERR.puts "Request Error: #{$!}"
615
+ STDERR.puts $!.backtrace.join("\n")
616
+ end
617
+ end
618
+ end
619
+ @app
620
+ end
621
+
622
+ def dejsonize_children(hash)
623
+ result = {}
624
+ hash.each_pair do |key, value|
625
+ result[key] = dejsonize(value)
626
+ end
627
+ result
628
+ end
629
+
630
+ def dejsonize(value)
631
+ value.is_a?(Hash) ? FFI_Yajl::Encoder.encode(value, :pretty => true) : value
632
+ end
633
+
634
+ def get_file(directory, path)
635
+ value = directory
636
+ path.split('/').each do |part|
637
+ value = value[part]
638
+ end
639
+ value
640
+ end
641
+ end
642
+ end