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.



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

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)


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

def options
  @options
end

#serverWEBrick::HTTPServer (readonly)

Returns:

  • (WEBrick::HTTPServer)


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

def server
  @server
end

Instance Method Details

#clear_dataObject



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

def clear_data
  data_store.clear
end

#data_storeChefZero::DataStore

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

Returns:



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

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



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

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



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

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

#handle_socketless_request(request_env) ⇒ Object



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

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

#inspectObject



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

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



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

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"'
  }
}

}



419
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
# File 'lib/chef_zero/server.rb', line 419

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



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

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



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

def on_request(&block)
  @on_request_proc = block
end

#on_response(&block) ⇒ Object



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

def on_response(&block)
  @on_response_proc = block
end

#portInteger

Returns:

  • (Integer)


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

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



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

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



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

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



230
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
# File 'lib/chef_zero/server.rb', line 230

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



284
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
# File 'lib/chef_zero/server.rb', line 284

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("/", Rack::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
    begin
      Thread.current.abort_on_exception = true
      @server.start
    ensure
      @port = nil
      @running = false
    end
  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



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

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



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

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



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

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:



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

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