Class: Chef::CookbookVersion

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/chef/cookbook_version.rb

Overview

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. – TODO: timh/cw: 5-24-2010: mutators for files (e.g., recipe_filenames=, recipe_filenames.insert) should dirty the manifest so it gets regenerated.

Constant Summary collapse

COOKBOOK_SEGMENTS =
[ :resources, :providers, :recipes, :libraries, :attributes, :files, :templates, :root_files ]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name) ⇒ CookbookVersion

Creates a new Chef::CookbookVersion object.

Returns

object<Chef::CookbookVersion>

Duh. :)



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/chef/cookbook_version.rb', line 176

def initialize(name)
  @name = name
  @frozen = false
  @attribute_filenames = Array.new
  @definition_filenames = Array.new
  @template_filenames = Array.new
  @file_filenames = Array.new
  @recipe_filenames = Array.new
  @recipe_filenames_by_name = Hash.new
  @library_filenames = Array.new
  @resource_filenames = Array.new
  @provider_filenames = Array.new
   = Array.new
  @root_dir = nil
  @root_filenames = Array.new
  @status = :ready
  @manifest = nil
  @file_vendor = nil
   = Chef::Cookbook::.new
end

Instance Attribute Details

#attribute_filenamesObject Also known as: attribute_files

attribute_filenames also has a setter that has non-default functionality.



111
112
113
# File 'lib/chef/cookbook_version.rb', line 111

def attribute_filenames
  @attribute_filenames
end

#attribute_filenames_by_short_filenameObject (readonly)

Returns the value of attribute attribute_filenames_by_short_filename.



118
119
120
# File 'lib/chef/cookbook_version.rb', line 118

def attribute_filenames_by_short_filename
  @attribute_filenames_by_short_filename
end

#definition_filenamesObject

Returns the value of attribute definition_filenames.



97
98
99
# File 'lib/chef/cookbook_version.rb', line 97

def definition_filenames
  @definition_filenames
end

#file_filenamesObject

Returns the value of attribute file_filenames.



99
100
101
# File 'lib/chef/cookbook_version.rb', line 99

def file_filenames
  @file_filenames
end

#library_filenamesObject

Returns the value of attribute library_filenames.



100
101
102
# File 'lib/chef/cookbook_version.rb', line 100

def library_filenames
  @library_filenames
end

#metadataObject

Returns the value of attribute metadata.



105
106
107
# File 'lib/chef/cookbook_version.rb', line 105

def 
  
end

#metadata_filenamesObject

Returns the value of attribute metadata_filenames.



106
107
108
# File 'lib/chef/cookbook_version.rb', line 106

def 
  
end

#nameObject

Returns the value of attribute name.



104
105
106
# File 'lib/chef/cookbook_version.rb', line 104

def name
  @name
end

#provider_filenamesObject

Returns the value of attribute provider_filenames.



102
103
104
# File 'lib/chef/cookbook_version.rb', line 102

def provider_filenames
  @provider_filenames
end

#recipe_filenamesObject Also known as: recipe_files

recipe_filenames also has a setter that has non-default functionality.



115
116
117
# File 'lib/chef/cookbook_version.rb', line 115

def recipe_filenames
  @recipe_filenames
end

#recipe_filenames_by_nameObject (readonly)

Returns the value of attribute recipe_filenames_by_name.



117
118
119
# File 'lib/chef/cookbook_version.rb', line 117

def recipe_filenames_by_name
  @recipe_filenames_by_name
end

#resource_filenamesObject

Returns the value of attribute resource_filenames.



101
102
103
# File 'lib/chef/cookbook_version.rb', line 101

def resource_filenames
  @resource_filenames
end

#root_dirObject

Returns the value of attribute root_dir.



96
97
98
# File 'lib/chef/cookbook_version.rb', line 96

def root_dir
  @root_dir
end

#root_filenamesObject

Returns the value of attribute root_filenames.



103
104
105
# File 'lib/chef/cookbook_version.rb', line 103

def root_filenames
  @root_filenames
end

#statusObject

Returns the value of attribute status.



107
108
109
# File 'lib/chef/cookbook_version.rb', line 107

def status
  @status
end

#template_filenamesObject

Returns the value of attribute template_filenames.



98
99
100
# File 'lib/chef/cookbook_version.rb', line 98

def template_filenames
  @template_filenames
end

Class Method Details

.cacheObject



141
142
143
# File 'lib/chef/cookbook_version.rb', line 141

def self.cache
  Chef::FileCache
end

.checksum_cookbook_file(filepath) ⇒ Object

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



122
123
124
125
126
127
# File 'lib/chef/cookbook_version.rb', line 122

def self.checksum_cookbook_file(filepath)
  Chef::ChecksumCache.generate_md5_checksum_for_file(filepath)
rescue Errno::ENOENT
  Chef::Log.debug("File #{filepath} does not exist, so there is no checksum to generate")
  nil
end

.cleanup_file_cacheObject



164
165
# File 'lib/chef/cookbook_version.rb', line 164

def self.cleanup_file_cache
end

.clear_obsoleted_cookbooks(cookbook_hash) ⇒ Object

Iterates over cached cookbooks’ files, removing files belonging to cookbooks that don’t appear in cookbook_hash



153
154
155
156
157
158
159
160
161
162
# File 'lib/chef/cookbook_version.rb', line 153

def self.clear_obsoleted_cookbooks(cookbook_hash)
  # Remove all cookbooks no longer relevant to this node
  cache.find(File.join(%w{cookbooks ** *})).each do |cache_file|
    cache_file =~ /^cookbooks\/([^\/]+)\//
    unless cookbook_hash.has_key?($1)
      Chef::Log.info("Removing #{cache_file} from the cache; its cookbook is no longer needed on this client.")
      cache.delete(cache_file)
    end
  end
end

.json_create(o) ⇒ Object



540
541
542
543
544
545
546
547
548
549
550
551
# File 'lib/chef/cookbook_version.rb', line 540

def self.json_create(o)
  cookbook_version = new(o["cookbook_name"])
  # We want the Chef::Cookbook::Metadata object to always be inflated
  cookbook_version. = Chef::Cookbook::.from_hash(o["metadata"])
  cookbook_version.manifest = o

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

  cookbook_version.freeze_version if o["frozen?"]
  cookbook_version
end

.reset_cache_validityObject



137
138
139
# File 'lib/chef/cookbook_version.rb', line 137

def self.reset_cache_validity
  @valid_cache_entries = nil
end

.valid_cache_entriesObject

Keep track of the filenames that we use in both eager cookbook downloading (during sync_cookbooks) and lazy (during the run itself, through FileVendor). After the run is over, clean up the cache.



133
134
135
# File 'lib/chef/cookbook_version.rb', line 133

def self.valid_cache_entries
  @valid_cache_entries ||= {}
end

Instance Method Details

#<=>(o) ⇒ Object



570
571
572
573
574
575
576
# File 'lib/chef/cookbook_version.rb', line 570

def <=>(o)
  raise Chef::Exceptions::CookbookVersionNameMismatch if self.name != o.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(self.version) <=> Chef::Version.new(o.version)
end

#checksumsObject

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



268
269
270
271
272
273
# File 'lib/chef/cookbook_version.rb', line 268

def checksums
  unless @checksums
    generate_manifest
  end
  @checksums
end

#freeze_versionObject



208
209
210
# File 'lib/chef/cookbook_version.rb', line 208

def freeze_version
  @frozen = true
end

#frozen_version?Boolean

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



204
205
206
# File 'lib/chef/cookbook_version.rb', line 204

def frozen_version?
  @frozen
end

#full_nameObject



280
281
282
# File 'lib/chef/cookbook_version.rb', line 280

def full_name
  "#{name}-#{version}"
end

#fully_qualified_recipe_namesObject

Return recipe names in the form of cookbook_name::recipe_name



295
296
297
298
299
300
301
# File 'lib/chef/cookbook_version.rb', line 295

def fully_qualified_recipe_names
  results = Array.new
  recipe_filenames_by_name.each_key do |rname|
    results << "#{name}::#{rname}"
  end
  results
end

#generate_manifest_with_urls(&url_generator) ⇒ Object



553
554
555
556
557
558
559
560
561
562
563
564
# File 'lib/chef/cookbook_version.rb', line 553

def generate_manifest_with_urls(&url_generator)
  rendered_manifest = manifest.dup
  COOKBOOK_SEGMENTS.each do |segment|
    if rendered_manifest.has_key?(segment)
      rendered_manifest[segment].each do |manifest_record|
        url_options = { :cookbook_name => name.to_s, :cookbook_version => version, :checksum => manifest_record["checksum"] }
        manifest_record["url"] = url_generator.call(url_options)
      end
    end
  end
  rendered_manifest
end

#load_recipe(recipe_name, run_context) ⇒ Object

called from DSL



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/chef/cookbook_version.rb', line 314

def load_recipe(recipe_name, run_context)
  unless recipe_filenames_by_name.has_key?(recipe_name)
    raise ArgumentError, "Cannot find a recipe matching #{recipe_name} in cookbook #{name}"
  end

  Chef::Log.debug("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 #{recipe_name} for cookbook #{name}"
  end

  recipe.from_file(recipe_filename)
  recipe
end

#manifestObject

A manifest is a Mash that maps segment names to arrays of manifest records (see #preferred_manifest_record for format of manifest records), as well as describing cookbook metadata. The manifest follows a form like the following:

{
  :cookbook_name = "apache2",
  :version = "1.0",
  :name = "Apache 2"
  :metadata = ???TODO: timh/cw: 5-24-2010: describe this format,

  :files => [
    {
      :name => "afile.rb",
      :path => "files/ubuntu-9.10/afile.rb",
      :checksum => "2222",
      :specificity => "ubuntu-9.10"
    },
  ],
  :templates => [ manifest_record1, ... ],
  ...
}


239
240
241
242
243
244
# File 'lib/chef/cookbook_version.rb', line 239

def manifest
  unless @manifest
    generate_manifest
  end
  @manifest
end

#manifest=(new_manifest) ⇒ Object



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/chef/cookbook_version.rb', line 246

def manifest=(new_manifest)
  @manifest = Mash.new new_manifest
  @checksums = extract_checksums_from_manifest(@manifest)
  @manifest_records_by_path = extract_manifest_records_by_path(@manifest)

  COOKBOOK_SEGMENTS.each do |segment|
    next unless @manifest.has_key?(segment)
    filenames = @manifest[segment].map{|manifest_record| manifest_record['name']}

    if segment == :recipes
      self.recipe_filenames = filenames
    elsif segment == :attributes
      self.attribute_filenames = filenames
    else
      segment_filenames(segment).clear
      filenames.each { |filename| segment_filenames(segment) << filename }
    end
  end
end

#manifest_records_by_pathObject



275
276
277
278
# File 'lib/chef/cookbook_version.rb', line 275

def manifest_records_by_path
  @manifest_records_by_path || generate_manifest
  @manifest_records_by_path
end

#metadata_rb_fileObject



566
567
568
# File 'lib/chef/cookbook_version.rb', line 566

def 
  File.join(root_dir, "metadata.rb")
end

#preferred_filename_on_disk_location(node, segment, filename, current_filepath = nil) ⇒ Object



401
402
403
404
405
406
407
408
# File 'lib/chef/cookbook_version.rb', line 401

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) ⇒ Object

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"



368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'lib/chef/cookbook_version.rb', line 368

def preferred_manifest_record(node, segment, filename)
  preferences = preferences_for_path(node, segment, filename)

  # ensure that we generate the manifest, which will also generate
  # @manifest_records_by_path
  manifest

  # in order of prefernce, look for the filename in the manifest
  found_pref = preferences.find {|preferred_filename| @manifest_records_by_path[preferred_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 = [
        "  #{segment}/#{node[:platform]}-#{node[:platform_version]}/#{filename}",
        "  #{segment}/#{node[:platform]}/#{filename}",
        "  #{segment}/default/#{filename}",
      ]
      error_message << error_locations.join("\n")
      existing_files = segment_filenames(segment)
      # Show the files that the cookbook does have. If the user made a typo,
      # hopefully they'll see it here.
      unless existing_files.empty?
        error_message << "\n\nThis cookbook _does_ contain: ['#{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) ⇒ Object

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.



451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
# File 'lib/chef/cookbook_version.rb', line 451

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 }

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

#relative_filenames_in_preferred_directory(node, segment, dirname) ⇒ Object



410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/chef/cookbook_version.rb', line 410

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 }

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

#segment_filenames(segment) ⇒ Object



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/chef/cookbook_version.rb', line 331

def segment_filenames(segment)
  unless COOKBOOK_SEGMENTS.include?(segment)
    raise ArgumentError, "invalid segment #{segment}: must be one of #{COOKBOOK_SEGMENTS.join(', ')}"
  end

  case segment.to_sym
  when :resources
    @resource_filenames
  when :providers
    @provider_filenames
  when :recipes
    @recipe_filenames
  when :libraries
    @library_filenames
  when :attributes
    @attribute_filenames
  when :files
    @file_filenames
  when :templates
    @template_filenames
  when :root_files
    @root_filenames
  end
end

#to_hashObject



527
528
529
530
531
532
# File 'lib/chef/cookbook_version.rb', line 527

def to_hash
  result = manifest.dup
  result['frozen?'] = frozen_version?
  result['chef_type'] = 'cookbook_version'
  result.to_hash
end

#to_json(*a) ⇒ Object



534
535
536
537
538
# File 'lib/chef/cookbook_version.rb', line 534

def to_json(*a)
  result = self.to_hash
  result['json_class'] = self.class.name
  result.to_json(*a)
end

#versionObject



197
198
199
# File 'lib/chef/cookbook_version.rb', line 197

def version
  .version
end

#version=(new_version) ⇒ Object



212
213
214
215
# File 'lib/chef/cookbook_version.rb', line 212

def version=(new_version)
  manifest["version"] = new_version
  .version(new_version)
end