class ChefZero::Server
Constants
- DEFAULT_OPTIONS
- GLOBAL_ENDPOINTS
Attributes
@return [Hash]
@return [WEBrick::HTTPServer]
Public Class Methods
# File lib/chef_zero/server.rb, line 129 def initialize(options = {}) @options = DEFAULT_OPTIONS.merge(options) if @options[:single_org] && !@options.has_key?(:osc_compat) @options[:osc_compat] = true end @options.freeze ChefZero::Log.level = @options[:log_level].to_sym @app = nil end
Public Instance Methods
# File lib/chef_zero/server.rb, line 517 def clear_data data_store.clear end
The data store for this server (default is in-memory).
@return [ChefZero::DataStore]
# File lib/chef_zero/server.rb, line 187 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
# File lib/chef_zero/server.rb, line 374 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
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 214 def generate_real_keys? !!@options[:generate_real_keys] end
# File lib/chef_zero/server.rb, line 335 def handle_socketless_request(request_env) app.call(request_env) end
# File lib/chef_zero/server.rb, line 529 def inspect "#<#{self.class} @url=#{url.inspect}>" end
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 270 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 { |l| l.close } @server.listeners.clear false end
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 417 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" if !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
# File lib/chef_zero/server.rb, line 177 def local_mode_url raise "Port not yet set, cannot generate URL" unless port.kind_of?(Integer) "chefzero://localhost:#{port}" end
# File lib/chef_zero/server.rb, line 386 def on_request(&block) @on_request_proc = block end
# File lib/chef_zero/server.rb, line 390 def on_response(&block) @on_response_proc = block end
@return [Integer]
# File lib/chef_zero/server.rb, line 143 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
# File lib/chef_zero/server.rb, line 521 def request_handler(&block) @request_handler = block end
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 347 def running? !@server.nil? && @running && @server.status == :Running end
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 228 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 Chef Zero (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 Chef Zero..." @server.shutdown end end # Move the background process to the main thread thread.join end
# File lib/chef_zero/server.rb, line 282 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 if !@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
# File lib/chef_zero/server.rb, line 331 def start_socketless @port = SocketlessServerMap.instance.register_no_listen_server(self) end
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 358 def stop(wait = 5) if @running @server.shutdown if @server @thread.join(wait) if @thread end rescue Timeout::Error if @thread ChefZero::Log.error("Chef Zero did not stop within #{wait} seconds! Killing...") @thread.kill SocketlessServerMap.deregister(port) end ensure @server = nil @thread = nil end
# File lib/chef_zero/server.rb, line 525 def to_s "#<#{self.class} #{url}>" end
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 167 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
# File lib/chef_zero/server.rb, line 642 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
# File lib/chef_zero/server.rb, line 701 def dejsonize(value) value.is_a?(Hash) ? FFI_Yajl::Encoder.encode(value, :pretty => true) : value end
# File lib/chef_zero/server.rb, line 693 def dejsonize_children(hash) result = {} hash.each_pair do |key, value| result[key] = dejsonize(value) end result end
# File lib/chef_zero/server.rb, line 535 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
# File lib/chef_zero/server.rb, line 705 def get_file(directory, path) value = directory path.split("/").each do |part| value = value[part] end value end
# File lib/chef_zero/server.rb, line 636 def global_endpoint?(ep) GLOBAL_ENDPOINTS.any? do |g_ep| ep.start_with?(g_ep) end end
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 715 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