chef-zero 4.2.3 → 4.3.0

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