chef-zero 4.2.3 → 4.3.0

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