class ChefZero::Server

Constants

DEFAULT_OPTIONS
GLOBAL_ENDPOINTS

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

Public Instance Methods

clear_data() click to toggle source
# File lib/chef_zero/server.rb, line 521
def clear_data
  data_store.clear
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 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_pair() click to toggle source
# 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?() 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 216
def generate_real_keys?
  !!@options[:generate_real_keys]
end
handle_socketless_request(request_env) click to toggle source
# File lib/chef_zero/server.rb, line 337
def handle_socketless_request(request_env)
  app.call(request_env)
end
inspect() click to toggle source
# File lib/chef_zero/server.rb, line 533
def inspect
  "#<#{self.class} @url=#{url.inspect}>"
end
listen(hosts, port) 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 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) 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 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_url() click to toggle source
# 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) click to toggle source
# File lib/chef_zero/server.rb, line 388
def on_request(&block)
  @on_request_proc = block
end
on_response(&block) click to toggle source
# File lib/chef_zero/server.rb, line 392
def on_response(&block)
  @on_response_proc = block
end
port() click to toggle source

@return [Integer]

# 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) click to toggle source
# File lib/chef_zero/server.rb, line 525
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 349
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 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) click to toggle source
# 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_socketless() click to toggle source
# File lib/chef_zero/server.rb, line 333
def start_socketless
  @port = SocketlessServerMap.instance.register_no_listen_server(self)
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 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_s() click to toggle source
# File lib/chef_zero/server.rb, line 529
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 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

Private Instance Methods

app() click to toggle source
# File lib/chef_zero/server.rb, line 646
def app
  return @app if @app

  router = RestRouter.new(endpoints)
  router.not_found = NotFoundEndpoint.new

  if options[:single_org]
    rest_base_prefix = [ "organizations", options[:single_org] ]
  else
    rest_base_prefix = []
  end
  @app = proc do |env|
    begin
      prefix = global_endpoint?(env["PATH_INFO"]) ? [] : rest_base_prefix

      request = RestRequest.new(env, 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"

      # Add CORS header
      response[1]["Access-Control-Allow-Origin"] = "*"

      # 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
  @app
end
dejsonize(value) click to toggle source
# File lib/chef_zero/server.rb, line 706
def dejsonize(value)
  value.is_a?(Hash) ? FFI_Yajl::Encoder.encode(value, pretty: true) : value
end
dejsonize_children(hash) click to toggle source
# File lib/chef_zero/server.rb, line 698
def dejsonize_children(hash)
  result = {}
  hash.each_pair do |key, value|
    result[key] = dejsonize(value)
  end
  result
end
endpoints() click to toggle source
# File lib/chef_zero/server.rb, line 539
def endpoints
  result = if options[:osc_compat]
             # OSC-only
             [
               [ "/organizations/*/users", ActorsEndpoint.new(self) ],
               [ "/organizations/*/users/*", ActorEndpoint.new(self) ],
               [ "/organizations/*/authenticate_user", OrganizationAuthenticateUserEndpoint.new(self) ],
             ]
           else
             # EC-only
             [
               [ "/organizations/*/users", OrganizationUsersEndpoint.new(self) ],
               [ "/organizations/*/users/*", OrganizationUserEndpoint.new(self) ],
               [ "/users", ActorsEndpoint.new(self, "username") ],
               [ "/users/*", ActorEndpoint.new(self, "username") ],
               [ "/users/*/_acl", AclsEndpoint.new(self) ],
               [ "/users/*/_acl/*", AclEndpoint.new(self) ],
               [ "/users/*/association_requests", UserAssociationRequestsEndpoint.new(self) ],
               [ "/users/*/association_requests/count", UserAssociationRequestsCountEndpoint.new(self) ],
               [ "/users/*/association_requests/*", UserAssociationRequestEndpoint.new(self) ],
               [ "/users/*/keys", ActorKeysEndpoint.new(self) ],
               [ "/users/*/keys/default", ActorDefaultKeyEndpoint.new(self) ],
               [ "/users/*/keys/*", ActorKeyEndpoint.new(self) ],
               [ "/users/*/organizations", UserOrganizationsEndpoint.new(self) ],
               [ "/authenticate_user", AuthenticateUserEndpoint.new(self) ],
               [ "/system_recovery", SystemRecoveryEndpoint.new(self) ],
               [ "/license", LicenseEndpoint.new(self) ],
               [ "/organizations", OrganizationsEndpoint.new(self) ],
               [ "/organizations/*", OrganizationEndpoint.new(self) ],
               [ "/organizations/*/_validator_key", OrganizationValidatorKeyEndpoint.new(self) ],
               [ "/organizations/*/association_requests", OrganizationAssociationRequestsEndpoint.new(self) ],
               [ "/organizations/*/association_requests/*", OrganizationAssociationRequestEndpoint.new(self) ],
               [ "/organizations/*/containers", ContainersEndpoint.new(self) ],
               [ "/organizations/*/containers/*", ContainerEndpoint.new(self) ],
               [ "/organizations/*/groups", GroupsEndpoint.new(self) ],
               [ "/organizations/*/groups/*", GroupEndpoint.new(self) ],
               [ "/organizations/*/organization/_acl", AclsEndpoint.new(self) ],
               [ "/organizations/*/organizations/_acl", AclsEndpoint.new(self) ],
               [ "/organizations/*/*/*/_acl", AclsEndpoint.new(self) ],
               [ "/organizations/*/organization/_acl/*", AclEndpoint.new(self) ],
               [ "/organizations/*/organizations/_acl/*", AclEndpoint.new(self) ],
               [ "/organizations/*/*/*/_acl/*", AclEndpoint.new(self) ],
             ]
           end
  result + [
    # Both
    [ "/dummy", DummyEndpoint.new(self) ],
    [ "/organizations/*/clients", ActorsEndpoint.new(self) ],
    [ "/organizations/*/clients/*", ActorEndpoint.new(self) ],
    [ "/organizations/*/clients/*/keys", ActorKeysEndpoint.new(self) ],
    [ "/organizations/*/clients/*/keys/default", ActorDefaultKeyEndpoint.new(self) ],
    [ "/organizations/*/clients/*/keys/*", ActorKeyEndpoint.new(self) ],
    [ "/organizations/*/users/*/keys", OrganizationUserKeysEndpoint.new(self) ],
    [ "/organizations/*/users/*/keys/default", OrganizationUserDefaultKeyEndpoint.new(self) ],
    [ "/organizations/*/users/*/keys/*", OrganizationUserKeyEndpoint.new(self) ],
    [ "/organizations/*/controls", ControlsEndpoint.new(self) ],
    [ "/organizations/*/cookbooks", CookbooksEndpoint.new(self) ],
    [ "/organizations/*/cookbooks/*", CookbookEndpoint.new(self) ],
    [ "/organizations/*/cookbooks/*/*", CookbookVersionEndpoint.new(self) ],
    [ "/organizations/*/cookbook_artifacts", CookbookArtifactsEndpoint.new(self) ],
    [ "/organizations/*/cookbook_artifacts/*", CookbookArtifactEndpoint.new(self) ],
    [ "/organizations/*/cookbook_artifacts/*/*", CookbookArtifactIdentifierEndpoint.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", NodesEndpoint.new(self) ],
    [ "/organizations/*/nodes/*", NodeEndpoint.new(self) ],
    [ "/organizations/*/nodes/*/_identifiers", NodeIdentifiersEndpoint.new(self) ],
    [ "/organizations/*/policies", PoliciesEndpoint.new(self) ],
    [ "/organizations/*/policies/*", PolicyEndpoint.new(self) ],
    [ "/organizations/*/policies/*/revisions", PolicyRevisionsEndpoint.new(self) ],
    [ "/organizations/*/policies/*/revisions/*", PolicyRevisionEndpoint.new(self) ],
    [ "/organizations/*/policy_groups", PolicyGroupsEndpoint.new(self) ],
    [ "/organizations/*/policy_groups/*", PolicyGroupEndpoint.new(self) ],
    [ "/organizations/*/policy_groups/*/policies/*", PolicyGroupPolicyEndpoint.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/*/universe", UniverseEndpoint.new(self) ],
    [ "/version", VersionEndpoint.new(self) ],
    [ "/server_api_version", ServerAPIVersionEndpoint.new(self) ],

    # Internal
    [ "/organizations/*/file_store/**", FileStoreFileEndpoint.new(self) ],
  ]
end
get_file(directory, path) click to toggle source
# File lib/chef_zero/server.rb, line 710
def get_file(directory, path)
  value = directory
  path.split("/").each do |part|
    value = value[part]
  end
  value
end
global_endpoint?(ep) click to toggle source
# File lib/chef_zero/server.rb, line 640
def global_endpoint?(ep)
  GLOBAL_ENDPOINTS.any? do |g_ep|
    ep.start_with?(g_ep)
  end
end
ssl_opts() click to toggle source

Disable unsecure ssl Ref: www.ruby-lang.org/en/news/2014/10/27/changing-default-settings-of-ext-openssl/

# File lib/chef_zero/server.rb, line 720
def ssl_opts
  ssl_opts = OpenSSL::SSL::OP_ALL
  ssl_opts &= ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS if defined?(OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS)
  ssl_opts |= OpenSSL::SSL::OP_NO_COMPRESSION if defined?(OpenSSL::SSL::OP_NO_COMPRESSION)
  ssl_opts |= OpenSSL::SSL::OP_NO_SSLv2 if defined?(OpenSSL::SSL::OP_NO_SSLv2)
  ssl_opts |= OpenSSL::SSL::OP_NO_SSLv3 if defined?(OpenSSL::SSL::OP_NO_SSLv3)
  ssl_opts
end