Runtime environment

The primary runtime environment for the provider is the Puppet Agent, a long-running daemon process. The provider can also be used in the puppet apply command, a self contained version of the agent, or the puppet resource command, a short-lived command line interface (CLI) process for listing or managing a single resource type. Other callers that want to access the provider must imitate these environments.

The primary life cycle of resource management in each of these tools is the transaction, a single set of changes, for example a catalog or a CLI invocation. The provider's class is instantiated one time for each transaction. Within that class the provider defines any number of helper methods to support itself. To allow for a transaction to set up the prerequisites for a provider and be used immediately, the provider is instantiated as late as possible. A transaction usually calls get one time, and may call set any number of times to make changes.

The object instance that hosts the get and set methods can be used to cache ephemeral state during execution. The provider should not try to cache state outside of its instances. In many cases, such caching won't help as the hosting process only manages a single transaction. In long-running runtime environments like the agent, the benefit of the caching needs to be balanced with the cost of the cache at rest, and the lifetime of cache entries, which are only useful when they are longer than the regular runinterval.

The runtime environment has the following utilities to provide a uniform experience for its users.

Logging and reporting utilities

The provider needs to signal changes, successes, and failures to the runtime environment. The context is the primary way to do this. It provides a structured logging interface for all provider actions. Using this information, the runtime environments can do automatic processing, emit human readable progress information, and provide status messages for operators.

To provide feedback about the overall operation of the provider, the context has the usual set of log level methods that take a string, and pass that up to the runtime environments logging infrastructure. For example:

context.warning("Unexpected state detected, continuing in degraded mode.")
Results in the following message:
Warning: apt_key: Unexpected state detected, continuing in degraded mode.
Other common messages include:

  • debug: Detailed messages to understand everything that is happening at runtime, shown on request.

  • info: Regular progress and status messages, especially useful before long-running operations, or before operations that can fail, to provide context for interactive users.

  • notice: Indicates state changes and other events of notice from the regular operations of the provider.

  • warning: Signals error conditions that do not (yet) prohibit execution of the main part of the provider; for example, deprecation warnings, temporary errors.

  • err: Signals error conditions that have caused normal operations to fail.

  • critical, alert, emerg: Should not be used by resource providers.

In simple cases, a provider passes off work to an external tool, logs the details there, and then reports back to Puppet acknowledging these changes. This is called resource status signaling, and looks like this:

@apt_key_cmd.run(context, action, key_id)
context.processed(key_id, is, should)
It reports all changes from is to should, using default messages.

Providers that want to have more control over the logging throughout the processing can use the more specific created(title), updated(title), deleted(title), unchanged(title) methods. To report the change of an attribute, the context provides a attribute_changed(title, attribute, old_value, new_value, message) method.

Most of those messages are expected to be relative to a specific resource instance, and a specific operation on that instance. To enable detailed logging without repeating key arguments, and to provide consistent error logging, the context provides logging context methods to capture the current action and resource instance:

context.updating(title) do
  if apt_key_not_found(title)
    context.warning('Original key not found')
  end

  # Update the key by calling CLI tool
  apt_key(...)

  context.attribute_changed('content', nil, content_hash,
    message: "Replaced with content hash #{content_hash}")
end
This results in the following messages:
Debug: Apt_key[F1D2D2F9]: Started updating
Warning: Apt_key[F1D2D2F9]: Updating: Original key not found
Debug: Apt_key[F1D2D2F9]: Executing 'apt-key ...'
Debug: Apt_key[F1D2D2F9]: Successfully executed 'apt-key ...'
Notice: Apt_key[F1D2D2F9]: Updating content: Replaced with content hash E242ED3B
Notice: Apt_key[F1D2D2F9]: Successfully updated
In the case of an exception escaping the block, the error is logged appropriately:
Debug: Apt_keyF1D2D2F9]: Started updating
Warning: Apt_key[F1D2D2F9]: Updating: Original key not found
Error: Apt_key[F1D2D2F9]: Updating failed: Something went wrong

Logging contexts process all exceptions. A StandardError is assumed to be regular failures in handling resources, and are consumed after logging. Everything else is assumed to be a fatal application-level issue, and is passed up the stack, ending execution. See the Ruby documentation for details on which exceptions are not a StandardError.

The equivalent long-hand form of manual error handling:

context.updating(title)
begin
  unless title_got_passed_to_set(title)
    raise Puppet::DevError, 'Managing resource outside of requested set: %{title}')
  end

  if apt_key_not_found(title)
    context.warning('Original key not found')
  end

  # Update the key by calling CLI tool
  result = @apt_key_cmd.run(...)

  if result.exitstatus != 0
    context.error(title, "Failed executing apt-key #{...}")
  else
    context.attribute_changed(title, 'content', nil, content_hash,
      message: "Replaced with content hash #{content_hash}")
  end
  context.changed(title)
rescue Exception => e
  context.error(title, e, message: 'Updating failed')
  raise unless e.is_a? StandardError
end
This example is only for demonstration purposes. In the normal course of operations, providers should always use the utility functions.

The following methods are available:

  • Block functions: these functions provide logging and timing around a provider's core actions. If the the passed &block returns, the action is recorded as successful. To signal a failure, the block should raise an exception explaining the problem:

    • creating(titles, message: 'Creating', &block)

    • updating(titles, message: 'Updating', &block)

    • deleting(titles, message: 'Deleting', &block)

    • processing(title, is, should, message: 'Processing', &block): generic processing of a resource, produces default change messages for the difference between is: and should:.

    • failing(titles, message: 'Failing', &block): unlikely to be used often, but provided for completeness. It always records a failure.

  • Action functions:

    • created(titles, message: 'Created')

    • updated(titles, message: 'Updated')

    • deleted(titles, message: 'Deleted')

    • processed(title, is, should): the resource has been processed. It produces default logging for the resource and each attribute

    • failed(titles, message:): the resource has not been updated successfully

  • Attribute Change notifications:

    • attribute_changed(title, attribute, is, should, message: nil): notify the runtime environment that a specific attribute for a specific resource has changed. is and should are the original and the new value of the attribute. Either can be nil.

  • Plain messages:

    • debug(message)

    • debug(titles, message:)

    • info(message)

    • info(titles, message:)

    • notice(message)

    • notice(titles, message:)

    • warning(message)

    • warning(titles, message:)

    • err(message)

    • err(titles, message:)

titles can be a single identifier for a resource or an array of values, if the following block batch processes multiple resources in one pass. If that processing is not atomic, providers should instead use the non-block forms of logging, and provide accurate status reporting on the individual parts of update operations.

A single set() execution may only log messages for instances that have been passed, as part of the changes to process. Logging for instances not requested to be changed causes an exception - the runtime environment is not prepared for other resources to change.

The provider is free to call different logging methods for different resources in any order it needs to. The only ordering restriction is for all calls specifying the same title. For example, the attribute_changed needs logged before that resource's action logging, and the context needs to be opened before any other logging for this resource.

Type definition

The provider can gain insight into the type definition through these context.type utility methods:

  • attributes: returns a hash containing the type attributes and its properties.

  • ensurable?: returns true if the type contains the ensure attribute.

  • feature?(feature): returns true if the type supports a given provider feature.

For example:

# example from simple_provider.rb

def set(context, changes)
  changes.each do |name, change|
  is = if context.type.feature?('simple_get_filter')
         change.key?(:is) ? change[:is] : (get(context, [name]) || []).find { |r| r[:name] == name }
       else
         change.key?(:is) ? change[:is] : (get(context) || []).find { |r| r[:name] == name }
       end
  ...

end