diff --git a/lib/puppet/indirector/active_record.rb b/lib/puppet/indirector/active_record.rb new file mode 100644 index 000000000..531109a18 --- /dev/null +++ b/lib/puppet/indirector/active_record.rb @@ -0,0 +1,28 @@ +require 'puppet/indirector' + +class Puppet::Indirector::ActiveRecord < Puppet::Indirector::Terminus + class << self + attr_accessor :ar_model + end + + def self.use_ar_model(klass) + self.ar_model = klass + end + + def ar_model + self.class.ar_model + end + + def initialize + Puppet::Rails.init + end + + def find(request) + return nil unless instance = ar_model.find_by_name(request.key) + instance.to_puppet + end + + def save(request) + ar_model.from_puppet(request.instance).save + end +end diff --git a/lib/puppet/indirector/facts/active_record.rb b/lib/puppet/indirector/facts/active_record.rb new file mode 100644 index 000000000..5fb2596d7 --- /dev/null +++ b/lib/puppet/indirector/facts/active_record.rb @@ -0,0 +1,35 @@ +require 'puppet/rails/fact_name' +require 'puppet/rails/fact_value' +require 'puppet/indirector/active_record' + +class Puppet::Node::Facts::ActiveRecord < Puppet::Indirector::ActiveRecord + use_ar_model Puppet::Rails::Host + + # Find the Rails host and pull its facts as a Facts instance. + def find(request) + return nil unless host = ar_model.find_by_name(request.key, :include => {:fact_values => :fact_name}) + + facts = Puppet::Node::Facts.new(host.name) + facts.values = host.get_facts_hash.inject({}) do |hash, ary| + # Convert all single-member arrays into plain values. + param = ary[0] + values = ary[1].collect { |v| v.value } + values = values[0] if values.length == 1 + hash[param] = values + hash + end + + facts + end + + # Save the values from a Facts instance as the facts on a Rails Host instance. + def save(request) + facts = request.instance + + host = ar_model.find_by_name(facts.name) || ar_model.create(:name => facts.name) + + host.setfacts(facts.values) + + host.save + end +end diff --git a/lib/puppet/indirector/node/active_record.rb b/lib/puppet/indirector/node/active_record.rb new file mode 100644 index 000000000..ab33af4b0 --- /dev/null +++ b/lib/puppet/indirector/node/active_record.rb @@ -0,0 +1,7 @@ +require 'puppet/rails/host' +require 'puppet/indirector/active_record' +require 'puppet/node' + +class Puppet::Node::ActiveRecord < Puppet::Indirector::ActiveRecord + use_ar_model Puppet::Rails::Host +end diff --git a/lib/puppet/rails/host.rb b/lib/puppet/rails/host.rb index 851cc21d9..23a22553d 100644 --- a/lib/puppet/rails/host.rb +++ b/lib/puppet/rails/host.rb @@ -1,206 +1,230 @@ require 'puppet/rails/resource' require 'puppet/rails/fact_name' require 'puppet/rails/source_file' require 'puppet/util/rails/collection_merger' class Puppet::Rails::Host < ActiveRecord::Base include Puppet::Util include Puppet::Util::CollectionMerger has_many :fact_values, :dependent => :destroy has_many :fact_names, :through => :fact_values belongs_to :source_file has_many :resources, :dependent => :destroy # If the host already exists, get rid of its objects def self.clean(host) if obj = self.find_by_name(host) obj.rails_objects.clear return obj else return nil end end + def self.from_puppet(node) + host = find_by_name(node.name) || new(:name => node.name) + + {"ipaddress" => "ip", "environment" => "environment"}.each do |myparam, itsparam| + if value = node.send(myparam) + host.send(itsparam + "=", value) + end + end + + host + end + # Store our host in the database. def self.store(node, resources) args = {} host = nil transaction do #unless host = find_by_name(name) seconds = Benchmark.realtime { unless host = find_by_name(node.name) host = new(:name => node.name) end } Puppet.debug("Searched for host in %0.2f seconds" % seconds) if ip = node.parameters["ipaddress"] host.ip = ip end if env = node.environment host.environment = env end # Store the facts into the database. host.setfacts node.parameters seconds = Benchmark.realtime { host.setresources(resources) } Puppet.debug("Handled resources in %0.2f seconds" % seconds) host.last_compile = Time.now host.save end return host end # Return the value of a fact. def fact(name) if fv = self.fact_values.find(:all, :include => :fact_name, :conditions => "fact_names.name = '#{name}'") return fv else return nil end end # returns a hash of fact_names.name => [ fact_values ] for this host. + # Note that 'fact_values' is actually a list of the value instances, not + # just actual values. def get_facts_hash fact_values = self.fact_values.find(:all, :include => :fact_name) return fact_values.inject({}) do | hash, value | hash[value.fact_name.name] ||= [] hash[value.fact_name.name] << value hash end end def setfacts(facts) facts = facts.dup ar_hash_merge(get_facts_hash(), facts, :create => Proc.new { |name, values| fact_name = Puppet::Rails::FactName.find_or_create_by_name(name) values = [values] unless values.is_a?(Array) values.each do |value| fact_values.build(:value => value, :fact_name => fact_name) end }, :delete => Proc.new { |values| values.each { |value| self.fact_values.delete(value) } }, :modify => Proc.new { |db, mem| mem = [mem].flatten fact_name = db[0].fact_name db_values = db.collect { |fact_value| fact_value.value } (db_values - (db_values & mem)).each do |value| db.find_all { |fact_value| fact_value.value == value }.each { |fact_value| fact_values.delete(fact_value) } end (mem - (db_values & mem)).each do |value| fact_values.build(:value => value, :fact_name => fact_name) end }) end # Set our resources. def setresources(list) resource_by_id = nil seconds = Benchmark.realtime { resource_by_id = find_resources() } Puppet.debug("Searched for resources in %0.2f seconds" % seconds) seconds = Benchmark.realtime { find_resources_parameters_tags(resource_by_id) } if id Puppet.debug("Searched for resource params and tags in %0.2f seconds" % seconds) seconds = Benchmark.realtime { compare_to_catalog(resource_by_id, list) } Puppet.debug("Resource comparison took %0.2f seconds" % seconds) end def find_resources resources.find(:all, :include => :source_file).inject({}) do | hash, resource | hash[resource.id] = resource hash end end def find_resources_parameters_tags(resources) # initialize all resource parameters resources.each do |key,resource| resource.params_hash = [] end find_resources_parameters(resources) find_resources_tags(resources) end # it seems that it can happen (see bug #2010) some resources are duplicated in the # database (ie logically corrupted database), in which case we remove the extraneous # entries. def compare_to_catalog(existing, list) extra_db_resources = [] resources = existing.inject({}) do |hash, res| resource = res[1] if hash.include?(resource.ref) extra_db_resources << hash[resource.ref] end hash[resource.ref] = resource hash end compiled = list.inject({}) do |hash, resource| hash[resource.ref] = resource hash end ar_hash_merge(resources, compiled, :create => Proc.new { |ref, resource| resource.to_rails(self) }, :delete => Proc.new { |resource| self.resources.delete(resource) }, :modify => Proc.new { |db, mem| mem.modify_rails(db) }) # fix-up extraneous resources extra_db_resources.each do |resource| self.resources.delete(resource) end end def find_resources_parameters(resources) params = Puppet::Rails::ParamValue.find_all_params_from_host(self) # assign each loaded parameters/tags to the resource it belongs to params.each do |param| resources[param['resource_id']].add_param_to_hash(param) if resources.include?(param['resource_id']) end end def find_resources_tags(resources) tags = Puppet::Rails::ResourceTag.find_all_tags_from_host(self) tags.each do |tag| resources[tag['resource_id']].add_tag_to_hash(tag) if resources.include?(tag['resource_id']) end end def update_connect_time self.last_connect = Time.now save end -end + def to_puppet + node = Puppet::Node.new(self.name) + {"ip" => "ipaddress", "environment" => "environment"}.each do |myparam, itsparam| + if value = send(myparam) + node.send(itsparam + "=", value) + end + end + + node + end +end diff --git a/spec/unit/indirector/active_record.rb b/spec/unit/indirector/active_record.rb new file mode 100755 index 000000000..6d81b0fbe --- /dev/null +++ b/spec/unit/indirector/active_record.rb @@ -0,0 +1,75 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' +require 'puppet/indirector/active_record' + +describe Puppet::Indirector::ActiveRecord do + before do + Puppet::Rails.stubs(:init) + + Puppet::Indirector::Terminus.stubs(:register_terminus_class) + @model = mock 'model' + @indirection = stub 'indirection', :name => :mystuff, :register_terminus_type => nil, :model => @model + Puppet::Indirector::Indirection.stubs(:instance).returns(@indirection) + + @active_record_class = Class.new(Puppet::Indirector::ActiveRecord) do + def self.to_s + "Mystuff::Testing" + end + end + + @ar_model = mock 'ar_model' + + @active_record_class.use_ar_model @ar_model + @terminus = @active_record_class.new + + @name = "me" + @instance = stub 'instance', :name => @name + + @request = stub 'request', :key => @name, :instance => @instance + end + + it "should allow declaration of an ActiveRecord model to use" do + @active_record_class.use_ar_model "foo" + @active_record_class.ar_model.should == "foo" + end + + describe "when initializing" do + it "should init Rails" do + Puppet::Rails.expects(:init) + @active_record_class.new + end + end + + describe "when finding an instance" do + it "should use the ActiveRecord model to find the instance" do + @ar_model.expects(:find_by_name).with(@name) + + @terminus.find(@request) + end + + it "should return nil if no instance is found" do + @ar_model.expects(:find_by_name).with(@name).returns nil + @terminus.find(@request).should be_nil + end + + it "should convert the instance to a Puppet object if it is found" do + instance = mock 'rails_instance' + instance.expects(:to_puppet).returns "mypuppet" + + @ar_model.expects(:find_by_name).with(@name).returns instance + @terminus.find(@request).should == "mypuppet" + end + end + + describe "when saving an instance" do + it "should use the ActiveRecord model to convert the instance into a Rails object and then save that rails object" do + rails_object = mock 'rails_object' + @ar_model.expects(:from_puppet).with(@instance).returns rails_object + + rails_object.expects(:save) + + @terminus.save(@request) + end + end +end diff --git a/spec/unit/indirector/facts/active_record.rb b/spec/unit/indirector/facts/active_record.rb new file mode 100755 index 000000000..340f2cf4c --- /dev/null +++ b/spec/unit/indirector/facts/active_record.rb @@ -0,0 +1,103 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +require 'puppet/node/facts' +require 'puppet/indirector/facts/active_record' + +describe Puppet::Node::Facts::ActiveRecord do + confine "Missing Rails" => Puppet.features.rails? + + before do + Puppet.features.stubs(:rails?).returns true + @terminus = Puppet::Node::Facts::ActiveRecord.new + end + + it "should be a subclass of the ActiveRecord terminus class" do + Puppet::Node::Facts::ActiveRecord.ancestors.should be_include(Puppet::Indirector::ActiveRecord) + end + + it "should use Puppet::Rails::Host as its ActiveRecord model" do + Puppet::Node::Facts::ActiveRecord.ar_model.should equal(Puppet::Rails::Host) + end + + describe "when finding an instance" do + before do + @request = stub 'request', :key => "foo" + end + + it "should use the Hosts ActiveRecord class to find the host" do + Puppet::Rails::Host.expects(:find_by_name).with { |key, args| key == "foo" } + @terminus.find(@request) + end + + it "should include the fact names and values when finding the host" do + Puppet::Rails::Host.expects(:find_by_name).with { |key, args| args[:include] == {:fact_values => :fact_name} } + @terminus.find(@request) + end + + it "should return nil if no host instance can be found" do + Puppet::Rails::Host.expects(:find_by_name).returns nil + + @terminus.find(@request).should be_nil + end + + it "should convert the node's parameters into a Facts instance if a host instance is found" do + host = stub 'host', :name => "foo" + host.expects(:get_facts_hash).returns("one" => [mock("two_value", :value => "two")], "three" => [mock("three_value", :value => "four")]) + + Puppet::Rails::Host.expects(:find_by_name).returns host + + result = @terminus.find(@request) + + result.should be_instance_of(Puppet::Node::Facts) + result.name.should == "foo" + result.values.should == {"one" => "two", "three" => "four"} + end + + it "should convert all single-member arrays into non-arrays" do + host = stub 'host', :name => "foo" + host.expects(:get_facts_hash).returns("one" => [mock("two_value", :value => "two")]) + + Puppet::Rails::Host.expects(:find_by_name).returns host + + @terminus.find(@request).values["one"].should == "two" + end + end + + describe "when saving an instance" do + before do + @host = stub 'host', :name => "foo", :save => nil, :setfacts => nil + Puppet::Rails::Host.stubs(:find_by_name).returns @host + @facts = Puppet::Node::Facts.new("foo", "one" => "two", "three" => "four") + @request = stub 'request', :key => "foo", :instance => @facts + end + + it "should find the Rails host with the same name" do + Puppet::Rails::Host.expects(:find_by_name).with("foo").returns @host + + @terminus.save(@request) + end + + it "should create a new Rails host if none can be found" do + Puppet::Rails::Host.expects(:find_by_name).with("foo").returns nil + + Puppet::Rails::Host.expects(:create).with(:name => "foo").returns @host + + @terminus.save(@request) + end + + it "should set the facts as facts on the Rails host instance" do + # There is other stuff added to the hash. + @host.expects(:setfacts).with { |args| args["one"] == "two" and args["three"] == "four" } + + @terminus.save(@request) + end + + it "should save the Rails host instance" do + @host.expects(:save) + + @terminus.save(@request) + end + end +end diff --git a/spec/unit/indirector/node/active_record.rb b/spec/unit/indirector/node/active_record.rb new file mode 100755 index 000000000..22a6bebaf --- /dev/null +++ b/spec/unit/indirector/node/active_record.rb @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +require 'puppet/node' +require 'puppet/indirector/node/active_record' + +describe Puppet::Node::ActiveRecord do + confine "Missing Rails" => Puppet.features.rails? + + it "should be a subclass of the ActiveRecord terminus class" do + Puppet::Node::ActiveRecord.ancestors.should be_include(Puppet::Indirector::ActiveRecord) + end + + it "should use Puppet::Rails::Host as its ActiveRecord model" do + Puppet::Node::ActiveRecord.ar_model.should equal(Puppet::Rails::Host) + end +end diff --git a/spec/unit/rails/host.rb b/spec/unit/rails/host.rb new file mode 100755 index 000000000..882abbd5a --- /dev/null +++ b/spec/unit/rails/host.rb @@ -0,0 +1,91 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +describe "Puppet::Rails::Host" do + confine "Cannot test without ActiveRecord" => Puppet.features.rails? + + def column(name, type) + ActiveRecord::ConnectionAdapters::Column.new(name, nil, type, false) + end + + before do + require 'puppet/rails/host' + + # Stub this so we don't need access to the DB. + Puppet::Rails::Host.stubs(:columns).returns([column("name", "string"), column("environment", "string"), column("ip", "string")]) + + @node = Puppet::Node.new("foo") + @node.environment = "production" + @node.ipaddress = "127.0.0.1" + + @host = stub 'host', :environment= => nil, :ip= => nil + end + + describe "when converting a Puppet::Node instance into a Rails instance" do + it "should modify any existing instance in the database" do + Puppet::Rails::Host.expects(:find_by_name).with("foo").returns @host + + Puppet::Rails::Host.from_puppet(@node) + end + + it "should create a new instance in the database if none can be found" do + Puppet::Rails::Host.expects(:find_by_name).with("foo").returns nil + Puppet::Rails::Host.expects(:new).with(:name => "foo").returns @host + + Puppet::Rails::Host.from_puppet(@node) + end + + it "should copy the environment from the Puppet instance" do + Puppet::Rails::Host.expects(:find_by_name).with("foo").returns @host + + @node.environment = "production" + @host.expects(:environment=).with "production" + + Puppet::Rails::Host.from_puppet(@node) + end + + it "should copy the ipaddress from the Puppet instance" do + Puppet::Rails::Host.expects(:find_by_name).with("foo").returns @host + + @node.ipaddress = "192.168.0.1" + @host.expects(:ip=).with "192.168.0.1" + + Puppet::Rails::Host.from_puppet(@node) + end + + it "should not save the Rails instance" do + Puppet::Rails::Host.expects(:find_by_name).with("foo").returns @host + + @host.expects(:save).never + + Puppet::Rails::Host.from_puppet(@node) + end + end + + describe "when converting a Puppet::Rails::Host instance into a Puppet::Node instance" do + before do + @host = Puppet::Rails::Host.new(:name => "foo", :environment => "production", :ip => "127.0.0.1") + @node = Puppet::Node.new("foo") + Puppet::Node.stubs(:new).with("foo").returns @node + end + + it "should create a new instance with the correct name" do + Puppet::Node.expects(:new).with("foo").returns @node + + @host.to_puppet + end + + it "should copy the environment from the Rails instance" do + @host.environment = "prod" + @node.expects(:environment=).with "prod" + @host.to_puppet + end + + it "should copy the ipaddress from the Rails instance" do + @host.ip = "192.168.0.1" + @node.expects(:ipaddress=).with "192.168.0.1" + @host.to_puppet + end + end +end