chef-zero 4.3.0 → 4.3.1

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