class Chef::CookbookVersion

Chef::CookbookVersion

CookbookVersion is a model object encapsulating the data about a Chef cookbook. Chef supports maintaining multiple versions of a cookbook on a single server; each version is represented by a distinct instance of this class.

Constants

COOKBOOK_SEGMENTS

Attributes

all_files[R]
identifier[RW]

The `identifier` field is used for cookbook_artifacts, which are organized on the chef server according to their content. If the policy_mode option to CookbookManifest is set to true it will include this field in the manifest Hash and in the upload URL.

This field may be removed or have different behavior in the future, don't use it in 3rd party code. @api private

metadata[R]

A Chef::Cookbook::Metadata object. It has a setter that fixes up the metadata to add descriptions of the recipes contained in this CookbookVersion.

name[RW]
root_paths[RW]

Public Class Methods

available_versions(cookbook_name) click to toggle source

Given a cookbook_name, get a list of all versions that exist on the server.

Returns

[String]

Array of cookbook versions, which are strings like 'x.y.z'

nil

if the cookbook doesn't exist. an error will also be logged.

# File lib/chef/cookbook_version.rb, line 490
def self.available_versions(cookbook_name)
  chef_server_rest.get("cookbooks/#{cookbook_name}")[cookbook_name]["versions"].map do |cb|
    cb["version"]
  end
rescue Net::HTTPClientException => e
  if e.to_s =~ /^404/
    Chef::Log.error("Cannot find a cookbook named #{cookbook_name}")
    nil
  else
    raise
  end
end
cache() click to toggle source
# File lib/chef/cookbook_version.rb, line 86
def self.cache
  Chef::FileCache
end
checksum_cookbook_file(filepath) click to toggle source

This is the one and only method that knows how cookbook files' checksums are generated.

# File lib/chef/cookbook_version.rb, line 79
def self.checksum_cookbook_file(filepath)
  Chef::Digester.generate_md5_checksum_for_file(filepath)
rescue Errno::ENOENT
  Chef::Log.trace("File #{filepath} does not exist, so there is no checksum to generate")
  nil
end
chef_server_rest() click to toggle source
# File lib/chef/cookbook_version.rb, line 456
def self.chef_server_rest
  Chef::ServerAPI.new(Chef::Config[:chef_server_url], { version_class: Chef::CookbookManifestVersions })
end
from_cb_artifact_data(o) click to toggle source
# File lib/chef/cookbook_version.rb, line 430
def self.from_cb_artifact_data(o)
  from_hash(o)
end
from_hash(o) click to toggle source
# File lib/chef/cookbook_version.rb, line 415
def self.from_hash(o)
  cookbook_version = new(o["cookbook_name"] || o["name"])

  # We want the Chef::Cookbook::Metadata object to always be inflated
  cookbook_version.manifest = o
  cookbook_version.metadata = Chef::Cookbook::Metadata.from_hash(o["metadata"])
  cookbook_version.identifier = o["identifier"] if o.key?("identifier")

  # We don't need the following step when we decide to stop supporting deprecated operators in the metadata (e.g. <<, >>)
  cookbook_version.manifest["metadata"] = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(cookbook_version.metadata))

  cookbook_version.freeze_version if o["frozen?"]
  cookbook_version
end
latest_cookbooks()
Alias for: list
list() click to toggle source

The API returns only a single version of each cookbook in the result from the cookbooks method

# File lib/chef/cookbook_version.rb, line 471
def self.list
  chef_server_rest.get("cookbooks")
end
Also aliased as: latest_cookbooks
list_all_versions() click to toggle source
# File lib/chef/cookbook_version.rb, line 480
def self.list_all_versions
  chef_server_rest.get("cookbooks?num_versions=all")
end
load(name, version = "_latest") click to toggle source
# File lib/chef/cookbook_version.rb, line 465
def self.load(name, version = "_latest")
  version = "_latest" if version == "latest"
  from_hash(chef_server_rest.get("cookbooks/#{name}/#{version}"))
end
new(name, *root_paths, chef_server_rest: nil) click to toggle source

Creates a new Chef::CookbookVersion object.

Returns

object<Chef::CookbookVersion>

Duh. :)

# File lib/chef/cookbook_version.rb, line 94
def initialize(name, *root_paths, chef_server_rest: nil)
  @name = name
  @root_paths = root_paths
  @frozen = false

  @all_files = Array.new

  @file_vendor = nil
  @cookbook_manifest = Chef::CookbookManifest.new(self)
  @metadata = Chef::Cookbook::Metadata.new
  @chef_server_rest = chef_server_rest
end

Public Instance Methods

<=>(other) click to toggle source
# File lib/chef/cookbook_version.rb, line 503
def <=>(other)
  raise Chef::Exceptions::CookbookVersionNameMismatch if name != other.name
  # FIXME: can we change the interface to the Metadata class such
  # that metadata.version returns a Chef::Version instance instead
  # of a string?
  Chef::Version.new(version) <=> Chef::Version.new(other.version)
end
all_files=(files) click to toggle source
# File lib/chef/cookbook_version.rb, line 72
def all_files=(files)
  @all_files = Array(files)
  cookbook_manifest.reset!
end
attribute_filenames_by_short_filename() click to toggle source
# File lib/chef/cookbook_version.rb, line 131
def attribute_filenames_by_short_filename
  @attribute_filenames_by_short_filename ||= begin
    name_map = filenames_by_name(files_for("attributes"))
    root_alias = cookbook_manifest.root_files.find { |record| record[:name] == "root_files/attributes.rb" }
    name_map["default"] = root_alias[:full_path] if root_alias
    name_map
  end
end
checksums() click to toggle source

Returns a hash of checksums to either nil or the on disk path (which is done by generate_manifest).

# File lib/chef/cookbook_version.rb, line 167
def checksums
  cookbook_manifest.checksums
end
chef_server_rest() click to toggle source

REST API

# File lib/chef/cookbook_version.rb, line 452
def chef_server_rest
  @chef_server_rest ||= chef_server_rest
end
cookbook_manifest() click to toggle source
# File lib/chef/cookbook_version.rb, line 511
def cookbook_manifest
  @cookbook_manifest ||= CookbookManifest.new(self)
end
destroy() click to toggle source
# File lib/chef/cookbook_version.rb, line 460
def destroy
  chef_server_rest.delete("cookbooks/#{name}/#{version}")
  self
end
display() click to toggle source
# File lib/chef/cookbook_version.rb, line 405
def display
  output = Mash.new
  output["cookbook_name"] = name
  output["name"] = full_name
  output["frozen?"] = frozen_version?
  output["metadata"] = metadata.to_h
  output["version"] = version
  output.merge(cookbook_manifest.by_parent_directory)
end
freeze_version() click to toggle source
# File lib/chef/cookbook_version.rb, line 118
def freeze_version
  @frozen = true
end
frozen_version?() click to toggle source

Indicates if this version is frozen or not. Freezing a coobkook version indicates that a new cookbook with the same name and version number shoule

# File lib/chef/cookbook_version.rb, line 114
def frozen_version?
  @frozen
end
full_name() click to toggle source
# File lib/chef/cookbook_version.rb, line 127
def full_name
  "#{name}-#{version}"
end
fully_qualified_recipe_names() click to toggle source

Return recipe names in the form of cookbook_name::recipe_name

# File lib/chef/cookbook_version.rb, line 176
def fully_qualified_recipe_names
  files_for("recipes").inject([]) do |memo, recipe|
    rname = recipe[:name].split("/")[1]
    rname = File.basename(rname, ".rb")
    memo << "#{name}::#{rname}"
    memo
  end
end
has_cookbook_file_for_node?(node, cookbook_filename) click to toggle source

Query whether a cookbook_file file cookbook_filename is available. File specificity for the given node is obeyed in the lookup.

# File lib/chef/cookbook_version.rb, line 215
def has_cookbook_file_for_node?(node, cookbook_filename)
  !!find_preferred_manifest_record(node, :files, cookbook_filename)
end
has_template_for_node?(node, template_filename) click to toggle source

Query whether a template file template_filename is available. File specificity for the given node is obeyed in the lookup.

# File lib/chef/cookbook_version.rb, line 209
def has_template_for_node?(node, template_filename)
  !!find_preferred_manifest_record(node, :templates, template_filename)
end
load_recipe(recipe_name, run_context) click to toggle source

called from DSL

# File lib/chef/cookbook_version.rb, line 186
def load_recipe(recipe_name, run_context)
  unless recipe_filenames_by_name.key?(recipe_name)
    raise Chef::Exceptions::RecipeNotFound, "could not find recipe #{recipe_name} for cookbook #{name}"
  end

  Chef::Log.trace("Found recipe #{recipe_name} in cookbook #{name}")
  recipe = Chef::Recipe.new(name, recipe_name, run_context)
  recipe_filename = recipe_filenames_by_name[recipe_name]

  unless recipe_filename
    raise Chef::Exceptions::RecipeNotFound, "could not find #{recipe_name} files for cookbook #{name}"
  end

  recipe.from_file(recipe_filename)
  recipe
end
manifest() click to toggle source
# File lib/chef/cookbook_version.rb, line 157
def manifest
  cookbook_manifest.manifest
end
manifest=(new_manifest) click to toggle source
# File lib/chef/cookbook_version.rb, line 161
def manifest=(new_manifest)
  cookbook_manifest.update_from(new_manifest)
end
manifest_records_by_path() click to toggle source
# File lib/chef/cookbook_version.rb, line 171
def manifest_records_by_path
  cookbook_manifest.manifest_records_by_path
end
metadata=(metadata) click to toggle source
# File lib/chef/cookbook_version.rb, line 152
def metadata=(metadata)
  @metadata = metadata
  @metadata.recipes_from_cookbook_version(self)
end
metadata_json_file() click to toggle source
# File lib/chef/cookbook_version.rb, line 434
def metadata_json_file
  File.join(root_paths[0], "metadata.json")
end
metadata_rb_file() click to toggle source
# File lib/chef/cookbook_version.rb, line 438
def metadata_rb_file
  File.join(root_paths[0], "metadata.rb")
end
preferred_filename_on_disk_location(node, segment, filename, current_filepath = nil) click to toggle source
# File lib/chef/cookbook_version.rb, line 271
def preferred_filename_on_disk_location(node, segment, filename, current_filepath = nil)
  manifest_record = preferred_manifest_record(node, segment, filename)
  if current_filepath && (manifest_record["checksum"] == self.class.checksum_cookbook_file(current_filepath))
    nil
  else
    file_vendor.get_filename(manifest_record["path"])
  end
end
preferred_manifest_record(node, segment, filename) click to toggle source

Determine the most specific manifest record for the given segment/filename, given information in the node. Throws FileNotFound if there is no such segment and filename in the manifest.

A manifest record is a Mash that follows the following form: {

:name => "example.rb",
:path => "files/default/example.rb",
:specificity => "default",
:checksum => "1234"

}

# File lib/chef/cookbook_version.rb, line 231
def preferred_manifest_record(node, segment, filename)
  found_pref = find_preferred_manifest_record(node, segment, filename)
  if found_pref
    manifest_records_by_path[found_pref]
  else
    if segment == :files || segment == :templates
      error_message = "Cookbook '#{name}' (#{version}) does not contain a file at any of these locations:\n"
      error_locations = if filename.is_a?(Array)
                          filename.map { |name| "  #{File.join(segment.to_s, name)}" }
                        else
                          [
                            "  #{segment}/host-#{node[:fqdn]}/#{filename}",
                            "  #{segment}/#{node[:platform]}-#{node[:platform_version]}/#{filename}",
                            "  #{segment}/#{node[:platform]}/#{filename}",
                            "  #{segment}/default/#{filename}",
                            "  #{segment}/#{filename}",
                          ]
                        end
      error_message << error_locations.join("\n")
      existing_files = segment_filenames(segment)
      # Strip the root_dir prefix off all files for readability
      pretty_existing_files = existing_files.map do |path|
        if root_dir
          path[root_dir.length + 1..-1]
        else
          path
        end
      end
      # Show the files that the cookbook does have. If the user made a typo,
      # hopefully they'll see it here.
      unless pretty_existing_files.empty?
        error_message << "\n\nThis cookbook _does_ contain: ['#{pretty_existing_files.join("','")}']"
      end
      raise Chef::Exceptions::FileNotFound, error_message
    else
      raise Chef::Exceptions::FileNotFound, "cookbook #{name} does not contain file #{segment}/#{filename}"
    end
  end
end
preferred_manifest_records_for_directory(node, segment, dirname) click to toggle source

Determine the manifest records from the most specific directory for the given node. See preferred_manifest_record for a description of entries of the returned Array.

# File lib/chef/cookbook_version.rb, line 320
def preferred_manifest_records_for_directory(node, segment, dirname)
  preferences = preferences_for_path(node, segment, dirname)
  records_by_pref = Hash.new
  preferences.each { |pref| records_by_pref[pref] = Array.new }

  files_for(segment).each do |manifest_record|
    manifest_record_path = manifest_record[:path]

    # extract the preference part from the path.
    if manifest_record_path =~ /(#{Regexp.escape(segment.to_s)}\/[^\/]+\/#{Regexp.escape(dirname)})\/.+$/
        # Note the specificy_dirname includes the segment and
        # dirname argument as above, which is what
        # preferences_for_path returns. It could be
        # "files/ubuntu-9.10/dirname", for example.
      specificity_dirname = $1

      # Record the specificity_dirname only if it's in the list of
      # valid preferences
      if records_by_pref[specificity_dirname]
        records_by_pref[specificity_dirname] << manifest_record
      end
    end
  end

  best_pref = preferences.find { |pref| !records_by_pref[pref].empty? }

  raise Chef::Exceptions::FileNotFound, "cookbook #{name} (#{version}) has no directory #{segment}/default/#{dirname}" unless best_pref

  records_by_pref[best_pref]
end
recipe_filenames_by_name() click to toggle source
# File lib/chef/cookbook_version.rb, line 140
def recipe_filenames_by_name
  @recipe_filenames_by_name ||= begin
    name_map = filenames_by_name(files_for("recipes"))
    root_alias = cookbook_manifest.root_files.find { |record| record[:name] == "root_files/recipe.rb" }
    if root_alias
      Chef::Log.error("Cookbook #{name} contains both recipe.rb and and recipes/default.rb, ignoring recipes/default.rb") if name_map["default"]
      name_map["default"] = root_alias[:full_path]
    end
    name_map
  end
end
relative_filenames_in_preferred_directory(node, segment, dirname) click to toggle source
# File lib/chef/cookbook_version.rb, line 280
def relative_filenames_in_preferred_directory(node, segment, dirname)
  preferences = preferences_for_path(node, segment, dirname)
  filenames_by_pref = Hash.new
  preferences.each { |pref| filenames_by_pref[pref] = Array.new }

  files_for(segment).each do |manifest_record|
    manifest_record_path = manifest_record[:path]

    # find the NON SPECIFIC filenames, but prefer them by filespecificity.
    # For example, if we have a file:
    # 'files/default/somedir/somefile.conf' we only keep
    # 'somedir/somefile.conf'. If there is also
    # 'files/$hostspecific/somedir/otherfiles' that matches the requested
    # hostname specificity, that directory will win, as it is more specific.
    #
    # This is clearly ugly b/c the use case is for remote directory, where
    # we're just going to make cookbook_files out of these and make the
    # cookbook find them by filespecificity again. but it's the shortest
    # path to "success" for now.
    if manifest_record_path =~ /(#{Regexp.escape(segment.to_s)}\/[^\/]*\/?#{Regexp.escape(dirname)})\/.+$/
      specificity_dirname = $1
      non_specific_path = manifest_record_path[/#{Regexp.escape(segment.to_s)}\/[^\/]*\/?#{Regexp.escape(dirname)}\/(.+)$/, 1]
      # Record the specificity_dirname only if it's in the list of
      # valid preferences
      if filenames_by_pref[specificity_dirname]
        filenames_by_pref[specificity_dirname] << non_specific_path
      end
    end
  end

  best_pref = preferences.find { |pref| !filenames_by_pref[pref].empty? }

  raise Chef::Exceptions::FileNotFound, "cookbook #{name} has no directory #{segment}/default/#{dirname}" unless best_pref

  filenames_by_pref[best_pref]
end
reload_metadata!() click to toggle source
# File lib/chef/cookbook_version.rb, line 442
def reload_metadata!
  if File.exists?(metadata_json_file)
    metadata.from_json(IO.read(metadata_json_file))
  end
end
root_dir() click to toggle source

The first root path is the primary cookbook dir, from which metadata is loaded

# File lib/chef/cookbook_version.rb, line 68
def root_dir
  root_paths[0]
end
segment_filenames(segment) click to toggle source
# File lib/chef/cookbook_version.rb, line 203
def segment_filenames(segment)
  files_for(segment).map { |f| f["full_path"] || File.join(root_dir, f["path"]) }
end
version() click to toggle source
# File lib/chef/cookbook_version.rb, line 107
def version
  metadata.version
end
version=(new_version) click to toggle source
# File lib/chef/cookbook_version.rb, line 122
def version=(new_version)
  cookbook_manifest.reset!
  metadata.version(new_version)
end

Private Instance Methods

file_vendor() click to toggle source
# File lib/chef/cookbook_version.rb, line 536
def file_vendor
  unless @file_vendor
    @file_vendor = Chef::Cookbook::FileVendor.create_from_manifest(cookbook_manifest)
  end
  @file_vendor
end
filenames_by_name(records) click to toggle source

For each manifest record, produce a mapping of base filename (i.e. recipe name or attribute file) to on disk location

# File lib/chef/cookbook_version.rb, line 532
def filenames_by_name(records)
  records.select { |record| record[:name] =~ /\.rb$/ }.inject({}) { |memo, record| memo[File.basename(record[:name], ".rb")] = record[:full_path]; memo }
end
find_preferred_manifest_record(node, segment, filename) click to toggle source
# File lib/chef/cookbook_version.rb, line 517
def find_preferred_manifest_record(node, segment, filename)
  preferences = preferences_for_path(node, segment, filename)

  # in order of prefernce, look for the filename in the manifest
  preferences.find { |preferred_filename| manifest_records_by_path[preferred_filename] }
end
preferences_for_path(node, segment, path) click to toggle source

Given a node, segment and path (filename or directory name), return the priority-ordered list of preference locations to look.

# File lib/chef/cookbook_version.rb, line 354
def preferences_for_path(node, segment, path)
  # only files and templates can be platform-specific
  if segment.to_sym == :files || segment.to_sym == :templates
    relative_search_path = if path.is_a?(Array)
                             path
                           else
                             begin
                               platform, version = Chef::Platform.find_platform_and_version(node)
                             rescue ArgumentError => e
                               # Skip platform/version if they were not found by find_platform_and_version
                               if e.message =~ /Cannot find a (?:platform|version)/
                                 platform = "/unknown_platform/"
                                 version = "/unknown_platform_version/"
                               else
                                 raise
                               end
                             end

                             fqdn = node[:fqdn]

                             # Break version into components, eg: "5.7.1" => [ "5.7.1", "5.7", "5" ]
                             search_versions = []
                             parts = version.to_s.split(".")

                             parts.size.times do
                               search_versions << parts.join(".")
                               parts.pop
                             end

                             # Most specific to least specific places to find the path
                             search_path = [ File.join("host-#{fqdn}", path) ]
                             search_versions.each do |v|
                               search_path << File.join("#{platform}-#{v}", path)
                             end
                             search_path << File.join(platform.to_s, path)
                             search_path << File.join("default", path)
                             search_path << path

                             search_path
                           end
    relative_search_path.map { |relative_path| File.join(segment.to_s, relative_path) }
  else
    if segment.to_sym == :root_files
      [path]
    else
      [File.join(segment, path)]
    end
  end
end
relative_paths_by_name(records) click to toggle source

For each manifest record, produce a mapping of base filename (i.e. recipe name or attribute file) to on disk location

# File lib/chef/cookbook_version.rb, line 526
def relative_paths_by_name(records)
  records.select { |record| record[:name] =~ /\.rb$/ }.inject({}) { |memo, record| memo[File.basename(record[:name], ".rb")] = record[:path]; memo }
end