class ChefZero::Server

Constants

DEFAULT_OPTIONS

Attributes

options[R]

@return [Hash]

server[R]

@return [WEBrick::HTTPServer]

Public Class Methods

new(options = {}) click to toggle source
# File lib/chef_zero/server.rb, line 73
def initialize(options = {})
  @options = DEFAULT_OPTIONS.merge(options)
  @options.freeze

  ChefZero::Log.level = @options[:log_level].to_sym
end

Public Instance Methods

clear_data() click to toggle source
# File lib/chef_zero/server.rb, line 365
def clear_data
  data_store.clear
  if options[:single_org]
    data_store.create_dir([ 'organizations' ], options[:single_org])
  end
end
data_store() click to toggle source

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

@return [ChefZero::DataStore]

# File lib/chef_zero/server.rb, line 120
def data_store
  @data_store ||= begin
    result = @options[:data_store] || DataStore::MemoryStoreV2.new
    if options[:single_org]
      if result.respond_to?(:interface_version) && result.interface_version >= 2 && result.interface_version < 3
        result.create_dir([ 'organizations' ], options[:single_org])
      else
        result = ChefZero::DataStore::V1ToV2Adapter.new(result, options[:single_org])
      end
    else
      if !(result.respond_to?(:interface_version) && result.interface_version >= 2 && result.interface_version < 3)
        raise "Multi-org not supported by data store #{result}!"
      end
    end

    result
  end
end
gen_key_pair() click to toggle source
# File lib/chef_zero/server.rb, line 286
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?() click to toggle source

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

@return [Boolean]

true if real keys should be created, false otherwise
# File lib/chef_zero/server.rb, line 146
def generate_real_keys?
  !!@options[:generate_real_keys]
end
inspect() click to toggle source
# File lib/chef_zero/server.rb, line 380
def inspect
  "#<#{self.class} @url=#{url.inspect}>"
end
load_data(contents, org_name = 'chef') click to toggle source

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

}

# File lib/chef_zero/server.rb, line 329
def load_data(contents, org_name = 'chef')
  %w(clients environments nodes roles users).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['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['cookbooks']
    contents['cookbooks'].each_pair do |name_version, cookbook|
      if name_version =~ /(.+)-(\d+\.\d+\.\d+)$/
        cookbook_data = CookbookData.to_hash(cookbook, $1, $2)
      else
        cookbook_data = CookbookData.to_hash(cookbook, name_version)
      end
      raise "No version specified" if !cookbook_data[:version]
      data_store.create_dir(['organizations', org_name, 'cookbooks'], cookbook_data[:cookbook_name], :recursive)
      data_store.set(['organizations', org_name, 'cookbooks', cookbook_data[:cookbook_name], cookbook_data[:version]], JSON.pretty_generate(cookbook_data), :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
on_request(&block) click to toggle source
# File lib/chef_zero/server.rb, line 298
def on_request(&block)
  @on_request_proc = block
end
on_response(&block) click to toggle source
# File lib/chef_zero/server.rb, line 302
def on_response(&block)
  @on_response_proc = block
end
port() click to toggle source

@return [Integer]

# File lib/chef_zero/server.rb, line 84
def port
  if @port
    @port
  elsif !options[:port].respond_to?(:each)
    options[:port]
  else
    raise "port cannot be determined until server is started"
  end
end
request_handler(&block) click to toggle source
# File lib/chef_zero/server.rb, line 372
def request_handler(&block)
  @request_handler = block
end
running?() click to toggle source

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.

@return [Boolean]

true if the server is accepting requests, false otherwise
# File lib/chef_zero/server.rb, line 260
def running?
  !@server.nil? && @running && @server.status == :Running
end
start(publish = true) click to toggle source

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

@param [Boolean|IO] publish

publish the server information to the publish parameter or to STDOUT if it's "true"

@return [nil]

this method will block the main thread until interrupted
# File lib/chef_zero/server.rb, line 160
    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 "          >> Starting Chef Zero (v#{ChefZero::VERSION})...
".gsub(/^ {10}/, '')
      end

      thread = start_background

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

".gsub(/^ {10}/, '')
      end

      %w[INT TERM].each do |signal|
        Signal.trap(signal) do
          puts "\n>> Stopping Chef Zero..."
          @server.shutdown
        end
      end

      # Move the background process to the main thread
      thread.join
    end
start_background(wait = 5) click to toggle source

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

@param [Fixnum] wait

the number of seconds to wait for the server to start

@return [Thread]

the thread the background process is running in
# File lib/chef_zero/server.rb, line 203
def start_background(wait = 5)
  @server = WEBrick::HTTPServer.new(
    :DoNotListen => true,
    :AccessLog   => [],
    :Logger      => WEBrick::Log.new(StringIO.new, 7),
    :StartCallback => proc {
      @running = true
    }
  )
  @server.mount('/', Rack::Handler::WEBrick, app)

  # Pick a port
  if options[:port].respond_to?(:each)
    options[:port].each do |port|
      begin
        @server.listen(options[:host], port)
        @port = port
        break
      rescue Errno::EADDRINUSE
        ChefZero::Log.info("Port #{port} in use: #{$!}")
      end
    end
    if !@port
      raise Errno::EADDRINUSE, "No port in :port range #{options[:port]} is available"
    end
  else
    @server.listen(options[:host], options[:port])
    @port = options[:port]
  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.
  while !@running && @thread.alive?
    sleep(0.01)
  end

  @thread
end
stop(wait = 5) click to toggle source

Gracefully stop the Chef Zero server.

@param [Fixnum] wait

the number of seconds to wait before raising force-terminating the
server
# File lib/chef_zero/server.rb, line 271
def stop(wait = 5)
  if @running
    @server.shutdown
    @thread.join(wait)
  end
rescue Timeout::Error
  if @thread
    ChefZero::Log.error("Chef Zero did not stop within #{wait} seconds! Killing...")
    @thread.kill
  end
ensure
  @server = nil
  @thread = nil
end
to_s() click to toggle source
# File lib/chef_zero/server.rb, line 376
def to_s
  "#<#{self.class} #{url}>"
end
url() click to toggle source

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

@see www.ietf.org/rfc/rfc2732.txt RFC-2732

@return [String]

# File lib/chef_zero/server.rb, line 107
def url
  @url ||= if @options[:host].include?(':')
             URI("http://[#{@options[:host]}]:#{port}").to_s
           else
             URI("http://#{@options[:host]}:#{port}").to_s
           end
end

Private Instance Methods

app() click to toggle source
# File lib/chef_zero/server.rb, line 423
def app
  router = RestRouter.new(open_source_endpoints)
  router.not_found = NotFoundEndpoint.new

  if options[:single_org]
    rest_base_prefix = [ 'organizations', options[:single_org] ]
  else
    rest_base_prefix = []
  end
  return proc do |env|
    begin
      request = RestRequest.new(env, rest_base_prefix)
      if @on_request_proc
        @on_request_proc.call(request)
      end
      response = nil
      if @request_handler
        response = @request_handler.call(request)
      end
      unless response
        response = router.call(request)
      end
      if @on_response_proc
        @on_response_proc.call(request, response)
      end

      # Insert Server header
      response[1]['Server'] = 'chef-zero'

      # Puma expects the response to be an array (chunked responses). Since
      # we are statically generating data, we won't ever have said chunked
      # response, so fake it.
      response[-1] = Array(response[-1])

      response
    rescue
      if options[:log_level] == :debug
        STDERR.puts "Request Error: #{$!}"
        STDERR.puts $!.backtrace.join("\n")
      end
    end
  end
end
dejsonize_children(hash) click to toggle source
# File lib/chef_zero/server.rb, line 467
def dejsonize_children(hash)
  result = {}
  hash.each_pair do |key, value|
    result[key] = value.is_a?(Hash) ? JSON.pretty_generate(value) : value
  end
  result
end
get_file(directory, path) click to toggle source
# File lib/chef_zero/server.rb, line 475
def get_file(directory, path)
  value = directory
  path.split('/').each do |part|
    value = value[part]
  end
  value
end
open_source_endpoints() click to toggle source
# File lib/chef_zero/server.rb, line 386
def open_source_endpoints
  [
    [ "/organizations/*/authenticate_user", AuthenticateUserEndpoint.new(self) ],
    [ "/organizations/*/clients", ActorsEndpoint.new(self) ],
    [ "/organizations/*/clients/*", ActorEndpoint.new(self) ],
    [ "/organizations/*/cookbooks", CookbooksEndpoint.new(self) ],
    [ "/organizations/*/cookbooks/*", CookbookEndpoint.new(self) ],
    [ "/organizations/*/cookbooks/*/*", CookbookVersionEndpoint.new(self) ],
    [ "/organizations/*/data", DataBagsEndpoint.new(self) ],
    [ "/organizations/*/data/*", DataBagEndpoint.new(self) ],
    [ "/organizations/*/data/*/*", DataBagItemEndpoint.new(self) ],
    [ "/organizations/*/environments", RestListEndpoint.new(self) ],
    [ "/organizations/*/environments/*", EnvironmentEndpoint.new(self) ],
    [ "/organizations/*/environments/*/cookbooks", EnvironmentCookbooksEndpoint.new(self) ],
    [ "/organizations/*/environments/*/cookbooks/*", EnvironmentCookbookEndpoint.new(self) ],
    [ "/organizations/*/environments/*/cookbook_versions", EnvironmentCookbookVersionsEndpoint.new(self) ],
    [ "/organizations/*/environments/*/nodes", EnvironmentNodesEndpoint.new(self) ],
    [ "/organizations/*/environments/*/recipes", EnvironmentRecipesEndpoint.new(self) ],
    [ "/organizations/*/environments/*/roles/*", EnvironmentRoleEndpoint.new(self) ],
    [ "/organizations/*/nodes", RestListEndpoint.new(self) ],
    [ "/organizations/*/nodes/*", NodeEndpoint.new(self) ],
    [ "/organizations/*/principals/*", PrincipalEndpoint.new(self) ],
    [ "/organizations/*/roles", RestListEndpoint.new(self) ],
    [ "/organizations/*/roles/*", RoleEndpoint.new(self) ],
    [ "/organizations/*/roles/*/environments", RoleEnvironmentsEndpoint.new(self) ],
    [ "/organizations/*/roles/*/environments/*", EnvironmentRoleEndpoint.new(self) ],
    [ "/organizations/*/sandboxes", SandboxesEndpoint.new(self) ],
    [ "/organizations/*/sandboxes/*", SandboxEndpoint.new(self) ],
    [ "/organizations/*/search", SearchesEndpoint.new(self) ],
    [ "/organizations/*/search/*", SearchEndpoint.new(self) ],
    [ "/organizations/*/users", ActorsEndpoint.new(self) ],
    [ "/organizations/*/users/*", ActorEndpoint.new(self) ],

    [ "/organizations/*/file_store/**", FileStoreFileEndpoint.new(self) ],
  ]
end