File Manager
Viewing File: node.rb
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Christopher Brown (<cb@chef.io>)
# Author:: Christopher Walters (<cw@chef.io>)
# Author:: Tim Hinderliter (<tim@chef.io>)
# Copyright:: Copyright 2008-2016, Chef Software, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require "forwardable"
require "chef/config"
require "chef/nil_argument"
require "chef/mixin/params_validate"
require "chef/mixin/from_file"
require "chef/mixin/deep_merge"
require "chef/dsl/include_attribute"
require "chef/dsl/universal"
require "chef/environment"
require "chef/server_api"
require "chef/run_list"
require "chef/node/attribute"
require "chef/mash"
require "chef/json_compat"
require "chef/search/query"
require "chef/whitelist"
class Chef
class Node
extend Forwardable
def_delegators :attributes, :keys, :each_key, :each_value, :key?, :has_key?
def_delegators :attributes, :rm, :rm_default, :rm_normal, :rm_override
def_delegators :attributes, :default!, :normal!, :override!, :force_default!, :force_override!
def_delegators :attributes, :default_unless, :normal_unless, :override_unless, :set_unless
def_delegators :attributes, :read, :read!, :write, :write!, :unlink, :unlink!
attr_accessor :recipe_list, :run_state, :override_runlist
attr_accessor :chef_server_rest
# RunContext will set itself as run_context via this setter when
# initialized. This is needed so DSL::IncludeAttribute (in particular,
# #include_recipe) can access the run_context to determine if an attributes
# file has been seen yet.
#--
# TODO: This is a pretty ugly way to solve that problem.
attr_accessor :run_context
include Chef::Mixin::FromFile
include Chef::DSL::IncludeAttribute
include Chef::DSL::Universal
include Chef::Mixin::ParamsValidate
NULL_ARG = Object.new
# Create a new Chef::Node object.
def initialize(chef_server_rest: nil)
@chef_server_rest = chef_server_rest
@name = nil
@chef_environment = "_default"
@primary_runlist = Chef::RunList.new
@override_runlist = Chef::RunList.new
@policy_name = nil
@policy_group = nil
@attributes = Chef::Node::Attribute.new({}, {}, {}, {}, self)
@run_state = {}
end
# after the run_context has been set on the node, go through the cookbook_collection
# and setup the node[:cookbooks] attribute so that it is published in the node object
def set_cookbook_attribute
return unless run_context.cookbook_collection
run_context.cookbook_collection.each do |cookbook_name, cookbook|
automatic_attrs[:cookbooks][cookbook_name][:version] = cookbook.version
end
end
# Used by DSL
def node
self
end
def chef_server_rest
# for saving node data we use validate_utf8: false which will not
# raise an exception on bad utf8 data, but will replace the bad
# characters and render valid JSON.
@chef_server_rest ||= Chef::ServerAPI.new(
Chef::Config[:chef_server_url],
client_name: Chef::Config[:node_name],
signing_key_filename: Chef::Config[:client_key],
validate_utf8: false
)
end
# Set the name of this Node, or return the current name.
def name(arg = nil)
if !arg.nil?
validate(
{ :name => arg },
{ :name => { :kind_of => String,
:cannot_be => :blank,
:regex => /^[\-[:alnum:]_:.]+$/ },
})
@name = arg
else
@name
end
end
def chef_environment(arg = nil)
set_or_return(
:chef_environment,
arg,
{ :regex => /^[\-[:alnum:]_]+$/, :kind_of => String }
)
end
def chef_environment=(environment)
chef_environment(environment)
end
alias :environment :chef_environment
# The `policy_name` for this node. Setting this to a non-nil value will
# enable policyfile mode when `chef-client` is run. If set in the config
# file or in node json, running `chef-client` will update this value.
#
# @see Chef::PolicyBuilder::Dynamic
# @see Chef::PolicyBuilder::Policyfile
#
# @param arg [String] the new policy_name value
# @return [String] the current policy_name, or the one you just set
def policy_name(arg = NULL_ARG)
return @policy_name if arg.equal?(NULL_ARG)
validate({ policy_name: arg }, { policy_name: { kind_of: [ String, NilClass ], regex: /^[\-:.[:alnum:]_]+$/ } })
@policy_name = arg
end
# A "non-DSL-style" setter for `policy_name`
#
# @see #policy_name
def policy_name=(policy_name)
policy_name(policy_name)
end
# The `policy_group` for this node. Setting this to a non-nil value will
# enable policyfile mode when `chef-client` is run. If set in the config
# file or in node json, running `chef-client` will update this value.
#
# @see Chef::PolicyBuilder::Dynamic
# @see Chef::PolicyBuilder::Policyfile
#
# @param arg [String] the new policy_group value
# @return [String] the current policy_group, or the one you just set
def policy_group(arg = NULL_ARG)
return @policy_group if arg.equal?(NULL_ARG)
validate({ policy_group: arg }, { policy_group: { kind_of: [ String, NilClass ], regex: /^[\-:.[:alnum:]_]+$/ } })
@policy_group = arg
end
# A "non-DSL-style" setter for `policy_group`
#
# @see #policy_group
def policy_group=(policy_group)
policy_group(policy_group)
end
def attributes
@attributes
end
alias :attribute :attributes
alias :construct_attributes :attributes
# Return an attribute of this node. Returns nil if the attribute is not found.
def [](attrib)
attributes[attrib]
end
# Set a normal attribute of this node, but auto-vivify any Mashes that
# might be missing
def normal
attributes.normal
end
def set
Chef.deprecated(:attributes, "node.set is deprecated and will be removed in Chef 14, please use node.default/node.override (or node.normal only if you really need persistence)")
normal
end
# Set a default of this node, but auto-vivify any Mashes that might
# be missing
def default
attributes.default
end
# Set an override attribute of this node, but auto-vivify any Mashes that
# might be missing
def override
attributes.override
end
alias :override_attrs :override
alias :default_attrs :default
alias :normal_attrs :normal
def override_attrs=(new_values)
attributes.override = new_values
end
def default_attrs=(new_values)
attributes.default = new_values
end
def normal_attrs=(new_values)
attributes.normal = new_values
end
def automatic_attrs
attributes.automatic
end
def automatic_attrs=(new_values)
attributes.automatic = new_values
end
# Return true if this Node has a given attribute, false if not. Takes either a symbol or
# a string.
#
# Only works on the top level. Preferred way is to use the normal [] style
# lookup and call attribute?()
def attribute?(attrib)
attributes.attribute?(attrib)
end
# Yield each key of the top level to the block.
def each(&block)
attributes.each(&block)
end
# Iterates over each attribute, passing the attribute and value to the block.
def each_attribute(&block)
attributes.each_attribute(&block)
end
# Only works for attribute fetches, setting is no longer supported
# XXX: this should be deprecated
def method_missing(method, *args, &block)
attributes.public_send(method, *args, &block)
end
# Fix respond_to + method so that it works with method_missing delegation
def respond_to_missing?(method, include_private = false)
attributes.respond_to?(method, false)
end
# Returns true if this Node expects a given recipe, false if not.
#
# First, the run list is consulted to see whether the recipe is
# explicitly included. If it's not there, it looks in
# `node[:recipes]`, which is populated when the run_list is expanded
#
# NOTE: It's used by cookbook authors
def recipe?(recipe_name)
run_list.include?(recipe_name) || Array(self[:recipes]).include?(recipe_name)
end
# used by include_recipe to add recipes to the expanded run_list to be
# saved back to the node and be searchable
def loaded_recipe(cookbook, recipe)
fully_qualified_recipe = "#{cookbook}::#{recipe}"
automatic_attrs[:recipes] << fully_qualified_recipe unless Array(self[:recipes]).include?(fully_qualified_recipe)
end
# Returns true if this Node expects a given role, false if not.
def role?(role_name)
run_list.include?("role[#{role_name}]")
end
def primary_runlist
@primary_runlist
end
def override_runlist(*args)
args.length > 0 ? @override_runlist.reset!(args) : @override_runlist
end
def select_run_list
@override_runlist.empty? ? @primary_runlist : @override_runlist
end
# Returns an Array of roles and recipes, in the order they will be applied.
# If you call it with arguments, they will become the new list of roles and recipes.
def run_list(*args)
rl = select_run_list
args.length > 0 ? rl.reset!(args) : rl
end
def run_list=(list)
rl = select_run_list
rl = list
end
# Returns true if this Node expects a given role, false if not.
def run_list?(item)
run_list.detect { |r| r == item } ? true : false
end
# Consume data from ohai and Attributes provided as JSON on the command line.
def consume_external_attrs(ohai_data, json_cli_attrs)
Chef::Log.debug("Extracting run list from JSON attributes provided on command line")
consume_attributes(json_cli_attrs)
self.automatic_attrs = ohai_data
platform, version = Chef::Platform.find_platform_and_version(self)
Chef::Log.debug("Platform is #{platform} version #{version}")
automatic[:platform] = platform
automatic[:platform_version] = version
end
# Consumes the combined run_list and other attributes in +attrs+
def consume_attributes(attrs)
normal_attrs_to_merge = consume_run_list(attrs)
normal_attrs_to_merge = consume_chef_environment(normal_attrs_to_merge)
Chef::Log.debug("Applying attributes from json file")
self.normal_attrs = Chef::Mixin::DeepMerge.merge(normal_attrs, normal_attrs_to_merge)
tags # make sure they're defined
end
# Lazy initializer for tags attribute
def tags
normal[:tags] = Array(normal[:tags])
normal[:tags]
end
def tag(*args)
args.each do |tag|
tags.push(tag.to_s) unless tags.include? tag.to_s
end
tags
end
# Extracts the run list from +attrs+ and applies it. Returns the remaining attributes
def consume_run_list(attrs)
attrs = attrs ? attrs.dup : {}
if new_run_list = attrs.delete("recipes") || attrs.delete("run_list")
if attrs.key?("recipes") || attrs.key?("run_list")
raise Chef::Exceptions::AmbiguousRunlistSpecification, "please set the node's run list using the 'run_list' attribute only."
end
Chef::Log.info("Setting the run_list to #{new_run_list} from CLI options")
run_list(new_run_list)
end
attrs
end
# chef_environment when set in -j JSON will take precedence over
# -E ENVIRONMENT. Ideally, IMO, the order of precedence should be (lowest to
# highest):
# config_file
# -j JSON
# -E ENVIRONMENT
# so that users could reuse their JSON and override the chef_environment
# configured within it with -E ENVIRONMENT. Because command line options are
# merged with Chef::Config there is currently no way to distinguish between
# an environment set via config from an environment set via command line.
def consume_chef_environment(attrs)
attrs = attrs ? attrs.dup : {}
if env = attrs.delete("chef_environment")
chef_environment(env)
end
attrs
end
# Clear defaults and overrides, so that any deleted attributes
# between runs are still gone.
def reset_defaults_and_overrides
default.clear
override.clear
end
# Expands the node's run list and sets the default and override
# attributes. Also applies stored attributes (from json provided
# on the command line)
#
# Returns the fully-expanded list of recipes, a RunListExpansion.
#
#--
# TODO: timh/cw, 5-14-2010: Should this method exist? Should we
# instead modify default_attrs and override_attrs whenever our
# run_list is mutated? Or perhaps do something smarter like
# on-demand generation of default_attrs and override_attrs,
# invalidated only when run_list is mutated?
def expand!(data_source = "server")
expansion = run_list.expand(chef_environment, data_source)
raise Chef::Exceptions::MissingRole, expansion if expansion.errors?
tags # make sure they're defined
automatic_attrs[:recipes] = expansion.recipes.with_duplicate_names
automatic_attrs[:expanded_run_list] = expansion.recipes.with_fully_qualified_names_and_version_constraints
automatic_attrs[:roles] = expansion.roles
apply_expansion_attributes(expansion)
expansion
end
# Apply the default and overrides attributes from the expansion
# passed in, which came from roles.
def apply_expansion_attributes(expansion)
loaded_environment = if chef_environment == "_default"
Chef::Environment.new.tap { |e| e.name("_default") }
else
Chef::Environment.load(chef_environment)
end
attributes.env_default = loaded_environment.default_attributes
attributes.env_override = loaded_environment.override_attributes
attribute.role_default = expansion.default_attrs
attributes.role_override = expansion.override_attrs
end
# Transform the node to a Hash
def to_hash
index_hash = Hash.new
index_hash["chef_type"] = "node"
index_hash["name"] = name
index_hash["chef_environment"] = chef_environment
attribute.each do |key, value|
index_hash[key] = value
end
index_hash["recipe"] = run_list.recipe_names if run_list.recipe_names.length > 0
index_hash["role"] = run_list.role_names if run_list.role_names.length > 0
index_hash["run_list"] = run_list.run_list_items
index_hash
end
def display_hash
display = {}
display["name"] = name
display["chef_environment"] = chef_environment
display["automatic"] = automatic_attrs
display["normal"] = normal_attrs
display["default"] = attributes.combined_default
display["override"] = attributes.combined_override
display["run_list"] = run_list.run_list_items
display
end
# Serialize this object as a hash
def to_json(*a)
Chef::JSONCompat.to_json(for_json, *a)
end
def for_json
result = {
"name" => name,
"chef_environment" => chef_environment,
"json_class" => self.class.name,
"automatic" => attributes.automatic,
"normal" => attributes.normal,
"chef_type" => "node",
"default" => attributes.combined_default,
"override" => attributes.combined_override,
#Render correctly for run_list items so malformed json does not result
"run_list" => @primary_runlist.run_list.map { |item| item.to_s },
}
# Chef Server rejects node JSON with extra keys; prior to 12.3,
# "policy_name" and "policy_group" are unknown; after 12.3 they are
# optional, therefore only including them in the JSON if present
# maximizes compatibility for most people.
unless policy_group.nil? && policy_name.nil?
result["policy_name"] = policy_name
result["policy_group"] = policy_group
end
result
end
def update_from!(o)
run_list.reset!(o.run_list)
self.automatic_attrs = o.automatic_attrs
self.normal_attrs = o.normal_attrs
self.override_attrs = o.override_attrs
self.default_attrs = o.default_attrs
chef_environment(o.chef_environment)
self
end
# Create a Chef::Node from JSON
def self.json_create(o)
Chef.deprecated(:json_auto_inflate, "Auto inflation of JSON data is deprecated. Please use Chef::Node#from_hash")
from_hash(o)
end
def self.from_hash(o)
return o if o.kind_of? Chef::Node
node = new
node.name(o["name"])
node.chef_environment(o["chef_environment"])
if o.has_key?("attributes")
node.normal_attrs = o["attributes"]
end
node.automatic_attrs = Mash.new(o["automatic"]) if o.has_key?("automatic")
node.normal_attrs = Mash.new(o["normal"]) if o.has_key?("normal")
node.default_attrs = Mash.new(o["default"]) if o.has_key?("default")
node.override_attrs = Mash.new(o["override"]) if o.has_key?("override")
if o.has_key?("run_list")
node.run_list.reset!(o["run_list"])
elsif o.has_key?("recipes")
o["recipes"].each { |r| node.recipes << r }
end
node.policy_name = o["policy_name"] if o.has_key?("policy_name")
node.policy_group = o["policy_group"] if o.has_key?("policy_group")
node
end
def self.list_by_environment(environment, inflate = false)
if inflate
response = Hash.new
Chef::Search::Query.new.search(:node, "chef_environment:#{environment}") { |n| response[n.name] = n unless n.nil? }
response
else
Chef::ServerAPI.new(Chef::Config[:chef_server_url]).get("environments/#{environment}/nodes")
end
end
def self.list(inflate = false)
if inflate
response = Hash.new
Chef::Search::Query.new.search(:node) do |n|
n = Chef::Node.from_hash(n)
response[n.name] = n unless n.nil?
end
response
else
Chef::ServerAPI.new(Chef::Config[:chef_server_url]).get("nodes")
end
end
def self.find_or_create(node_name)
load(node_name)
rescue Net::HTTPServerException => e
raise unless e.response.code == "404"
node = build(node_name)
node.create
end
def self.build(node_name)
node = new
node.name(node_name)
node.chef_environment(Chef::Config[:environment]) unless Chef::Config[:environment].nil? || Chef::Config[:environment].chomp.empty?
node
end
# Load a node by name
def self.load(name)
from_hash(Chef::ServerAPI.new(Chef::Config[:chef_server_url]).get("nodes/#{name}"))
end
# Remove this node via the REST API
def destroy
chef_server_rest.delete("nodes/#{name}")
end
# Save this node via the REST API
def save
# Try PUT. If the node doesn't yet exist, PUT will return 404,
# so then POST to create.
begin
if Chef::Config[:why_run]
Chef::Log.warn("In why-run mode, so NOT performing node save.")
else
chef_server_rest.put("nodes/#{name}", data_for_save)
end
rescue Net::HTTPServerException => e
if e.response.code == "404"
chef_server_rest.post("nodes", data_for_save)
# Chef Server before 12.3 rejects node JSON with 'policy_name' or
# 'policy_group' keys, but 'policy_name' will be detected first.
# Backcompat can be removed in 13.0
elsif e.response.code == "400" && e.response.body.include?("Invalid key policy_name")
save_without_policyfile_attrs
else
raise
end
end
self
end
# Create the node via the REST API
def create
chef_server_rest.post("nodes", data_for_save)
self
rescue Net::HTTPServerException => e
# Chef Server before 12.3 rejects node JSON with 'policy_name' or
# 'policy_group' keys, but 'policy_name' will be detected first.
# Backcompat can be removed in 13.0
if e.response.code == "400" && e.response.body.include?("Invalid key policy_name")
chef_server_rest.post("nodes", data_for_save_without_policyfile_attrs)
else
raise
end
end
def to_s
"node[#{name}]"
end
def ==(other)
if other.kind_of?(self.class)
name == other.name
else
false
end
end
def <=>(other)
name <=> other.name
end
private
def save_without_policyfile_attrs
trimmed_data = data_for_save_without_policyfile_attrs
chef_server_rest.put("nodes/#{name}", trimmed_data)
rescue Net::HTTPServerException => e
raise e unless e.response.code == "404"
chef_server_rest.post("nodes", trimmed_data)
end
def data_for_save_without_policyfile_attrs
data_for_save.tap do |trimmed_data|
trimmed_data.delete("policy_name")
trimmed_data.delete("policy_group")
end
end
def data_for_save
data = for_json
%w{automatic default normal override}.each do |level|
whitelist_config_option = "#{level}_attribute_whitelist".to_sym
whitelist = Chef::Config[whitelist_config_option]
unless whitelist.nil? # nil => save everything
Chef::Log.info("Whitelisting #{level} node attributes for save.")
data[level] = Chef::Whitelist.filter(data[level], whitelist)
end
end
data
end
end
end