File Manager
Viewing File: chef_mirror.rb
require_relative "../../cheffish"
require_relative "../../cheffish/base_resource"
require "chef/chef_fs/file_pattern"
require "chef/chef_fs/file_system"
require "chef/chef_fs/parallelizer"
require "chef/chef_fs/file_system/chef_server/chef_server_root_dir"
require "chef/chef_fs/file_system/repository/chef_repository_file_system_root_dir"
class Chef
class Resource
class ChefMirror < Cheffish::BaseResource
provides :chef_mirror
# Path of the data to mirror, e.g. nodes, nodes/*, nodes/mynode,
# */*, **, roles/base, data/secrets, cookbooks/apache2, etc.
property :path, String, name_property: true
# Local path. Can be a string (top level of repository) or hash
# (:chef_repo_path, :node_path, etc.)
# If neither chef_repo_path nor versioned_cookbooks are set, they default to their
# Chef::Config values. If chef_repo_path is set but versioned_cookbooks is not,
# versioned_cookbooks defaults to true.
property :chef_repo_path, [ String, Hash ]
# Whether the repo path should contain cookbooks with versioned names,
# i.e. cookbooks/mysql-1.0.0, cookbooks/mysql-1.2.0, etc.
# Defaults to true if chef_repo_path is specified, or to Chef::Config.versioned_cookbooks otherwise.
property :versioned_cookbooks, [TrueClass, FalseClass]
# Whether to purge deleted things: if we do not have cookbooks/x locally and we
# *do* have cookbooks/x remotely, then :upload with purge will delete it.
# Defaults to false.
property :purge, [TrueClass, FalseClass]
# Whether to freeze cookbooks on upload
property :freeze_on_upload, [TrueClass, FalseClass]
# `freeze` is an already-existing instance method on Object, so we can't use it or we'll throw
# a deprecation warning. `freeze` has been renamed to `freeze_on_upload` and this method
# is here to log a deprecation warning.
def freeze(arg = nil)
Chef::Log.warn("Property `freeze` on the `chef_mirror` resource has changed to `freeze_on_upload`." \
"Please use `freeze_on_upload` instead. This will raise an exception in a future version of the cheffish gem.")
set_or_return(
:freeze_on_upload,
arg,
kind_of: [TrueClass, FalseClass]
)
end
# If this is true, only new files will be copied. File contents will not be
# diffed, so changed files will never be uploaded.
property :no_diff, [TrueClass, FalseClass]
# Number of parallel threads to list/upload/download with. Defaults to 10.
property :concurrency, Integer, default: 10, desired_state: false
action :upload do
with_modified_config do
copy_to(local_fs, remote_fs)
end
end
action :download do
with_modified_config do
copy_to(remote_fs, local_fs)
end
end
action_class.class_eval do
def with_modified_config
# pre-Chef-12 ChefFS reads versioned_cookbooks out of Chef::Config instead of
# taking it as an input, so we need to modify it for the duration of copy_to
@old_versioned_cookbooks = Chef::Config.versioned_cookbooks
# If versioned_cookbooks is explicitly set, set it.
if !new_resource.versioned_cookbooks.nil?
Chef::Config.versioned_cookbooks = new_resource.versioned_cookbooks
# If new_resource.chef_repo_path is set, versioned_cookbooks defaults to true.
# Otherwise, it stays at its current Chef::Config value.
elsif new_resource.chef_repo_path
Chef::Config.versioned_cookbooks = true
end
begin
yield
ensure
Chef::Config.versioned_cookbooks = @old_versioned_cookbooks
end
end
def copy_to(src_root, dest_root)
if new_resource.concurrency <= 0
raise "chef_mirror.concurrency must be above 0! Was set to #{new_resource.concurrency}"
end
# Honor concurrency
Chef::ChefFS::Parallelizer.threads = new_resource.concurrency - 1
# We don't let the user pass absolute paths; we want to reserve those for
# multi-org support (/organizations/foo).
if new_resource.path[0] == "/"
raise "Absolute paths in chef_mirror not yet supported."
end
# Copy!
path = Chef::ChefFS::FilePattern.new("/#{new_resource.path}")
ui = CopyListener.new(self)
error = Chef::ChefFS::FileSystem.copy_to(path, src_root, dest_root, nil, options, ui, proc { |p| p.path })
if error
raise "Errors while copying:#{ui.errors.map { |e| "#{e}\n" }.join("")}"
end
end
def local_fs
# If chef_repo_path is set to a string, put it in the form it usually is in
# chef config (:chef_repo_path, :node_path, etc.)
path_config = new_resource.chef_repo_path
if path_config.is_a?(Hash)
chef_repo_path = path_config.delete(:chef_repo_path)
elsif path_config
chef_repo_path = path_config
path_config = {}
else
chef_repo_path = Chef::Config.chef_repo_path
path_config = Chef::Config
end
chef_repo_path = Array(chef_repo_path).flatten
# Go through the expected object paths and figure out the local paths for each.
case repo_mode
when "hosted_everything"
object_names = %w{acls clients cookbooks containers data_bags environments groups nodes roles}
else
object_names = %w{clients cookbooks data_bags environments nodes roles users}
end
object_paths = {}
object_names.each do |object_name|
variable_name = "#{object_name[0..-2]}_path" # cookbooks -> cookbook_path
if path_config[variable_name.to_sym]
paths = Array(path_config[variable_name.to_sym]).flatten
else
paths = chef_repo_path.map { |path| ::File.join(path, object_name) }
end
object_paths[object_name] = paths.map { |path| ::File.expand_path(path) }
end
# Set up the root dir
Chef::ChefFS::FileSystem::Repository::ChefRepositoryFileSystemRootDir.new(object_paths)
end
def remote_fs
config = {
chef_server_url: new_resource.chef_server[:chef_server_url],
node_name: new_resource.chef_server[:options][:client_name],
client_key: new_resource.chef_server[:options][:signing_key_filename],
repo_mode: repo_mode,
versioned_cookbooks: Chef::Config.versioned_cookbooks,
}
Chef::ChefFS::FileSystem::ChefServer::ChefServerRootDir.new("remote", config)
end
def repo_mode
new_resource.chef_server[:chef_server_url] =~ %r{/organizations/} ? "hosted_everything" : "everything"
end
def options
result = {
purge: new_resource.purge,
freeze: new_resource.freeze_on_upload,
diff: new_resource.no_diff,
dry_run: whyrun_mode?,
}
result[:diff] = !result[:diff]
result[:repo_mode] = repo_mode
result[:concurrency] = new_resource.concurrency if new_resource.concurrency
result
end
def load_current_resource; end
class CopyListener
def initialize(mirror)
@mirror = mirror
@errors = []
end
attr_reader :mirror
attr_reader :errors
# TODO output is not *always* indicative of a change. We may want to give
# ChefFS the ability to tell us that info. For now though, assuming any output
# means change is pretty damn close to the truth.
def output(str)
mirror.converge_by str do
end
end
def warn(str)
mirror.converge_by "WARNING: #{str}" do
end
end
def error(str)
mirror.converge_by "ERROR: #{str}" do
end
@errors << str
end
end
end
end
end
end