puppetlabs-puppet/ext/puppet-test

567 строки
17 KiB
Ruby
Executable File

#!/usr/bin/env ruby
# == Synopsis
#
# Test individual client performance. Can compile configurations, describe
# files, or retrieve files.
#
# = Usage
#
# puppet-test [-c|--compile] [-D|--describe <file>] [-d|--debug]
# [--fork <num>] [-h|--help] [-H|--hostname <host name>] [-l|--list] [-r|--repeat <number=1>]
# [-R|--retrieve <file>] [-t|--test <test>] [-V|--version] [-v|--verbose]
#
# = Description
#
# This is a simple script meant for doing performance tests with Puppet. By
# default it pulls down a compiled configuration, but it supports multiple
# other tests.
#
# = Options
#
# Note that any configuration parameter that's valid in the configuration file
# is also a valid long argument. For example, 'server' is a valid configuration
# parameter, so you can specify '--server <servername>' 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 puppetd with
# '--genconfig'.
#
# compile::
# Compile the client's configuration. The default.
#
# debug::
# Enable full debugging.
#
# describe::
# Describe the file being tested. This is a query to get information about
# the file from the server, to determine if it should be copied, and is the
# first half of every file transfer.
#
# fork::
# Fork the specified number of times, thus acting as multiple clients.
#
# fqdn::
# Set the fully-qualified domain name of the client. This is only used for
# certificate purposes, but can be used to override the discovered hostname.
# If you need to use this flag, it is generally an indication of a setup problem.
#
# help::
# Print this help message
#
# list::
# List all available tests.
#
# node::
# Specify the node to use. This is useful for looking up cached yaml data
# in your :clientyaml directory, and forcing a specific host's configuration to
# get compiled.
#
# pause::
# Pause before starting test (useful for testing with dtrace).
#
# repeat::
# How many times to perform the test.
#
# retrieve::
# Test file retrieval performance. Retrieves the specified file from the
# remote system. Note that the server should be specified via --server,
# so the argument to this option is just the remote module name and path,
# e.g., "/dist/apps/myapp/myfile", where "dist" is the module and
# "apps/myapp/myfile" is the path to the file relative to the module.
#
# test::
# Specify the test to run. You can see the list of tests by running this command with --list.
#
# verbose::
# Turn on verbose reporting.
#
# version::
# Print the puppet version number and exit.
#
# = Example
#
# puppet-test --retrieve /module/path/to/file
#
# = License
# Copyright 2011 Luke Kanies
#
# 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.
# Do an initial trap, so that cancels don't get a stack trace.
trap(:INT) do
$stderr.puts "Cancelling startup"
exit(1)
end
require 'puppet'
require 'puppet/network/client'
require 'getoptlong'
class Suite
attr_reader :name, :doc
@@suites = {}
@@tests = {}
def self.[](name)
@@suites[name]
end
# Run a test by first finding the suite then running the appropriate test.
def self.run(test)
unless suite_name = @@tests[test]
raise "Unknown test %s" % test
end
unless suite = @@suites[suite_name]
raise "Unknown test suite %s from test %s" % [suite_name, test]
end
suite.run(test)
end
# What suites are available?
def self.suites
@@suites.keys
end
def forked?
defined? @forking
end
# Create a new test suite.
def initialize(name, doc, &block)
@name = name
@doc = doc
@tests = {}
@@suites[name] = self
raise "You must pass a block to the Test" unless block_given?
instance_eval(&block)
end
# Define a new type of test on this suite.
def newtest(name, doc, &block)
@tests[name] = doc
if @@tests[name]
raise "Test names must be unique; cannot redefine %s" % name
end
@@tests[name] = @name
meta_def(name, &block)
end
# Run the actual test.
def run(test)
unless doc = @tests[test]
raise "Suite %s only supports tests %s; not %s" % [@name, @tests.keys.collect { |k| k.to_s }.join(","), test]
end
puts "Running %s %s test" % [@name, test]
prepare() if respond_to?(:prepare)
if $options[:pause]
puts "Hit any key to continue"
$stdin.readline
puts "Continuing with test"
end
if $options[:fork] > 0
@forking = true
$options[:fork].times {
if pid = fork
$pids << pid
else
break
end
}
end
$options[:repeat].times do |i|
@count = i
if forked?
msg = doc + " in PID %s" % Process.pid
else
msg = doc
end
Puppet::Util.benchmark(:notice, msg) do
begin
send(test)
rescue => detail
puts detail.backtrace if Puppet[:trace]
Puppet.err "%s failed: %s" % [@name, detail.to_s]
end
end
end
end
# What tests are available on this suite?
def tests
@tests.keys
end
end
Suite.new :parser, "Manifest parsing" do
newtest :parse, "Parsed files" do
@parser = Puppet::Parser::Parser.new(:environment => Puppet[:environment])
@parser.file = Puppet[:manifest]
@parser.parse
end
end
Suite.new :local_catalog, "Local catalog handling" do
def prepare
@node = Puppet::Node.find($options[:nodes][0])
end
newtest :compile, "Compiled catalog" do
Puppet::Resource::Catalog.find(@node)
end
end
Suite.new :resource_type, "Managing resource types" do
newtest :find, "Find a type" do
Puppet::Resource::Type.terminus_class = :parser
ARGV.each do |name|
json = Puppet::Resource::Type.find(name).to_pson
data = PSON.parse(json)
p Puppet::Resource::Type.from_pson(data)
end
end
newtest :search_types, "Find all types" do
Puppet::Resource::Type.terminus_class = :rest
result = Puppet::Resource::Type.search("*")
result.each { |r| p r }
end
newtest :restful_type, "Find a type and return it via REST" do
Puppet::Resource::Type.terminus_class = :rest
ARGV.each do |name|
p Puppet::Resource::Type.find(name)
end
end
end
Suite.new :remote_catalog, "Remote catalog handling" do
def prepare
$args[:cache] = false
# Create a config client and pull the config down
@client = Puppet::Network::Client.master.new($args)
unless @client.read_cert
fail "Could not read client certificate"
end
if tmp = Puppet::Node::Facts.find($options[:nodes][0])
@facts = tmp.values
else
raise "Could not find facts for %s" % $optins[:nodes][0]
end
if host = $options[:fqdn]
@facts["fqdn"] = host
@facts["hostname"] = host.sub(/\..+/, '')
@facts["domain"] = host.sub(/^[^.]+\./, '')
end
@facts = YAML.dump(@facts)
end
newtest :getconfig, "Compiled catalog" do
@client.driver.getconfig(@facts, "yaml")
end
# This test will always force a false answer.
newtest :fresh, "Checked freshness" do
@client.driver.freshness
end
end
Suite.new :file, "File interactions" do
def prepare
unless $options[:file]
fail "You must specify a file (using --file <file>) to interact with on the server"
end
@client = Puppet::Network::Client.file.new($args)
unless @client.read_cert
fail "Could not read client certificate"
end
end
newtest :describe, "Described file" do
@client.describe($options[:file], :ignore)
end
newtest :retrieve, "Retrieved file" do
@client.retrieve($options[:file], :ignore)
end
end
Suite.new :filebucket, "Filebucket interactions" do
def prepare
require 'tempfile'
@client = Puppet::FileBucket::Dipper.new($args)
end
newtest :backup, "Backed up file" do
Tempfile.open("bucket_testing") do |f|
f.print rand(1024)
f.close
@client.backup(f.path)
end
end
end
# Note that this uses an env variable to determine how many resources per
# host to create (with a default of 10). 'repeat' determines how
# many hosts to create. You probably will get mad failures if you
# use forking and sqlite.
# Here's an example run of this test, using sqlite:
# RESOURCE_COUNT=50 ext/puppet-test --suite rails --test storage --confdir /tmp/storagetesting --vardir /tmp/storagetesting --repeat 10
Suite.new :rails, "Rails Interactions" do
def prepare
Puppet::Rails.init
@facts = Facter.to_hash
if num = ENV["RESOURCECOUNT"]
@resources = Integer(num)
else
@resources = 10
end
end
Resource = Puppet::Parser::Resource
def execute(test, msg)
begin
send(test)
rescue => detail
puts detail.backtrace if Puppet[:trace]
Puppet.err "%s failed: %s" % [@name, detail.to_s]
end
end
def mkresource(type, title, parameters)
source = "fakesource"
res = Resource.new(:type => type, :title => title, :source => source, :scope => "fakescope")
parameters.each do |param, value|
res.set(param, value, source)
end
res
end
def ref(type, title)
Resource::Reference.new(:type => type, :title => title)
end
newtest :storage, "Stored resources" do
hostname = "host%s" % @count
@facts["hostname"] = hostname
args = {:facts => @facts, :name => hostname}
# Make all of the resources we want. Make them at least slightly complex,
# so we model real resources as close as we can.
resources = []
args[:resources] = resources
@resources.times do |resnum|
exec = mkresource("exec", "exec%s" % resnum, :command => "/bin/echo do something %s" % resnum)
exec.tags = %w{exec one} << "exec%s" % resnum
user = mkresource("user", "user%s" % resnum, :uid => resnum.to_s, :require => ref("exec", "exec%s" % resnum))
user.tags = %w{user one two} << "user%s" % resnum
file = mkresource("file", "/tmp/file%s" % resnum, :owner => resnum.to_s, :require => [ref("exec", "exec%s" % resnum), ref("user", "user%s" % resnum)])
file.tags = %w{file one three} << "file%s" % resnum
file.exported = true
resources << exec << user << file
end
Puppet::Rails::Host.store(args)
end
end
Suite.new :report, "Reports interactions" do
def prepare
Puppet::Transaction::Report.terminus_class = :rest
end
newtest :empty, "send empty report" do
report = Puppet::Transaction::Report.new
report.time = Time.now
report.save
end
newtest :fake, "send fake report" do
report = Puppet::Transaction::Report.new
resourcemetrics = {
:total => 12,
:out_of_sync => 20,
:applied => 45,
:skipped => 1,
:restarted => 23,
:failed_restarts => 1,
:scheduled => 10
}
report.newmetric(:resources, resourcemetrics)
timemetrics = {
:resource1 => 10,
:resource2 => 50,
:resource3 => 40,
:resource4 => 20,
}
report.newmetric(:times, timemetrics)
report.newmetric(:changes,
:total => 20
)
report.time = Time.now
report.save
end
end
$cmdargs = [
[ "--compile", "-c", GetoptLong::NO_ARGUMENT ],
[ "--describe", GetoptLong::REQUIRED_ARGUMENT ],
[ "--retrieve", "-R", GetoptLong::REQUIRED_ARGUMENT ],
[ "--fork", GetoptLong::REQUIRED_ARGUMENT ],
[ "--fqdn", "-F", GetoptLong::REQUIRED_ARGUMENT ],
[ "--suite", "-s", GetoptLong::REQUIRED_ARGUMENT ],
[ "--test", "-t", GetoptLong::REQUIRED_ARGUMENT ],
[ "--pause", "-p", GetoptLong::NO_ARGUMENT ],
[ "--repeat", "-r", GetoptLong::REQUIRED_ARGUMENT ],
[ "--node", "-n", GetoptLong::REQUIRED_ARGUMENT ],
[ "--debug", "-d", GetoptLong::NO_ARGUMENT ],
[ "--help", "-h", GetoptLong::NO_ARGUMENT ],
[ "--list", "-l", GetoptLong::NO_ARGUMENT ],
[ "--verbose", "-v", GetoptLong::NO_ARGUMENT ],
[ "--version", "-V", GetoptLong::NO_ARGUMENT ],
]
# Add all of the config parameters as valid $options.
Puppet.settings.addargs($cmdargs)
Puppet::Util::Log.newdestination(:console)
Puppet::Node.terminus_class = :plain
Puppet::Node.cache_class = :yaml
Puppet::Node::Facts.terminus_class = :facter
Puppet::Node::Facts.cache_class = :yaml
result = GetoptLong.new(*$cmdargs)
$args = {}
$options = {:repeat => 1, :fork => 0, :pause => false, :nodes => []}
begin
explicit_waitforcert = false
result.each { |opt,arg|
case opt
# First check to see if the argument is a valid configuration parameter;
# if so, set it.
when "--compile"
$options[:suite] = :configuration
$options[:test] = :compile
when "--retrieve"
$options[:suite] = :file
$options[:test] = :retrieve
$options[:file] = arg
when "--fork"
begin
$options[:fork] = Integer(arg)
rescue => detail
$stderr.puts "The argument to 'fork' must be an integer"
exit(14)
end
when "--describe"
$options[:suite] = :file
$options[:test] = :describe
$options[:file] = arg
when "--fqdn"
$options[:fqdn] = arg
when "--repeat"
$options[:repeat] = Integer(arg)
when "--help"
if Puppet.features.usage?
RDoc::usage && exit
else
puts "No help available unless you have RDoc::usage installed"
exit
end
when "--version"
puts "%s" % Puppet.version
exit
when "--verbose"
Puppet::Util::Log.level = :info
Puppet::Util::Log.newdestination(:console)
when "--debug"
Puppet::Util::Log.level = :debug
Puppet::Util::Log.newdestination(:console)
when "--suite"
$options[:suite] = arg.intern
when "--test"
$options[:test] = arg.intern
when "--file"
$options[:file] = arg
when "--pause"
$options[:pause] = true
when "--node"
$options[:nodes] << arg
when "--list"
Suite.suites.sort { |a,b| a.to_s <=> b.to_s }.each do |suite_name|
suite = Suite[suite_name]
tests = suite.tests.sort { |a,b| a.to_s <=> b.to_s }.join(", ")
puts "%20s: %s" % [suite_name, tests]
end
exit(0)
else
Puppet.settings.handlearg(opt, arg)
end
}
rescue GetoptLong::InvalidOption => detail
$stderr.puts detail
$stderr.puts "Try '#{$0} --help'"
exit(1)
end
# Now parse the config
Puppet.parse_config
$options[:nodes] << Puppet.settings[:certname] if $options[:nodes].empty?
$args[:Server] = Puppet[:server]
unless $options[:test]
$options[:suite] = :remote_catalog
$options[:test] = :getconfig
end
unless $options[:test]
raise "A suite was specified without a test"
end
$pids = []
Suite.run($options[:test])
if $options[:fork] > 0
Process.waitall
end