summaryrefslogtreecommitdiff
path: root/lib/chef/node.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/chef/node.rb')
-rw-r--r--lib/chef/node.rb476
1 files changed, 476 insertions, 0 deletions
diff --git a/lib/chef/node.rb b/lib/chef/node.rb
new file mode 100644
index 0000000000..92a2374bce
--- /dev/null
+++ b/lib/chef/node.rb
@@ -0,0 +1,476 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Brown (<cb@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Copyright:: Copyright (c) 2008-2011 Opscode, 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/check_helper'
+require 'chef/mixin/params_validate'
+require 'chef/mixin/from_file'
+require 'chef/mixin/deep_merge'
+require 'chef/dsl/include_attribute'
+require 'chef/environment'
+require 'chef/rest'
+require 'chef/run_list'
+require 'chef/node/attribute'
+require 'chef/mash'
+require 'chef/json_compat'
+require 'chef/search/query'
+
+class Chef
+ class Node
+
+ extend Forwardable
+
+ def_delegators :attributes, :keys, :each_key, :each_value, :key?, :has_key?
+
+ attr_accessor :recipe_list, :run_state, :run_list
+
+ attr_accessor :run_context
+
+ include Chef::Mixin::FromFile
+ include Chef::DSL::IncludeAttribute
+
+ include Chef::Mixin::CheckHelper
+ include Chef::Mixin::ParamsValidate
+
+ # Create a new Chef::Node object.
+ def initialize
+ @name = nil
+
+ @chef_environment = '_default'
+ @run_list = Chef::RunList.new
+
+ @attributes = Chef::Node::Attribute.new({}, {}, {}, {})
+
+ @run_state = {}
+ end
+
+ # Used by DSL
+ def node
+ self
+ end
+
+ def chef_server_rest
+ Chef::REST.new(Chef::Config[:chef_server_url])
+ 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
+
+ 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
+
+ alias_method :set, :normal
+
+ # Set a normal attribute of this node, auto-vivifying any mashes that are
+ # missing, but if the final value already exists, don't set it
+ def normal_unless
+ attributes.set_unless_value_present = true
+ attributes.normal
+ end
+ alias_method :set_unless, :normal_unless
+
+ # Set a default of this node, but auto-vivify any Mashes that might
+ # be missing
+ def default
+ attributes.default
+ end
+
+ # Set a default attribute of this node, auto-vivifying any mashes that are
+ # missing, but if the final value already exists, don't set it
+ def default_unless
+ attributes.set_unless_value_present = true
+ attributes.default
+ end
+
+ # Set an override attribute of this node, but auto-vivify any Mashes that
+ # might be missing
+ def override
+ attributes.override
+ end
+
+ # Set an override attribute of this node, auto-vivifying any mashes that
+ # are missing, but if the final value already exists, don't set it
+ def override_unless
+ attributes.set_unless_value_present = true
+ attributes.override
+ end
+
+
+ def override_attrs
+ attributes.override
+ end
+
+ def override_attrs=(new_values)
+ attributes.override = new_values
+ end
+
+ def default_attrs
+ attributes.default
+ end
+
+ def default_attrs=(new_values)
+ attributes.default = new_values
+ end
+
+ def normal_attrs
+ attributes.normal
+ 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
+ def method_missing(symbol, *args)
+ attributes.send(symbol, *args)
+ 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) || self[recipes].include?(recipe_name)
+ end
+
+ # Returns true if this Node expects a given role, false if not.
+ def role?(role_name)
+ run_list.include?("role[#{role_name}]")
+ 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)
+ args.length > 0 ? @run_list.reset!(args) : @run_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}")
+ self.automatic[:platform] = platform
+ self.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)
+ Chef::Log.debug("Applying attributes from json file")
+ self.normal_attrs = Chef::Mixin::DeepMerge.merge(normal_attrs,normal_attrs_to_merge)
+ self.tags # make sure they're defined
+ end
+
+ # Lazy initializer for tags attribute
+ def tags
+ normal[:tags] = [] unless attribute?(:tags)
+ normal[: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.inspect} from JSON")
+ run_list(new_run_list)
+ end
+ attrs
+ end
+
+ # Clear defaults and overrides, so that any deleted attributes
+ # between runs are still gone.
+ def reset_defaults_and_overrides
+ self.default.clear
+ self.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?
+
+ self.tags # make sure they're defined
+
+ automatic_attrs[:recipes] = expansion.recipes
+ automatic_attrs[:roles] = expansion.roles
+
+ expansion
+ end
+
+ # Apply the default and overrides attributes from the expansion
+ # passed in, which came from roles.
+ def apply_expansion_attributes(expansion)
+ load_chef_environment_object = (chef_environment == "_default" ? nil : Chef::Environment.load(chef_environment))
+ environment_default_attrs = load_chef_environment_object.nil? ? {} : load_chef_environment_object.default_attributes
+ default_before_roles = Chef::Mixin::DeepMerge.merge(default_attrs, environment_default_attrs)
+ self.default_attrs = Chef::Mixin::DeepMerge.merge(default_before_roles, expansion.default_attrs)
+ environment_override_attrs = load_chef_environment_object.nil? ? {} : load_chef_environment_object.override_attributes
+ overrides_before_environments = Chef::Mixin::DeepMerge.merge(override_attrs, expansion.override_attrs)
+ self.override_attrs = Chef::Mixin::DeepMerge.merge(overrides_before_environments, environment_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 if run_list.run_list.length > 0
+ 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"] = default_attrs
+ display["override"] = override_attrs
+ display["run_list"] = run_list.run_list
+ display
+ end
+
+ # Serialize this object as a hash
+ def to_json(*a)
+ result = {
+ "name" => name,
+ "chef_environment" => chef_environment,
+ 'json_class' => self.class.name,
+ "automatic" => attributes.automatic,
+ "normal" => attributes.normal,
+ "chef_type" => "node",
+ "default" => attributes.default,
+ "override" => attributes.override,
+ #Render correctly for run_list items so malformed json does not result
+ "run_list" => run_list.run_list.map { |item| item.to_s }
+ }
+ result.to_json(*a)
+ 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)
+ 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"])
+ else
+ o["recipes"].each { |r| node.recipes << r }
+ end
+ 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::REST.new(Chef::Config[:chef_server_url]).get_rest("environments/#{environment}/nodes")
+ end
+ end
+
+ def self.list(inflate=false)
+ if inflate
+ response = Hash.new
+ Chef::Search::Query.new.search(:node) do |n|
+ response[n.name] = n unless n.nil?
+ end
+ response
+ else
+ Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("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].chop.empty?
+ node
+ end
+
+ # Load a node by name
+ def self.load(name)
+ Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("nodes/#{name}")
+ end
+
+ # Remove this node via the REST API
+ def destroy
+ chef_server_rest.delete_rest("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 whyrun mode, so NOT performing node save.")
+ else
+ chef_server_rest.put_rest("nodes/#{name}", self)
+ end
+ rescue Net::HTTPServerException => e
+ raise e unless e.response.code == "404"
+ chef_server_rest.post_rest("nodes", self)
+ end
+ self
+ end
+
+ # Create the node via the REST API
+ def create
+ chef_server_rest.post_rest("nodes", self)
+ self
+ end
+
+ def to_s
+ "node[#{name}]"
+ end
+
+ end
+end