Class: ChefZero::Server

Inherits:
Object
  • Object
show all
Includes:
Endpoints
Defined in:
lib/chef_zero/server.rb

Constant Summary collapse

DEFAULT_OPTIONS =
{
  host: ["127.0.0.1"],
  port: 8889,
  log_level: :warn,
  generate_real_keys: true,
  single_org: "chef",
  ssl: false,
}.freeze
GLOBAL_ENDPOINTS =
[
  "/license",
  "/version",
  "/server_api_version",
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Server

Returns a new instance of Server.



131
132
133
134
135
136
137
138
139
# File 'lib/chef_zero/server.rb', line 131

def initialize(options = {})
  @options = DEFAULT_OPTIONS.merge(options)
  if @options[:single_org] && !@options.key?(:osc_compat)
    @options[:osc_compat] = true
  end
  @options.freeze
  ChefZero::Log.level = @options[:log_level].to_sym
  @app = nil
end

Instance Attribute Details

#optionsHash (readonly)

Returns:

  • (Hash)


142
143
144
# File 'lib/chef_zero/server.rb', line 142

def options
  @options
end

#serverWEBrick::HTTPServer (readonly)

Returns:

  • (WEBrick::HTTPServer)


157
158
159
# File 'lib/chef_zero/server.rb', line 157

def server
  @server
end

Instance Method Details

#clear_dataObject



522
523
524
# File 'lib/chef_zero/server.rb', line 522

def clear_data
  data_store.clear
end

#data_storeChefZero::DataStore

The data store for this server (default is in-memory).

Returns:



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/chef_zero/server.rb', line 190

def data_store
  @data_store ||= begin
    result = @options[:data_store] || DataStore::DefaultFacade.new(DataStore::MemoryStoreV2.new, options[:single_org], options[:osc_compat])
    if options[:single_org]

      if !result.respond_to?(:interface_version) || result.interface_version == 1
        result = ChefZero::DataStore::V1ToV2Adapter.new(result, options[:single_org])
        result = ChefZero::DataStore::DefaultFacade.new(result, options[:single_org], options[:osc_compat])
      end

    else
      if !result.respond_to?(:interface_version) || result.interface_version == 1
        raise "Multi-org not supported by data store #{result}!"
      end
    end

    result
  end
end

#gen_key_pairObject



377
378
379
380
381
382
383
384
385
386
387
# File 'lib/chef_zero/server.rb', line 377

def gen_key_pair
  if generate_real_keys?
    private_key = OpenSSL::PKey::RSA.new(2048)
    public_key = private_key.public_key.to_s
    public_key.sub!(/^-----BEGIN RSA PUBLIC KEY-----/, "-----BEGIN PUBLIC KEY-----")
    public_key.sub!(/-----END RSA PUBLIC KEY-----(\s+)$/, '-----END PUBLIC KEY-----\1')
    [private_key.to_s, public_key]
  else
    [PRIVATE_KEY, PUBLIC_KEY]
  end
end

#generate_real_keys?Boolean

Boolean method to determine if real Public/Private keys should be generated.

Returns:

  • (Boolean)

    true if real keys should be created, false otherwise



217
218
219
# File 'lib/chef_zero/server.rb', line 217

def generate_real_keys?
  !!@options[:generate_real_keys]
end

#handle_socketless_request(request_env) ⇒ Object



338
339
340
# File 'lib/chef_zero/server.rb', line 338

def handle_socketless_request(request_env)
  app.call(request_env)
end

#inspectObject



534
535
536
# File 'lib/chef_zero/server.rb', line 534

def inspect
  "#<#{self.class} @url=#{url.inspect}>"
end

#listen(hosts, port) ⇒ Thread

Start a Chef Zero server in a forked process. This method returns the PID to the forked process.

Parameters:

  • wait (Fixnum)

    the number of seconds to wait for the server to start

Returns:

  • (Thread)

    the thread the background process is running in



273
274
275
276
277
278
279
280
281
282
283
# File 'lib/chef_zero/server.rb', line 273

def listen(hosts, port)
  hosts.each do |host|
    @server.listen(host, port)
  end
  true
rescue Errno::EADDRINUSE
  ChefZero::Log.warn("Port #{port} not available")
  @server.listeners.each(&:close)
  @server.listeners.clear
  false
end

#load_data(contents, org_name = nil) ⇒ Object

Load data in a nice, friendly form: {

'roles' => {
  'desert' => '{ "description": "Hot and dry"' },
  'rainforest' => { "description" => 'Wet and humid' }
},
'cookbooks' => {
  'apache2-1.0.1' => {
    'templates' => { 'default' => { 'blah.txt' => 'hi' }}
    'recipes' => { 'default.rb' => 'template "blah.txt"' }
    'metadata.rb' => 'depends "mysql"'
  },
  'apache2-1.2.0' => {
    'templates' => { 'default' => { 'blah.txt' => 'lo' }}
    'recipes' => { 'default.rb' => 'template "blah.txt"' }
    'metadata.rb' => 'depends "mysql"'
  },
  'mysql' => {
    'recipes' => { 'default.rb' => 'file { contents "hi" }' },
    'metadata.rb' => 'version "1.0.0"'
  }
}

}



420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
# File 'lib/chef_zero/server.rb', line 420

def load_data(contents, org_name = nil)
  org_name ||= options[:single_org]
  if org_name.nil? && contents.keys != [ "users" ]
    raise "Must pass an org name to load_data or run in single_org mode"
  end

  %w{clients containers environments groups nodes roles sandboxes}.each do |data_type|
    if contents[data_type]
      dejsonize_children(contents[data_type]).each_pair do |name, data|
        data_store.set(["organizations", org_name, data_type, name], data, :create)
      end
    end
  end

  if contents["users"]
    dejsonize_children(contents["users"]).each_pair do |name, data|
      if options[:osc_compat]
        data_store.set(["organizations", org_name, "users", name], data, :create)
      else
        # Create the user and put them in the org
        data_store.set(["users", name], data, :create)
        if org_name
          data_store.set(["organizations", org_name, "users", name], "{}", :create)
        end
      end
    end
  end

  if contents["members"]
    contents["members"].each do |name|
      data_store.set(["organizations", org_name, "users", name], "{}", :create)
    end
  end

  if contents["invites"]
    contents["invites"].each do |name|
      data_store.set(["organizations", org_name, "association_requests", name], "{}", :create)
    end
  end

  if contents["acls"]
    dejsonize_children(contents["acls"]).each do |path, acl|
      path = [ "organizations", org_name ] + path.split("/")
      path = ChefData::AclPath.get_acl_data_path(path)
      ChefZero::RSpec.server.data_store.set(path, acl)
    end
  end

  if contents["data"]
    contents["data"].each_pair do |key, data_bag|
      data_store.create_dir(["organizations", org_name, "data"], key, :recursive)
      dejsonize_children(data_bag).each do |item_name, item|
        data_store.set(["organizations", org_name, "data", key, item_name], item, :create)
      end
    end
  end

  if contents["policies"]
    contents["policies"].each_pair do |policy_name, policy_struct|
      # data_store.create_dir(['organizations', org_name, 'policies', policy_name], "revisions", :recursive)
      dejsonize_children(policy_struct).each do |revision, policy_data|
        data_store.set(["organizations", org_name, "policies", policy_name,
                        "revisions", revision], policy_data, :create, :create_dir)
      end
    end
  end

  if contents["policy_groups"]
    contents["policy_groups"].each_pair do |group_name, group|
      group["policies"].each do |policy_name, policy_revision|
        data_store.set(["organizations", org_name, "policy_groups", group_name, "policies", policy_name], FFI_Yajl::Encoder.encode(policy_revision["revision_id"], pretty: true), :create, :create_dir)
      end
    end
  end

  %w{cookbooks cookbook_artifacts}.each do |cookbook_type|
    if contents[cookbook_type]
      contents[cookbook_type].each_pair do |name_version, cookbook|
        if cookbook_type == "cookbook_artifacts"
          name, _, identifier = name_version.rpartition("-")
          cookbook_data = ChefData::CookbookData.to_hash(cookbook, name, identifier)
        elsif name_version =~ /(.+)-(\d+\.\d+\.\d+)$/
          cookbook_data = ChefData::CookbookData.to_hash(cookbook, $1, $2)
        else
          cookbook_data = ChefData::CookbookData.to_hash(cookbook, name_version)
        end
        raise "No version specified" unless cookbook_data[:version]

        data_store.create_dir(["organizations", org_name, cookbook_type], cookbook_data[:cookbook_name], :recursive)
        data_store.set(["organizations", org_name, cookbook_type, cookbook_data[:cookbook_name], cookbook_data[:version]], FFI_Yajl::Encoder.encode(cookbook_data, pretty: true), :create)
        cookbook_data.values.each do |files|
          next unless files.is_a? Array

          files.each do |file|
            data_store.set(["organizations", org_name, "file_store", "checksums", file[:checksum]], get_file(cookbook, file[:path]), :create)
          end
        end
      end
    end
  end
end

#local_mode_urlObject



179
180
181
182
183
# File 'lib/chef_zero/server.rb', line 179

def local_mode_url
  raise "Port not yet set, cannot generate URL" unless port.is_a?(Integer)

  "chefzero://localhost:#{port}"
end

#on_request(&block) ⇒ Object



389
390
391
# File 'lib/chef_zero/server.rb', line 389

def on_request(&block)
  @on_request_proc = block
end

#on_response(&block) ⇒ Object



393
394
395
# File 'lib/chef_zero/server.rb', line 393

def on_response(&block)
  @on_response_proc = block
end

#portInteger

Returns:

  • (Integer)


145
146
147
148
149
150
151
152
153
154
# File 'lib/chef_zero/server.rb', line 145

def port
  if @port
    @port
  # If options[:port] is not an Array or an Enumerable, it is just an Integer.
  elsif !options[:port].respond_to?(:each)
    options[:port]
  else
    raise "port cannot be determined until server is started"
  end
end

#request_handler(&block) ⇒ Object



526
527
528
# File 'lib/chef_zero/server.rb', line 526

def request_handler(&block)
  @request_handler = block
end

#running?Boolean

Boolean method to determine if the server is currently ready to accept requests. This method will attempt to make an HTTP request against the server. If this method returns true, you are safe to make a request.

Returns:

  • (Boolean)

    true if the server is accepting requests, false otherwise



350
351
352
# File 'lib/chef_zero/server.rb', line 350

def running?
  !@server.nil? && @running && @server.status == :Running
end

#start(publish = true) ⇒ nil

Start a Chef Zero server in the current thread. You can stop this server by canceling the current thread.

Parameters:

  • publish (Boolean|IO) (defaults to: true)

    publish the server information to the publish parameter or to STDOUT if it’s “true”

Returns:

  • (nil)

    this method will block the main thread until interrupted



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/chef_zero/server.rb', line 231

def start(publish = true)
  publish = publish[:publish] if publish.is_a?(Hash) # Legacy API

  if publish
    output = publish.respond_to?(:puts) ? publish : STDOUT
    output.puts <<-EOH.gsub(/^ {10}/, "")
      >> Starting #{ChefZero::Dist::PRODUCT} (v#{ChefZero::VERSION})...
    EOH
  end

  thread = start_background

  if publish
    output = publish.respond_to?(:puts) ? publish : STDOUT
    output.puts <<-EOH.gsub(/^ {10}/, "")
      >> WEBrick (v#{WEBrick::VERSION}) on Rack (v#{Rack.release}) is listening at #{url}
      >> Press CTRL+C to stop

    EOH
  end

  %w{INT TERM}.each do |signal|
    Signal.trap(signal) do
      puts "\n>> Stopping #{ChefZero::Dist::PRODUCT}..."
      @server.shutdown
    end
  end

  # Move the background process to the main thread
  thread.join
end

#start_background(wait = 5) ⇒ Object



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/chef_zero/server.rb', line 285

def start_background(wait = 5)
  @server = WEBrick::HTTPServer.new(
    DoNotListen: true,
    AccessLog: [],
    Logger: WEBrick::Log.new(StringIO.new, 7),
    RequestTimeout: 300,
    SSLEnable: options[:ssl],
    SSLOptions: ssl_opts,
    SSLCertName: [ [ "CN", WEBrick::Utils.getservername ] ],
    StartCallback: proc do
      @running = true
    end
  )
  ENV["HTTPS"] = "on" if options[:ssl]
  @server.mount("/", Rackup::Handler::WEBrick, app)

  # Pick a port
  # If options[:port] can be an Enumerator, an Array, or an Integer,
  # we need something that can respond to .each (Enum and Array can already).
  Array(options[:port]).each do |port|
    if listen(Array(options[:host]), port)
      @port = port
      break
    end
  end
  unless @port
    raise Errno::EADDRINUSE,
      "No port in :port range #{options[:port]} is available"
  end

  # Start the server in the background
  @thread = Thread.new do

    Thread.current.abort_on_exception = true
    @server.start
  ensure
    @port = nil
    @running = false

  end

  # Do not return until the web server is genuinely started.
  sleep(0.01) while !@running && @thread.alive?

  SocketlessServerMap.instance.register_port(@port, self)

  @thread
end

#start_socketlessObject



334
335
336
# File 'lib/chef_zero/server.rb', line 334

def start_socketless
  @port = SocketlessServerMap.instance.register_no_listen_server(self)
end

#stop(wait = 5) ⇒ Object

Gracefully stop the Chef Zero server.

Parameters:

  • wait (Fixnum) (defaults to: 5)

    the number of seconds to wait before raising force-terminating the server



361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/chef_zero/server.rb', line 361

def stop(wait = 5)
  if @running
    @server.shutdown if @server
    @thread.join(wait) if @thread
  end
rescue Timeout::Error
  if @thread
    ChefZero::Log.error("#{ChefZero::Dist::PRODUCT} did not stop within #{wait} seconds! Killing...")
    @thread.kill
    SocketlessServerMap.deregister(port)
  end
ensure
  @server = nil
  @thread = nil
end

#to_sObject



530
531
532
# File 'lib/chef_zero/server.rb', line 530

def to_s
  "#<#{self.class} #{url}>"
end

#urlString

The URL for this Chef Zero server. If the given host is an IPV6 address, it is escaped in brackets according to RFC-2732.

Returns:

  • (String)

See Also:



169
170
171
172
173
174
175
176
177
# File 'lib/chef_zero/server.rb', line 169

def url
  sch = @options[:ssl] ? "https" : "http"
  hosts = Array(@options[:host])
  @url ||= if hosts.first.include?(":")
             URI("#{sch}://[#{hosts.first}]:#{port}").to_s
           else
             URI("#{sch}://#{hosts.first}:#{port}").to_s
           end
end