Hi, Here is the second iteration for the puppetclean application. This version takes into account review comments, and adds the possibility to "unexport" exported resources (by unexport, I mean that it puts ensure => absent if it is possible on those exported resources instead of removing all storedconfig content).
I also moved the report destroy stuff to the various report processors (even though only the store report implements it for the moment), but I'm not sure that was the right way of doing this. I couldn't see how to move the reports knowledge directly into the node. I'm open for better alternatives. Thanks, Brice Original commit msg: Here is a changeset that adds a new puppet application to the puppet application portfolio: puppetcleaner. This application removes all traces of a node on the puppetmaster (including certs, cached facts and nodes, reports, and storedconfig entries). Usage: puppetclean <host> Signed-off-by: Brice Figureau <brice-pup...@daysofwonder.com> --- bin/puppetclean | 65 +++++++++ lib/puppet/application/puppetclean.rb | 118 +++++++++++++++++ spec/unit/application/puppetclean.rb | 235 +++++++++++++++++++++++++++++++++ 3 files changed, 418 insertions(+), 0 deletions(-) create mode 100644 bin/puppetclean create mode 100644 lib/puppet/application/puppetclean.rb create mode 100644 spec/unit/application/puppetclean.rb diff --git a/bin/puppetclean b/bin/puppetclean new file mode 100644 index 0000000..2e9243b --- /dev/null +++ b/bin/puppetclean @@ -0,0 +1,65 @@ +#!/usr/bin/env ruby +# vim: softtabstop=4 shiftwidth=4 expandtab +# +# = Synopsis +# +# Fully clean a puppetmaster view of a host +# +# = Usage +# +# puppetclean [-h|--help] [-d|--debug] [-v|--verbose] [-u|--unexport] host +# +# = Description +# +# This command cleans everything a puppetmaster knows about a node, including +# +# * Signed certificates ($vardir/ssl/ca/signed/node.domain.pem) +# * Cached facts ($vardir/yaml/facts/node.domain.yaml) +# * Cached node stuff ($vardir/yaml/node/node.domain.yaml) +# * Reports ($vardir/reports/node.domain) +# * Stored configs: it can either remove all data from an host in your storedconfig +# database, or with --unexport turn every exported resource supporting ensure to absent +# so that any other host checking out their config can remove those exported configurations. +# +# = Options +# +# Note that any configuration parameter that's valid in the configuration file +# is also a valid long argument. For example, 'ssldir' is a valid configuration +# parameter, so you can specify '--ssldir <directory>' as an argument. +# +# See the configuration file documentation at +# http://reductivelabs.com/projects/puppet/reference/configref.html for +# the full list of acceptable parameters. A commented list of all +# configuration options can also be generated by running puppet with +# '--genconfig'. +# +# debug:: +# Enable full debugging. +# +# help: +# Print this help message. +# +# verbose:: +# Print extra information. +# +# unexport:: +# Instead of removing all the storedconfig data, it changes all exported resource's ensure parameter +# of this host to "absent". Then it is necessary to wait that all other host have checked out their +# configuration to run again puppetclean whithout this option to finish the clean-up. +# +# +# = Example +# +# puppeclean some-random-host.reductivelabs.com +# +# = Author +# +# Brice Figureau +# +# = Copyright +# +# Copyright (c) 2005-2007 Reductive Labs, LLC +# Licensed under the GNU Public License + +require 'puppet/application/puppetclean' +Puppet::Application[:puppetclean].run diff --git a/lib/puppet/application/puppetclean.rb b/lib/puppet/application/puppetclean.rb new file mode 100644 index 0000000..71af71e --- /dev/null +++ b/lib/puppet/application/puppetclean.rb @@ -0,0 +1,118 @@ +require 'puppet' +require 'puppet/application' + +Puppet::Application.new(:puppetclean) do + + should_not_parse_config + + attr_accessor :host + + option("--debug","-d") + option("--verbose","-v") + option("--unexport","-u") + + command(:main) do + if ARGV.length > 0 + host = ARGV.shift + else + raise "You must specify the host to clean" + end + + [ :clean_cert, :clean_cached_facts, :clean_cached_node, :clean_reports, :clean_storeconfigs ].each do |m| + begin + send(m, host) + rescue => detail + puts detail.backtrace if Puppet[:trace] + puts detail.to_s + end + end + end + + # clean signed cert for +host+ + def clean_cert(host) + Puppet::SSL::Host.destroy(host) + Puppet.info "%s certificates removed from ca" % host + end + + # clean facts for +host+ + def clean_cached_facts(host) + Puppet::Node::Facts.destroy(host) + Puppet.info "%s's facts removed" % host + end + + # clean cached node +host+ + def clean_cached_node(host) + Puppet::Node.indirection.cache.destroy(Puppet::Node.indirection.request(:destroy, host)) + Puppet.info "%s's cached node removed" % host + end + + # clean node reports for +host+ + def clean_reports(host) + Puppet::Transaction::Report.destroy(host) + Puppet.info "%s's reports removed" % host + end + + # clean store config for +host+ + def clean_storeconfigs(host) + return unless Puppet.features.rails? + + require 'puppet/rails' + Puppet::Rails.connect + + return unless rail_host = Puppet::Rails::Host.find_by_name(host) + + if options[:unexport] + unexport(rail_host) + Puppet.notice "Force %s's exported resources to absent" % host + Puppet.warning "Please wait other host have checked-out their configuration before finishing clean-up wih:" + Puppet.warning "$ puppetclean #{host}" + else + rail_host.destroy + Puppet.notice "%s storeconfigs removed" % host + end + end + + def unexport(host) + # fetch all exported resource + query = {:include => {:param_values => :param_name}} + values = [true, host.id] + query[:conditions] = ["exported=? AND host_id=?", *values] + + Puppet::Rails::Resource.find(:all, query).each do |resource| + if Puppet::Type.type(resource.restype.downcase.to_sym).validattr?(:ensure) + line = 0 + param_name = Puppet::Rails::ParamName.find_or_create_by_name("ensure") + + if ensure_param = resource.param_values.find(:first, :conditions => [ 'param_name_id = ?', param_name.id]) + line = ensure_param.line.to_i + Puppet::Rails::ParamValue.delete(ensure_param.id); + end + + # force ensure parameter to "absent" + resource.param_values.create(:value => "absent", + :line => line, + :param_name => param_name) + Puppet.info("%s has been marked as \"absent\"" % resource.name) + end + end + end + + setup do + Puppet::Util::Log.newdestination(:console) + + Puppet.parse_config + + # let's pretend we are puppetmasterd to access the + # right configuration settings from puppet.conf + Puppet[:name] = "puppetmasterd" + + if options[:debug] + Puppet::Util::Log.level = :debug + elsif options[:verbose] + Puppet::Util::Log.level = :info + end + + Puppet::Node::Facts.terminus_class = :yaml + Puppet::Node.cache_class = :yaml + end +end diff --git a/spec/unit/application/puppetclean.rb b/spec/unit/application/puppetclean.rb new file mode 100644 index 0000000..8bd6ec7 --- /dev/null +++ b/spec/unit/application/puppetclean.rb @@ -0,0 +1,235 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +require 'puppet/application/puppetclean' + +describe "puppetclean" do + before :each do + @puppetclean = Puppet::Application[:puppetclean] + end + + it "should not ask Puppet::Application to parse Puppet configuration file" do + @puppetclean.should_not be_should_parse_config + end + + it "should declare a main command" do + @puppetclean.should respond_to(:main) + end + + describe "when handling options" do + [:debug, :verbose, :unexport].each do |option| + it "should declare handle_#{option} method" do + @puppetclean.should respond_to("handle_#{option}".to_sym) + end + + it "should store argument value when calling handle_#{option}" do + @puppetclean.options.expects(:[]=).with(option, 'arg') + @puppetclean.send("handle_#{option}".to_sym, 'arg') + end + end + end + + describe "during setup" do + before :each do + Puppet::Log.stubs(:newdestination) + Puppet::Log.stubs(:level=) + Puppet.stubs(:parse_config) + Puppet.stubs(:[]=).with(:name, "puppetmasterd") + Puppet::Node::Facts.stubs(:terminus_class=) + Puppet::Node.stubs(:cache_class=) + end + + it "should set console as the log destination" do + Puppet::Log.expects(:newdestination).with(:console) + + @puppetclean.run_setup + end + + it "should parse puppet configuration" do + Puppet.expects(:parse_config) + + @puppetclean.run_setup + end + + it "should change application name to get puppetmasterd options" do + Puppet.expects(:[]=).with(:name, "puppetmasterd") + + @puppetclean.run_setup + end + + it "should set log level to debug if --debug was passed" do + @puppetclean.options.stubs(:[]).with(:debug).returns(true) + + Puppet::Log.expects(:level=).with(:debug) + + @puppetclean.run_setup + end + + it "should set log level to info if --verbose was passed" do + @puppetclean.options.stubs(:[]).with(:debug).returns(false) + @puppetclean.options.stubs(:[]).with(:verbose).returns(true) + + Puppet::Log.expects(:level=).with(:info) + + @puppetclean.run_setup + end + + it "should set facts terminus to yaml" do + Puppet::Node::Facts.expects(:terminus_class=).with(:yaml) + + @puppetclean.run_setup + end + + it "should set node cache as yaml" do + Puppet::Node.expects(:cache_class=).with(:yaml) + + @puppetclean.run_setup + end + end + + describe "when running" do + + before :each do + # @puppetclean.stubs(:puts) + @host = 'node' + ARGV.stubs(:shift).returns(@host) + ARGV.stubs(:length).returns(1) + Puppet.stubs(:info) + [ "cert", "cached_facts", "cached_node", "reports", "storeconfigs" ].each do |m| + @puppetclean.stubs("clean_#{m}".to_sym).with(@host) + end + end + + it "should raise an error if no type is given" do + ARGV.stubs(:length).returns(0) + + lambda { @puppetclean.main }.should raise_error + end + + + [ "cert", "cached_facts", "cached_node", "reports", "storeconfigs" ].each do |m| + it "should clean #{m.sub('_',' ')}" do + @puppetclean.expects("clean_#{m}".to_sym).with(@host) + + @puppetclean.main + end + end + end + + describe "when cleaning certificate" do + before :each do + Puppet::SSL::Host.stubs(:destroy) + end + + it "should send the :destroy order to the SSL host infrastructure" do + Puppet::SSL::Host.stubs(:destroy).with(@host) + + @puppetclean.clean_cert(@host) + end + end + + describe "when cleaning cached facts" do + it "should destroy facts" do + Puppet::Node::Facts.expects(:destroy).with(@host) + + @puppetclean.clean_cached_facts(@host) + end + end + + describe "when cleaning cached node" do + it "should destroy the cached node" do + cache = stub_everything 'cache' + request = stub_everything 'request' + + Puppet::Node.indirection.stubs(:request).with(:destroy, @host).returns(request) + Puppet::Node.indirection.stubs(:cache).returns(cache) + + Puppet::Node.indirection.cache.expects(:destroy).with(request) + + @puppetclean.clean_cached_node(@host) + end + end + + describe "when cleaning archived reports" do + it "should tell the reports to remove themselves" do + Puppet::Transaction::Report.stubs(:destroy).with(@host) + + @puppetclean.clean_reports(@host) + end + end + + describe "when cleaning storeconfigs entries for host" do + before :each do + @puppetclean.options.stubs(:[]).with(:unexport).returns(false) + Puppet.features.stubs(:rails?).returns(true) + Puppet::Rails.stubs(:connect) + @rails_node = stub_everything 'rails_node' + Puppet::Rails::Host.stubs(:find_by_name).returns(@rails_node) + @rail_node.stubs(:destroy) + end + + it "should connect to the database" do + Puppet::Rails.expects(:connect) + + @puppetclean.clean_storeconfigs(@host) + end + + it "should find the right host entry" do + Puppet::Rails::Host.expects(:find_by_name).with(@host).returns(@rails_node) + + @puppetclean.clean_storeconfigs(@host) + end + + describe "without unexport" do + it "should remove the host and it's content" do + @rails_node.expects(:destroy) + + @puppetclean.clean_storeconfigs(@host) + end + end + + describe "with unexport" do + before :each do + @puppetclean.options.stubs(:[]).with(:unexport).returns(true) + @rails_node.stubs(:id).returns(1234) + + @type = stub_everything 'type' + Puppet::Type.stubs(:type).returns(@type) + @type.stubs(:validattr?).with(:ensure).returns(true) + + @ensure_name = stub_everything 'ensure_name', :id => 23453 + Puppet::Rails::ParamName.stubs(:find_or_create_by_name).returns(@ensure_name) + + @param_values = stub_everything 'param_values' + @resource = stub_everything 'resource', :param_values => @param_values, :restype => "File" + Puppet::Rails::Resource.stubs(:find).returns([...@resource]) + end + + it "should find all resources" do + Puppet::Rails::Resource.expects(:find).with(:all, {:include => {:param_values => :param_name}, :conditions => ["exported=? AND host_id=?", true, 1234]}).returns([]) + + @puppetclean.clean_storeconfigs(@host) + end + + it "should delete the old ensure parameter" do + ensure_param = stub 'ensure_param', :id => 12345, :line => 12 + @param_values.stubs(:find).returns(ensure_param) + + Puppet::Rails::ParamValue.expects(:delete).with(12345); + + @puppetclean.clean_storeconfigs(@host) + end + + it "should add an ensure => absent parameter" do + @param_values.expects(:create).with(:value => "absent", + :line => 0, + :param_name => @ensure_name) + + + @puppetclean.clean_storeconfigs(@host) + end + + end + end +end -- 1.6.0.2 --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Puppet Developers" group. To post to this group, send email to puppet-dev@googlegroups.com To unsubscribe from this group, send email to puppet-dev+unsubscr...@googlegroups.com For more options, visit this group at http://groups.google.com/group/puppet-dev?hl=en -~----------~----~----~----~------~----~------~--~---