# # @api private # # The Compiler service is used to submit and retrieve data from the # puppetserver. # class Puppet::HTTP::Service::Compiler < Puppet::HTTP::Service # @api private # @return [String] Default API for the Compiler service API = '/puppet/v3'.freeze # # @api private # # @param [Puppet::HTTP::Client] client # @param [Puppet::HTTP::Session] session # @param [String] server (Puppet[:ca_server]) If an explicit server is given, # create a service using that server. If server is nil, the default value # is used to create the service. # @param [Integer] port (Puppet[:ca_port]) If an explicit port is given, create # a service using that port. If port is nil, the default value is used to # create the service. # def initialize(client, session, server, port) url = build_url(API, server || Puppet[:server], port || Puppet[:serverport]) super(client, session, url) end # # @api private # # Submit a GET request to retrieve a node from the server # # @param [String] name The name of the node being requested # @param [String] environment The name of the environment we are operating in # @param [String] configured_environment Optional, the name of the configured # environment. If unset, `environment` is used. # @param [String] transaction_uuid An agent generated transaction uuid, used # for connecting catalogs and reports. # # @return [Array] An array containing # the request response and the deserialized requested node # def get_node(name, environment:, configured_environment: nil, transaction_uuid: nil) headers = add_puppet_headers('Accept' => get_mime_types(Puppet::Node).join(', ')) response = @client.get( with_base_url("/node/#{name}"), headers: headers, params: { environment: environment, configured_environment: configured_environment || environment, transaction_uuid: transaction_uuid, } ) process_response(response) [response, deserialize(response, Puppet::Node)] end # # @api private # # Submit a POST request to submit a catalog to the server # # @param [String] name The name of the catalog to be submitted # @param [Puppet::Node::Facts] facts Facts for this catalog # @param [String] environment The name of the environment we are operating in # @param [String] configured_environment Optional, the name of the configured # environment. If unset, `environment` is used. # @param [Boolean] check_environment If true, request that the server check if # our `environment` matches the server-specified environment. If they do not # match, then the server may return an empty catalog in the server-specified # environment. # @param [String] transaction_uuid An agent generated transaction uuid, used # for connecting catalogs and reports. # @param [String] job_uuid A unique job identifier defined when the orchestrator # starts a puppet run via pxp-agent. This is used to correlate catalogs and # reports with the orchestrator job. # @param [Boolean] static_catalog Indicates if the file metadata(s) are inlined # in the catalog. This informs the agent if it needs to make a second request # to retrieve metadata in addition to the initial catalog request. # @param [Array] checksum_type An array of accepted checksum type. # Currently defaults to `["md5", "sha256", "sha384", "sha512", "sha224"]`, # or `["sha256", "sha384", "sha512", "sha224"]` if fips is enabled. # # @return [Array] An array # containing the request response and the deserialized catalog returned by # the server # def post_catalog(name, facts:, environment:, configured_environment: nil, check_environment: false, transaction_uuid: nil, job_uuid: nil, static_catalog: true, checksum_type: Puppet[:supported_checksum_types]) if Puppet[:preferred_serialization_format] == "pson" formatter = Puppet::Network::FormatHandler.format_for(:pson) # must use 'pson' instead of 'text/pson' facts_format = 'pson' else formatter = Puppet::Network::FormatHandler.format_for(:json) facts_format = formatter.mime end facts_as_string = serialize(formatter, facts) # query parameters are sent in the POST request body body = { facts_format: facts_format, facts: Puppet::Util.uri_query_encode(facts_as_string), environment: environment, configured_environment: configured_environment || environment, check_environment: !!check_environment, transaction_uuid: transaction_uuid, job_uuid: job_uuid, static_catalog: static_catalog, checksum_type: checksum_type.join('.') }.map do |key, value| "#{key}=#{Puppet::Util.uri_query_encode(value.to_s)}" end.join("&") headers = add_puppet_headers( 'Accept' => get_mime_types(Puppet::Resource::Catalog).join(', '), 'Content-Type' => 'application/x-www-form-urlencoded' ) response = @client.post( with_base_url("/catalog/#{name}"), body, headers: headers, # for legacy reasons we always send environment as a query parameter too params: { environment: environment }, ) process_response(response) [response, deserialize(response, Puppet::Resource::Catalog)] end # # @api private # # Submit a POST request to request a catalog to the server using v4 endpoint # # @param [String] certname The name of the node for which to compile the catalog. # @param [Hash] persistent A hash containing two required keys, facts and catalog, # which when set to true will cause the facts and reports to be stored in # PuppetDB, or discarded if set to false. # @param [String] environment The name of the environment for which to compile the catalog. # @param [Hash] facts A hash with a required values key, containing a hash of all the # facts for the node. If not provided, Puppet will attempt to fetch facts for the node # from PuppetDB. # @param [Hash] trusted_facts A hash with a required values key containing a hash of # the trusted facts for a node # @param [String] transaction_uuid The id for tracking the catalog compilation and # report submission. # @param [String] job_id The id of the orchestrator job that triggered this run. # @param [Hash] options A hash of options beyond direct input to catalogs. Options: # - prefer_requested_environment Whether to always override a node's classified # environment with the one supplied in the request. If this is true and no environment # is supplied, fall back to the classified environment, or finally, 'production'. # - capture_logs Whether to return the errors and warnings that occurred during # compilation alongside the catalog in the response body. # - log_level The logging level to use during the compile when capture_logs is true. # Options are 'err', 'warning', 'info', and 'debug'. # # @return [Array>] An array # containing the request response, the deserialized catalog returned by # the server and array containing logs (log array will be empty if capture_logs is false) # def post_catalog4(certname, persistence:, environment:, facts: nil, trusted_facts: nil, transaction_uuid: nil, job_id: nil, options: nil) unless persistence.is_a?(Hash) && (missing = [:facts, :catalog] - persistence.keys.map(&:to_sym)).empty? raise ArgumentError.new("The 'persistence' hash is missing the keys: #{missing.join(', ')}") end raise ArgumentError.new("Facts must be a Hash not a #{facts.class}") unless facts.nil? || facts.is_a?(Hash) body = { certname: certname, persistence: persistence, environment: environment, transaction_uuid: transaction_uuid, job_id: job_id, options: options } body[:facts] = { values: facts } unless facts.nil? body[:trusted_facts] = { values: trusted_facts } unless trusted_facts.nil? headers = add_puppet_headers( 'Accept' => get_mime_types(Puppet::Resource::Catalog).join(', '), 'Content-Type' => 'application/json' ) url = URI::HTTPS.build(host: @url.host, port: @url.port, path: Puppet::Util.uri_encode("/puppet/v4/catalog")) response = @client.post( url, body.to_json, headers: headers ) process_response(response) begin response_body = JSON.parse(response.body) catalog = Puppet::Resource::Catalog.from_data_hash(response_body['catalog']) rescue => err raise Puppet::HTTP::SerializationError.new("Failed to deserialize catalog from puppetserver response: #{err.message}", err) end logs = response_body['logs'] || [] [response, catalog, logs] end # # @api private # # Submit a GET request to retrieve the facts for the named node # # @param [String] name Name of the node to retrieve facts for # @param [String] environment Name of the environment we are operating in # # @return [Array] An array # containing the request response and the deserialized facts for the # specified node # def get_facts(name, environment:) headers = add_puppet_headers('Accept' => get_mime_types(Puppet::Node::Facts).join(', ')) response = @client.get( with_base_url("/facts/#{name}"), headers: headers, params: { environment: environment } ) process_response(response) [response, deserialize(response, Puppet::Node::Facts)] end # # @api private # # Submits a PUT request to submit facts for the node to the server # # @param [String] name Name of the node we are submitting facts for # @param [String] environment Name of the environment we are operating in # @param [Puppet::Node::Facts] facts Facts for the named node # # @return [Puppet::HTTP::Response] The request response # def put_facts(name, environment:, facts:) formatter = Puppet::Network::FormatHandler.format_for(Puppet[:preferred_serialization_format]) headers = add_puppet_headers( 'Accept' => get_mime_types(Puppet::Node::Facts).join(', '), 'Content-Type' => formatter.mime ) response = @client.put( with_base_url("/facts/#{name}"), serialize(formatter, facts), headers: headers, params: { environment: environment }, ) process_response(response) response end # # @api private # # Submit a GET request to find the status of a compiler # # @param [String] name The name of the node that a status being requested for # # @return [Array] An array containing # the request response and the deserialized status returned from the server # def get_status(name) headers = add_puppet_headers('Accept' => get_mime_types(Puppet::Status).join(', ')) response = @client.get( with_base_url("/status/#{name}"), headers: headers, params: { # environment is required, but meaningless, default to production environment: 'production' }, ) process_response(response) [response, deserialize(response, Puppet::Status)] end # # @api private # # Submit a GET request to retrieve a file stored with filebucket # # @param [String] path The request path, formatted by Puppet::FileBucket::Dipper # @param [String] environment Name of the environment we are operating in. # This should not impact filebucket at all, but is included to be consistent # with legacy code. # @param [String] bucket_path # @param [String] diff_with a checksum to diff against if we are comparing # files that are both stored in the bucket # @param [String] list_all # @param [String] fromdate # @param [String] todate # # @return [Array] An array # containing the request response and the deserialized file returned from # the server. # def get_filebucket_file(path, environment:, bucket_path: nil, diff_with: nil, list_all: nil, fromdate: nil, todate: nil) headers = add_puppet_headers('Accept' => 'application/octet-stream') response = @client.get( with_base_url("/file_bucket_file/#{path}"), headers: headers, params: { environment: environment, bucket_path: bucket_path, diff_with: diff_with, list_all: list_all, fromdate: fromdate, todate: todate } ) process_response(response) [response, deserialize(response, Puppet::FileBucket::File)] end # # @api private # # Submit a PUT request to store a file with filebucket # # @param [String] path The request path, formatted by Puppet::FileBucket::Dipper # @param [String] body The contents of the file to be backed # @param [String] environment Name of the environment we are operating in. # This should not impact filebucket at all, but is included to be consistent # with legacy code. # # @return [Puppet::HTTP::Response] The response request # def put_filebucket_file(path, body:, environment:) headers = add_puppet_headers({ 'Accept' => 'application/octet-stream', 'Content-Type' => 'application/octet-stream' }) response = @client.put( with_base_url("/file_bucket_file/#{path}"), body, headers: headers, params: { environment: environment } ) process_response(response) response end # # @api private # # Submit a HEAD request to check the status of a file stored with filebucket # # @param [String] path The request path, formatted by Puppet::FileBucket::Dipper # @param [String] environment Name of the environment we are operating in. # This should not impact filebucket at all, but is included to be consistent # with legacy code. # @param [String] bucket_path # # @return [Puppet::HTTP::Response] The request response # def head_filebucket_file(path, environment:, bucket_path: nil) headers = add_puppet_headers('Accept' => 'application/octet-stream') response = @client.head( with_base_url("/file_bucket_file/#{path}"), headers: headers, params: { environment: environment, bucket_path: bucket_path } ) process_response(response) response end end