diff --git a/.gitignore b/.gitignore index 3c4c506c8..a2082370b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,3 @@ .rspec results .*.sw[op] -*.wixobj -downloads/* -pkg/* -stagedir/* -wix/fragments/* diff --git a/README_DEVELOPER.md b/README_DEVELOPER.md index f38067a0e..aa6fb2bf4 100644 --- a/README_DEVELOPER.md +++ b/README_DEVELOPER.md @@ -1,98 +1,91 @@ # Developer README # This file is intended to provide a place for developers and contributors to document what other developers need to know about changes made to Puppet. # UTF-8 Handling # As Ruby 1.9 becomes more commonly used with Puppet, developers should be aware of major changes to the way Strings and Regexp objects are handled. Specifically, every instance of these two classes will have an encoding attribute determined in a number of ways. * If the source file has an encoding specified in the magic comment at the top, the instance will take on that encoding. * Otherwise, the encoding will be determined by the LC\_LANG or LANG environment variables. * Otherwise, the encoding will default to ASCII-8BIT ## References ## Excellent information about the differences between encodings in Ruby 1.8 and Ruby 1.9 is published in this blog series: [Understanding M17n](http://links.puppetlabs.com/understanding_m17n) ## Encodings of Regexp and String instances ## In general, please be aware that Ruby 1.9 regular expressions need to be compatible with the encoding of a string being used to match them. If they are not compatible you can expect to receive and error such as: Encoding::CompatibilityError: incompatible encoding regexp match (ASCII-8BIT regexp with UTF-8 string) In addition, some escape sequences were valid in Ruby 1.8 are no longer valid in 1.9 if the regular expression is not marked as an ASCII-8BIT object. You may expect errors like this in this situation: SyntaxError: (irb):7: invalid multibyte escape: /\xFF/ This error is particularly common when serializing a string to other representations like JSON or YAML. To resolve the problem you can explicitly mark the regular expression as ASCII-8BIT using the /n flag: "a" =~ /\342\230\203/n Finally, any time you're thinking of a string as an array of bytes rather than an array of characters, common when escaping a string, you should work with everything in ASCII-8BIT. Changing the encoding will not change the data itself and allow the Regexp and the String to deal with bytes rather than characters. Puppet provides a monkey patch to String which returns an encoding suitable for byte manipulations: # Example of how to escape non ASCII printable characters for YAML. >> snowman = "☃" >> snowman.to_ascii8bit.gsub(/([\x80-\xFF])/n) { |x| "\\x#{x.unpack("C")[0].to_s(16)} } => "\\xe2\\x98\\x83" If the Regexp is not marked as ASCII-8BIT using /n, then you can expect the SyntaxError, invalid multibyte escape as mentioned above. # Windows # If you'd like to run Puppet from source on Windows platforms, the include `ext/envpuppet.bat` will help. All file paths in the Puppet code base should use a path separator of / regardless of Windows or Unix filesystem. To quickly run Puppet from source, assuming you already have Ruby installed from [rubyinstaller.org](http://rubyinstaller.org). gem install sys-admin win32-process win32-dir win32-taskscheduler --no-rdoc --no-ri gem install win32-service --platform=mswin32 --no-rdoc --no-ri --version 0.7.1 net use Z: "\\vmware-host\Shared Folders" /persistent:yes Z: cd set PATH=%PATH%;Z:\\ext envpuppet puppet --version 2.7.9 Some spec tests are known to fail on Windows, e.g. no mount provider on Windows, so use the following rspec exclude filter: cd envpuppet rspec --tag ~fails_on_windows spec This will give you a shared filesystem with your Mac and allow you to run Puppet directly from source without using install.rb or copying files around. -## Building Windows Packages ## - -Please see the README files in `tasks/windows/` for more information about -building MSI packages of Puppet for Windows. Please see also -[#11205](http://projects.puppetlabs.com/issues/11205) for up to date progress -on this project. - EOF diff --git a/acceptance/tests/cycle_detection.rb b/acceptance/tests/cycle_detection.rb new file mode 100644 index 000000000..c59ca9e0f --- /dev/null +++ b/acceptance/tests/cycle_detection.rb @@ -0,0 +1,26 @@ +test_name "cycle detection and reporting" + +step "check we report a simple cycle" +manifest = < Notify["a2"] } +notify { "a2": require => Notify["a1"] } +EOT + +apply_manifest_on(agents, manifest) do + assert_match(/Found 1 dependency cycle/, stdout, + "found and reported the cycle correctly") +end + +step "report multiple cycles in the same graph" +manifest = < Notify["a2"] } +notify { "a2": require => Notify["a1"] } + +notify { "b1": require => Notify["b2"] } +notify { "b2": require => Notify["b1"] } +EOT + +apply_manifest_on(agents, manifest) do + assert_match(/Found 2 dependency cycles/, stdout, + "found and reported the cycle correctly") +end diff --git a/acceptance/tests/modules/fake_modulepath/apache/metadata.json b/acceptance/tests/modules/fake_modulepath/apache/metadata.json new file mode 100644 index 000000000..87fc9a78a --- /dev/null +++ b/acceptance/tests/modules/fake_modulepath/apache/metadata.json @@ -0,0 +1,54 @@ +{ + "name": "puppetlabs-apache", + "dependencies": [ + + ], + "author": "", + "license": "", + "version": "0.0.3", + "checksums": { + "tests/ssl.pp": "191912535199531fd631f911c6329e56", + "manifests/params.pp": "8728cf041cdd94bb0899170eb2b417d9", + "tests/vhost.pp": "1b91e03c8ef89a7ecb6793831ac18399", + "manifests/php.pp": "8a5ca4035b1c22892923f3fde55e3d5e", + "lib/puppet/provider/a2mod/a2mod.rb": "18c5bb180b75a2375e95e07f88a94257", + "tests/php.pp": "ce7bb9eef69d32b42a32ce32d9653625", + "files/httpd": "295f5e924afe6f752d29327e73fe6d0a", + "manifests/dev.pp": "bc54a5af648cb04b7b3bb0e3f7be6543", + "manifests/ssl.pp": "11ed1861298c72cca3a706480bb0b67c", + "files/test.vhost": "0602022c19a7b6b289f218c7b93c1aea", + "tests/init.pp": "4eac4a7ef68499854c54a78879e25535", + "manifests/vhost.pp": "7806a6c098e217da046d0555314756c4", + "lib/puppet/type/a2mod.rb": "0e1b4843431413a10320ac1f6a055d15", + "templates/vhost-default.conf.erb": "ed64a53af0d7bad762176a98c9ea3e62", + "tests/dev.pp": "4cf15c1fecea3ca86009f182b402c7ab", + "tests/apache.pp": "4eac4a7ef68499854c54a78879e25535", + "Modulefile": "9b7a414bf15b06afe2f011068fcaff52", + "manifests/init.pp": "9ef7e081c832bca8f861c3a9feb9949d" + }, + "types": [ + { + "name": "a2mod", + "doc": "Manage Apache 2 modules on Debian and Ubuntu", + "parameters": [ + { + "name": "name", + "doc": "The name of the module to be managed" + } + ], + "providers": [ + { + "name": "a2mod", + "doc": "Manage Apache 2 modules on Debian and Ubuntu Required binaries: ``a2enmod``, ``a2dismod``. Default for ``operatingsystem`` == ``debianubuntu``. " + } + ], + "properties": [ + { + "name": "ensure", + "doc": "The basic property that the resource should be in. Valid values are ``present``, ``absent``." + } + ] + } + ], + "source": "" +} \ No newline at end of file diff --git a/acceptance/tests/modules/fake_modulepath/bacula/metadata.json b/acceptance/tests/modules/fake_modulepath/bacula/metadata.json new file mode 100644 index 000000000..dbc780841 --- /dev/null +++ b/acceptance/tests/modules/fake_modulepath/bacula/metadata.json @@ -0,0 +1,53 @@ +{ + "description": "This module manages Bacula, a complete backup solution", + "source": "http://github.com/puppetlabs/puppetlabs-bacula", + "checksums": { + "templates/client_config.erb": "5d4005eda5ace78fd90a8cb6dcb388bd", + "spec/spec_helper.rb": "ca19ec4f451ebc7fdb035b52eae6e909", + "templates/bacula-fd.conf.erb": "344fff616e138fbf8cd150f4bab8125d", + "CHANGELOG": "a20043858790de6086b9b30c36806cd2", + "manifests/config/validate.pp": "7d2f9f7cffb2bd1d221eb699ea55f246", + "LICENSE": "26ce13c80c7a8493533c65c32dc29f09", + "manifests/director.pp": "36f34f749314634173b6a47b92a58421", + "manifests/config/client.pp": "5d25cbfd94be2829d9d530673cd2307f", + "lib/puppet/parser/functions/generate_clients.rb": "e09d77f88d18ec3e59bbe7bae4a48036", + "manifests/common.pp": "67391e5560ee7fc97d873ab5c3e2f84c", + "manifests/client.pp": "42e9255711d0a6abed6ab8deafb1248c", + "manifests/console.pp": "4cf8ac2f96f9268e2a3d70ed777797b7", + "README.md": "13d0f1119d510e41c539cbc3825a4f82", + "templates/bacula-sd.conf.erb": "df7987695bf1dd18bb789a2673b2cd95", + "manifests/config.pp": "624a225f85f907e7db5cf717561953f3", + "tests/init.pp": "b95d199119d5e592df7e1580bf23fe06", + "templates/bconsole.conf.erb": "ba5e1ef7d320a48bde6e403dc956952a", + "manifests/bat.pp": "3743bddf0197b4110e54a8e297e927e8", + "manifests/storage.pp": "84f4ec1413df4fdec7288c18d6ba2897", + "templates/bacula-dir.conf.erb": "5ab5aa263cf318ebf65c6b52a0726647", + "metadata.json": "d34d0b70aba36510fbc2df4e667479ef", + "spec/spec.opts": "a600ded995d948e393fbe2320ba8e51c", + "Modulefile": "63f93af605871a69211cd3f79ac62582", + "manifests/init.pp": "fafac38161dd69f4b7e452cf910812f4" + }, + "summary": "This module manages a bacula infrastructure", + "author": "Puppet Labs", + "dependencies": [ + { + "version_requirement": ">= 2.2.0", + "name": "puppetlabs/stdlib" + }, + { + "version_requirement": ">= 0.0.1", + "name": "puppetlabs/mysql" + }, + { + "version_requirement": ">= 0.0.1", + "name": "puppetlabs/sqlite" + } + ], + "project_page": "http://github.com/puppetlabs/puppetlabs-bacula", + "types": [ + + ], + "license": "Apache", + "version": "0.0.2", + "name": "puppetlabs-bacula" +} \ No newline at end of file diff --git a/acceptance/tests/modules/fake_modulepath/mysql/metadata.json b/acceptance/tests/modules/fake_modulepath/mysql/metadata.json new file mode 100644 index 000000000..0aa833df3 --- /dev/null +++ b/acceptance/tests/modules/fake_modulepath/mysql/metadata.json @@ -0,0 +1,140 @@ +{ + "description": "Mysql module", + "source": "git://github.com/puppetlabs/puppetlabs-mysql.git", + "checksums": { + "lib/puppet/provider/database_grant/default.rb": "38a9c5fe0fe1b8474cc2bfd475a225f1", + "manifests/python.pp": "743e5ce2255afa9113a82c5e7fee3740", + "manifests/ruby.pp": "7b57a3321f90c455bccea9de1d57149a", + "manifests/params.pp": "9aeda052d3518d3fcd6e9ee353c899c5", + "lib/puppet/type/database_user.rb": "134269c960f9f751c33e0f023692e256", + "tests/mysql_user.pp": "7b066843d7cdcc54e95ae13ab82ec4f3", + "CHANGELOG": "f2e3e57948da2dcab3bdbe782efd6b11", + "lib/puppet/type/database.rb": "f6ca3a0d053c06752fec999a33c1f5a0", + "templates/my.cnf.erb": "302d55a6dfa368e3957abdd018e0c915", + "manifests/server.pp": "870e294ec504bde5174c203747312f8a", + "LICENSE": "0e5ccf641e613489e66aa98271dbe798", + "templates/my.cnf.pass.erb": "a4952e72bb8aea85a07274c2c1c0334f", + "manifests/server/mysqltuner.pp": "68951b161e11dfce8d93b202d7937704", + "manifests/server/monitor.pp": "76bb559e957086f6bd97ed286f15fd0c", + "lib/puppet/provider/database/mysql.rb": "92bd9124898e9a6258b585085034af4e", + "README": "33f2ef98ed5732170ea12de2598342a5", + "manifests/config.pp": "264b959f3529558050205eae26a61883", + "tests/python.pp": "b093828acfed9c14e25ebdd60d90c282", + "lib/puppet/provider/database/default.rb": "2f4d021abda21e363604403b0e0be231", + "lib/puppet/type/database_grant.rb": "d1b41c45e9c18262310b55170b364c75", + "files/mysqltuner.pl": "de535154b7fb28e437ba412434ea535e", + "tests/init.pp": "6b34827ac4731829c8a117f0b3fb8167", + "manifests/db.pp": "167ab5ec006ad0a9ea6d8a52f554eef5", + "TODO": "88ca4024a37992b46c34cb46e4ac39e6", + "tests/ruby.pp": "6c5071fcaf731995c9b8e31e00eaffa0", + "tests/mysql_database.pp": "2c611d35a1fabe5c418a917391dccade", + "lib/puppet/provider/database_grant/mysql.rb": "43cccef7eaf04b5cf343d2aff9147b99", + "tests/mysql_grant.pp": "106e1671b1f68701778401e4a3fc8d05", + "tests/server.pp": "afa67b373af325b705b49239c7e2efcf", + "lib/puppet/provider/database_user/mysql.rb": "5433dbcc8b596d6a141d0ee31e590f3e", + "lib/puppet/parser/functions/mysql_password.rb": "08aaa14cfbe99ceac1b59053685ee4c0", + "lib/puppet/provider/database_user/default.rb": "31cc564c11b58a23ab694ed17143f70f", + "Modulefile": "49f8c465c58c8841c2c1a98a8ad485dc", + "manifests/init.pp": "ed5175393dfa7da87e75a5f1ebfa21ef" + }, + "summary": "Mysql module", + "author": "PuppetLabs", + "dependencies": [ + { + "version_requirement": ">= 0.0.1", + "name": "bodepd/create_resources" + } + ], + "project_page": "http://github.com/puppetlabs/puppetlabs-mysql", + "types": [ + { + "parameters": [ + { + "doc": "The name of the database.", + "name": "name" + } + ], + "doc": "Manage creation/deletion of a database.", + "providers": [ + { + "doc": "This is a default provider that does nothing. This allows us to install mysql on the same puppet run where we want to use it. ", + "name": "default" + }, + { + "doc": "Create mysql database. Required binaries: `mysql`, `mysqladmin`, `mysqlshow`. Default for `kernel` == `Linux`. ", + "name": "mysql" + } + ], + "name": "database", + "properties": [ + { + "doc": "The basic property that the resource should be in. Valid values are `present`, `absent`.", + "name": "ensure" + }, + { + "doc": "The characterset to use for a database Values can match `/^\\S+$/`.", + "name": "charset" + } + ] + }, + { + "parameters": [ + { + "doc": "The primary key: either user@host for global privilges or user@host/database for database specific privileges", + "name": "name" + } + ], + "doc": "Manage a database user's rights.", + "providers": [ + { + "doc": "Uses mysql as database. ", + "name": "default" + }, + { + "doc": "Uses mysql as database. Required binaries: `mysql`, `mysqladmin`. Default for `kernel` == `Linux`. ", + "name": "mysql" + } + ], + "name": "database_grant", + "properties": [ + { + "doc": "The privileges the user should have. The possible values are implementation dependent.", + "name": "privileges" + } + ] + }, + { + "parameters": [ + { + "doc": "The name of the user. This uses the 'username@hostname' or username@hosname.", + "name": "name" + } + ], + "doc": "Manage a database user. This includes management of users password as well as priveleges", + "providers": [ + { + "doc": "manage users for a mysql database. ", + "name": "default" + }, + { + "doc": "manage users for a mysql database. Required binaries: `mysql`, `mysqladmin`. Default for `kernel` == `Linux`. ", + "name": "mysql" + } + ], + "name": "database_user", + "properties": [ + { + "doc": "The basic property that the resource should be in. Valid values are `present`, `absent`.", + "name": "ensure" + }, + { + "doc": "The password hash of the user. Use mysql_password() for creating such a hash. Values can match `/\\w+/`.", + "name": "password_hash" + } + ] + } + ], + "license": "Apache", + "version": "0.0.0", + "name": "puppetlabs-mysql" +} diff --git a/acceptance/tests/modules/fake_modulepath/sqlite/metadata.json b/acceptance/tests/modules/fake_modulepath/sqlite/metadata.json new file mode 100644 index 000000000..280db67d8 --- /dev/null +++ b/acceptance/tests/modules/fake_modulepath/sqlite/metadata.json @@ -0,0 +1,26 @@ +{ + "description": "This module provides a sqlite class to manage\nthe installation of sqlite on a node. It also provides\na sqlite::db defined type to manage databases on a system", + "source": "https://github.com/puppetlabs/puppetlabs-sqlite/", + "checksums": { + "spec/spec_helper.rb": "ca19ec4f451ebc7fdb035b52eae6e909", + "README.md": "ed04f8ed93d3a6ce19b9153b9444039c", + "tests/init.pp": "e8b321554c2d582e35beb01c57951062", + "manifests/db.pp": "ce94dbfcc3b10738eeec23304898ee78", + "metadata.json": "d34d0b70aba36510fbc2df4e667479ef", + "spec/spec.opts": "a600ded995d948e393fbe2320ba8e51c", + "Modulefile": "dda385f94c11e563df1fbe11eeba272d", + "manifests/init.pp": "859cb8ed63863adbaa202c45561280c5" + }, + "summary": "Manage a sqlite installation and databases", + "author": "puppetlabs", + "dependencies": [ + + ], + "project_page": "http://projects.puppetlabs.com/projects/modules/issues", + "types": [ + + ], + "license": "Apache", + "version": "0.0.1.1", + "name": "puppetlabs-sqlite" +} diff --git a/acceptance/tests/modules/list.rb b/acceptance/tests/modules/list.rb new file mode 100644 index 000000000..76ac6b86b --- /dev/null +++ b/acceptance/tests/modules/list.rb @@ -0,0 +1,26 @@ +test_name "puppet module list test output and dependency error checking" + +step "Run puppet module list" +expected_stdout = <<-HEREDOC.strip +/opt/puppet-git-repos/puppet/acceptance/tests/modules/fake_modulepath + apache (0.0.3) + bacula (0.0.2) + mysql (0.0.0) + sqlite (0.0.1.1) + HEREDOC + +expected_stderr = <<-HEREDOC.strip +Version dependency mismatch `mysql` (0.0.0): + `bacula` (0.0.2) requires `puppetlabs/mysql` (>= 0.0.1) +Non semantic version dependency `sqlite` (0.0.1.1): + `bacula` (0.0.2) requires `puppetlabs/sqlite` (>= 0.0.1) +Missing dependency `stdlib`: + `bacula` (0.0.2) requires `puppetlabs/stdlib` (>= 2.2.0) +Missing dependency `create_resources`: + `mysql` (0.0.0) requires `bodepd/create_resources` (>= 0.0.1) + HEREDOC + +on master, "puppet module list --modulepath /opt/puppet-git-repos/puppet/acceptance/tests/modules/fake_modulepath" do + assert_equal(expected_stdout, stdout.strip, "puppet module list did not output expected stdout") + assert_equal(expected_stderr, stderr.strip, "puppet module list did not output expected stderr") +end diff --git a/acceptance/tests/resource/file/symbolic_modes.rb b/acceptance/tests/resource/file/symbolic_modes.rb new file mode 100644 index 000000000..3d088eebc --- /dev/null +++ b/acceptance/tests/resource/file/symbolic_modes.rb @@ -0,0 +1,115 @@ +test_name "file resource: symbolic modes" + +def validate(path, mode) + "ruby -e 'exit (File::Stat.new(#{path.inspect}).mode & 0777 == #{mode})'" +end + +def tmpfile(what) + "/tmp/symbolic-mode-#{what}-" + + "#{Time.now.strftime("%Y%m%d")}-#{$$}-" + + "#{rand(0x100000000).to_s(36)}.test" +end + +# Generate some standard, remote-safe names for our scratch content, and make +# sure that they exist already. Don't need to care about modes or owners, as +# we reset anything that matters inside the test loop. +file = tmpfile('file') +dir = tmpfile('dir') + +on agents, "touch #{file} ; mkdir #{dir}" + +# Infrastructure for a nice, table driven test. Yum. +# +# For your reference: +# 4000 the set-user-ID-on-execution bit +# 2000 the set-group-ID-on-execution bit +# 1000 the sticky bit +# 0400 Allow read by owner. +# 0200 Allow write by owner. +# 0100 For files, allow execution by owner. For directories, allow the +# owner to search in the directory. +# 0040 Allow read by group members. +# 0020 Allow write by group members. +# 0010 For files, allow execution by group members. For directories, allow +# group members to search in the directory. +# 0004 Allow read by others. +# 0002 Allow write by others. +# 0001 For files, allow execution by others. For directories allow others +# to search in the directory. +# +# fields are: start_mode, symbolic_mode, file_mode, dir_mode +# start_mode is passed to chmod on the target platform +# symbolic_mode is the mode set in our test +# the file and dir mode values are the numeric resultant values we should get +tests = < [3]) { - assert_match(/Triggering #{target}/, stdout, "didn't trigger #{target} on #{host}" ) - } + if host['platform'].include?('windows') + on(host, puppet_kick(target), :acceptable_exit_codes => [1]) { + assert_match(/Puppet kick is not supported/, stderr) + } + else + on(host, puppet_kick(target), :acceptable_exit_codes => [3]) { + assert_match(/Triggering #{target}/, stdout, "didn't trigger #{target} on #{host}" ) + } + end end diff --git a/install.rb b/install.rb index 5fa360531..a9b9394c4 100755 --- a/install.rb +++ b/install.rb @@ -1,453 +1,440 @@ #! /usr/bin/env ruby #-- # Copyright 2004 Austin Ziegler # Install utility. Based on the original installation script for rdoc by the # Pragmatic Programmers. # # This program is free software. It may be redistributed and/or modified under # the terms of the GPL version 2 (or later) or the Ruby licence. # # Usage # ----- # In most cases, if you have a typical project layout, you will need to do # absolutely nothing to make this work for you. This layout is: # # bin/ # executable files -- "commands" # lib/ # the source of the library # tests/ # unit tests # # The default behaviour: # 1) Run all unit test files (ending in .rb) found in all directories under # tests/. # 2) Build Rdoc documentation from all files in bin/ (excluding .bat and .cmd), # all .rb files in lib/, ./README, ./ChangeLog, and ./Install. # 3) Build ri documentation from all files in bin/ (excluding .bat and .cmd), # and all .rb files in lib/. This is disabled by default on Microsoft Windows. # 4) Install commands from bin/ into the Ruby bin directory. On Windows, if a # if a corresponding batch file (.bat or .cmd) exists in the bin directory, # it will be copied over as well. Otherwise, a batch file (always .bat) will # be created to run the specified command. # 5) Install all library files ending in .rb from lib/ into Ruby's # site_lib/version directory. # #++ require 'rbconfig' require 'find' require 'fileutils' +require 'tempfile' begin require 'ftools' # apparently on some system ftools doesn't get loaded $haveftools = true rescue LoadError puts "ftools not found. Using FileUtils instead.." $haveftools = false end require 'optparse' require 'ostruct' begin require 'rdoc/rdoc' $haverdoc = true rescue LoadError puts "Missing rdoc; skipping documentation" $haverdoc = false end PREREQS = %w{openssl facter cgi} MIN_FACTER_VERSION = 1.5 InstallOptions = OpenStruct.new def glob(list) g = list.map { |i| Dir.glob(i) } g.flatten! g.compact! g.reject! { |e| e =~ /\.svn/ } g end # Set these values to what you want installed. configs = glob(%w{conf/auth.conf}) sbins = glob(%w{sbin/*}) bins = glob(%w{bin/*}) rdoc = glob(%w{bin/* sbin/* lib/**/*.rb README README-library CHANGELOG TODO Install}).reject { |e| e=~ /\.(bat|cmd)$/ } ri = glob(%w{bin/*.rb sbin/* lib/**/*.rb}).reject { |e| e=~ /\.(bat|cmd)$/ } man = glob(%w{man/man[0-9]/*}) libs = glob(%w{lib/**/*.rb lib/**/*.erb lib/**/*.py lib/puppet/util/command_line/*}) tests = glob(%w{test/**/*.rb}) def do_configs(configs, target, strip = 'conf/') Dir.mkdir(target) unless File.directory? target configs.each do |cf| ocf = File.join(InstallOptions.config_dir, cf.gsub(/#{strip}/, '')) if $haveftools File.install(cf, ocf, 0644, true) else FileUtils.install(cf, ocf, {:mode => 0644, :verbose => true}) end end end def do_bins(bins, target, strip = 's?bin/') Dir.mkdir(target) unless File.directory? target bins.each do |bf| obf = bf.gsub(/#{strip}/, '') install_binfile(bf, obf, target) end end def do_libs(libs, strip = 'lib/') libs.each do |lf| olf = File.join(InstallOptions.site_dir, lf.gsub(/#{strip}/, '')) op = File.dirname(olf) if $haveftools File.makedirs(op, true) File.chmod(0755, op) File.install(lf, olf, 0644, true) else FileUtils.makedirs(op, {:mode => 0755, :verbose => true}) FileUtils.chmod(0755, op) FileUtils.install(lf, olf, {:mode => 0644, :verbose => true}) end end end def do_man(man, strip = 'man/') man.each do |mf| omf = File.join(InstallOptions.man_dir, mf.gsub(/#{strip}/, '')) om = File.dirname(omf) if $haveftools File.makedirs(om, true) File.chmod(0755, om) File.install(mf, omf, 0644, true) else FileUtils.makedirs(om, {:mode => 0755, :verbose => true}) FileUtils.chmod(0755, om) FileUtils.install(mf, omf, {:mode => 0644, :verbose => true}) end gzip = %x{which gzip} gzip.chomp! %x{#{gzip} -f #{omf}} end end # Verify that all of the prereqs are installed def check_prereqs PREREQS.each { |pre| begin require pre if pre == "facter" # to_f isn't quite exact for strings like "1.5.1" but is good # enough for this purpose. facter_version = Facter.version.to_f if facter_version < MIN_FACTER_VERSION puts "Facter version: #{facter_version}; minimum required: #{MIN_FACTER_VERSION}; cannot install" exit -1 end end rescue LoadError puts "Could not load #{pre}; cannot install" exit -1 end } end ## # Prepare the file installation. # def prepare_installation $operatingsystem = Facter["operatingsystem"].value InstallOptions.configs = true # Only try to do docs if we're sure they have rdoc if $haverdoc InstallOptions.rdoc = true InstallOptions.ri = $operatingsystem != "windows" else InstallOptions.rdoc = false InstallOptions.ri = false end InstallOptions.tests = true ARGV.options do |opts| opts.banner = "Usage: #{File.basename($0)} [options]" opts.separator "" opts.on('--[no-]rdoc', 'Prevents the creation of RDoc output.', 'Default on.') do |onrdoc| InstallOptions.rdoc = onrdoc end opts.on('--[no-]ri', 'Prevents the creation of RI output.', 'Default off on mswin32.') do |onri| InstallOptions.ri = onri end opts.on('--[no-]tests', 'Prevents the execution of unit tests.', 'Default on.') do |ontest| InstallOptions.tests = ontest end opts.on('--[no-]configs', 'Prevents the installation of config files', 'Default off.') do |ontest| InstallOptions.configs = ontest end opts.on('--destdir[=OPTIONAL]', 'Installation prefix for all targets', 'Default essentially /') do |destdir| InstallOptions.destdir = destdir end opts.on('--configdir[=OPTIONAL]', 'Installation directory for config files', 'Default /etc/puppet') do |configdir| InstallOptions.configdir = configdir end opts.on('--bindir[=OPTIONAL]', 'Installation directory for binaries', 'overrides Config::CONFIG["bindir"]') do |bindir| InstallOptions.bindir = bindir end opts.on('--sbindir[=OPTIONAL]', 'Installation directory for system binaries', 'overrides Config::CONFIG["sbindir"]') do |sbindir| InstallOptions.sbindir = sbindir end opts.on('--sitelibdir[=OPTIONAL]', 'Installation directory for libraries', 'overrides Config::CONFIG["sitelibdir"]') do |sitelibdir| InstallOptions.sitelibdir = sitelibdir end opts.on('--mandir[=OPTIONAL]', 'Installation directory for man pages', 'overrides Config::CONFIG["mandir"]') do |mandir| InstallOptions.mandir = mandir end opts.on('--quick', 'Performs a quick installation. Only the', 'installation is done.') do |quick| InstallOptions.rdoc = false InstallOptions.ri = false InstallOptions.tests = false InstallOptions.configs = true end opts.on('--full', 'Performs a full installation. All', 'optional installation steps are run.') do |full| InstallOptions.rdoc = true InstallOptions.ri = true InstallOptions.tests = true InstallOptions.configs = true end opts.separator("") opts.on_tail('--help', "Shows this help text.") do $stderr.puts opts exit end opts.parse! end - tmpdirs = [ENV['TMP'], ENV['TEMP'], "/tmp", "/var/tmp", "."] - version = [Config::CONFIG["MAJOR"], Config::CONFIG["MINOR"]].join(".") libdir = File.join(Config::CONFIG["libdir"], "ruby", version) # Mac OS X 10.5 and higher declare bindir and sbindir as # /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin # /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/sbin # which is not generally where people expect executables to be installed # These settings are appropriate defaults for all OS X versions. if RUBY_PLATFORM =~ /^universal-darwin[\d\.]+$/ Config::CONFIG['bindir'] = "/usr/bin" Config::CONFIG['sbindir'] = "/usr/sbin" end if not InstallOptions.configdir.nil? configdir = InstallOptions.configdir elsif $operatingsystem == "windows" begin require 'win32/dir' rescue LoadError => e puts "Cannot run on Microsoft Windows without the sys-admin, win32-process, win32-dir & win32-service gems: #{e}" exit -1 end configdir = File.join(Dir::COMMON_APPDATA, "PuppetLabs", "puppet", "etc") else configdir = "/etc/puppet" end if not InstallOptions.bindir.nil? bindir = InstallOptions.bindir else bindir = Config::CONFIG['bindir'] end if not InstallOptions.sbindir.nil? sbindir = InstallOptions.sbindir else sbindir = Config::CONFIG['sbindir'] end if not InstallOptions.sitelibdir.nil? sitelibdir = InstallOptions.sitelibdir else sitelibdir = Config::CONFIG["sitelibdir"] if sitelibdir.nil? sitelibdir = $LOAD_PATH.find { |x| x =~ /site_ruby/ } if sitelibdir.nil? sitelibdir = File.join(libdir, "site_ruby") elsif sitelibdir !~ Regexp.quote(version) sitelibdir = File.join(sitelibdir, version) end end end if not InstallOptions.mandir.nil? mandir = InstallOptions.mandir else mandir = Config::CONFIG['mandir'] end # This is the new way forward if not InstallOptions.destdir.nil? destdir = InstallOptions.destdir # To be deprecated once people move over to using --destdir option elsif not ENV['DESTDIR'].nil? destdir = ENV['DESTDIR'] warn "DESTDIR is deprecated. Use --destdir instead." else destdir = '' end configdir = join(destdir, configdir) bindir = join(destdir, bindir) sbindir = join(destdir, sbindir) mandir = join(destdir, mandir) sitelibdir = join(destdir, sitelibdir) FileUtils.makedirs(configdir) if InstallOptions.configs FileUtils.makedirs(bindir) FileUtils.makedirs(sbindir) FileUtils.makedirs(mandir) FileUtils.makedirs(sitelibdir) - tmpdirs << bindir - - InstallOptions.tmp_dirs = tmpdirs.compact InstallOptions.site_dir = sitelibdir InstallOptions.config_dir = configdir InstallOptions.bin_dir = bindir InstallOptions.sbin_dir = sbindir InstallOptions.lib_dir = libdir InstallOptions.man_dir = mandir end ## # Join two paths. On Windows, dir must be converted to a relative path, # by stripping the drive letter, but only if the basedir is not empty. # def join(basedir, dir) return "#{basedir}#{dir[2..-1]}" if $operatingsystem == "windows" and basedir.length > 0 and dir.length > 2 "#{basedir}#{dir}" end ## # Build the rdoc documentation. Also, try to build the RI documentation. # def build_rdoc(files) return unless $haverdoc begin r = RDoc::RDoc.new r.document(["--main", "README", "--title", "Puppet -- Site Configuration Management", "--line-numbers"] + files) rescue RDoc::RDocError => e $stderr.puts e.message rescue Exception => e $stderr.puts "Couldn't build RDoc documentation\n#{e.message}" end end def build_ri(files) return unless $haverdoc begin ri = RDoc::RDoc.new #ri.document(["--ri-site", "--merge"] + files) ri.document(["--ri-site"] + files) rescue RDoc::RDocError => e $stderr.puts e.message rescue Exception => e $stderr.puts "Couldn't build Ri documentation\n#{e.message}" $stderr.puts "Continuing with install..." end end def run_tests(test_list) require 'test/unit/ui/console/testrunner' $LOAD_PATH.unshift "lib" test_list.each do |test| next if File.directory?(test) require test end tests = [] ObjectSpace.each_object { |o| tests << o if o.kind_of?(Class) } tests.delete_if { |o| !o.ancestors.include?(Test::Unit::TestCase) } tests.delete_if { |o| o == Test::Unit::TestCase } tests.each { |test| Test::Unit::UI::Console::TestRunner.run(test) } $LOAD_PATH.shift rescue LoadError puts "Missing testrunner library; skipping tests" end ## # Install file(s) from ./bin to Config::CONFIG['bindir']. Patch it on the way # to insert a #! line; on a Unix install, the command is named as expected # (e.g., bin/rdoc becomes rdoc); the shebang line handles running it. Under # windows, we add an '.rb' extension and let file associations do their stuff. def install_binfile(from, op_file, target) - tmp_dir = nil - InstallOptions.tmp_dirs.each do |t| - if File.directory?(t) and File.writable?(t) - tmp_dir = t - break - end - end - - fail "Cannot find a temporary directory" unless tmp_dir - tmp_file = File.join(tmp_dir, '_tmp') + tmp_file = Tempfile.new('puppet-binfile') ruby = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']) File.open(from) do |ip| - File.open(tmp_file, "w") do |op| + File.open(tmp_file.path, "w") do |op| ruby = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']) op.puts "#!#{ruby}" contents = ip.readlines contents.shift if contents[0] =~ /^#!/ op.write contents.join end end if $operatingsystem == "windows" installed_wrapper = false if File.exists?("#{from}.bat") FileUtils.install("#{from}.bat", File.join(target, "#{op_file}.bat"), :mode => 0755, :verbose => true) installed_wrapper = true end if File.exists?("#{from}.cmd") FileUtils.install("#{from}.cmd", File.join(target, "#{op_file}.cmd"), :mode => 0755, :verbose => true) installed_wrapper = true end if not installed_wrapper - tmp_file2 = File.join(tmp_dir, '_tmp_wrapper') + tmp_file2 = Tempfile.new('puppet-wrapper') cwv = <<-EOS @echo off setlocal set RUBY_BIN=%~dp0 set RUBY_BIN=%RUBY_BIN:\\=/% "%RUBY_BIN%ruby.exe" -x "%RUBY_BIN%puppet" %* EOS - File.open(tmp_file2, "w") { |cw| cw.puts cwv } - FileUtils.install(tmp_file2, File.join(target, "#{op_file}.bat"), :mode => 0755, :verbose => true) + File.open(tmp_file2.path, "w") { |cw| cw.puts cwv } + FileUtils.install(tmp_file2.path, File.join(target, "#{op_file}.bat"), :mode => 0755, :verbose => true) - File.unlink(tmp_file2) + tmp_file2.unlink installed_wrapper = true end end - FileUtils.install(tmp_file, File.join(target, op_file), :mode => 0755, :verbose => true) - File.unlink(tmp_file) + FileUtils.install(tmp_file.path, File.join(target, op_file), :mode => 0755, :verbose => true) + tmp_file.unlink end check_prereqs prepare_installation #run_tests(tests) if InstallOptions.tests #build_rdoc(rdoc) if InstallOptions.rdoc #build_ri(ri) if InstallOptions.ri do_configs(configs, InstallOptions.config_dir) if InstallOptions.configs do_bins(sbins, InstallOptions.sbin_dir) do_bins(bins, InstallOptions.bin_dir) do_libs(libs) do_man(man) unless $operatingsystem == "windows" diff --git a/lib/puppet/application/face_base.rb b/lib/puppet/application/face_base.rb index a111518f1..14ce67b42 100644 --- a/lib/puppet/application/face_base.rb +++ b/lib/puppet/application/face_base.rb @@ -1,247 +1,254 @@ require 'puppet/application' require 'puppet/face' require 'optparse' require 'pp' class Puppet::Application::FaceBase < Puppet::Application should_parse_config run_mode :agent option("--debug", "-d") do |arg| Puppet::Util::Log.level = :debug end option("--verbose", "-v") do Puppet::Util::Log.level = :info end option("--render-as FORMAT") do |format| self.render_as = format.to_sym end option("--mode RUNMODE", "-r") do |arg| raise "Invalid run mode #{arg}; supported modes are user, agent, master" unless %w{user agent master}.include?(arg) self.class.run_mode(arg.to_sym) set_run_mode self.class.run_mode end attr_accessor :face, :action, :type, :arguments, :render_as def render_as=(format) if format == :json then @render_as = Puppet::Network::FormatHandler.format(:pson) else @render_as = Puppet::Network::FormatHandler.format(format) end @render_as or raise ArgumentError, "I don't know how to render '#{format}'" end - def render(result) - # Invoke the rendering hook supplied by the user, if appropriate. - if hook = action.when_rendering(render_as.name) - result = hook.call(result) + def render(result, args_and_options) + hook = action.when_rendering(render_as.name) + + if hook + # when defining when_rendering on your action you can optionally + # include arguments and options + if hook.arity > 1 + result = hook.call(result, *args_and_options) + else + result = hook.call(result) + end end render_as.render(result) end def preinit super Signal.trap(:INT) do $stderr.puts "Cancelling Face" exit(0) end end def parse_options # We need to parse enough of the command line out early, to identify what # the action is, so that we can obtain the full set of options to parse. # REVISIT: These should be configurable versions, through a global # '--version' option, but we don't implement that yet... --daniel 2011-03-29 @type = self.class.name.to_s.sub(/.+:/, '').downcase.to_sym @face = Puppet::Face[@type, :current] # Now, walk the command line and identify the action. We skip over # arguments based on introspecting the action and all, and find the first # non-option word to use as the action. action_name = nil index = -1 until action_name or (index += 1) >= command_line.args.length do item = command_line.args[index] if item =~ /^-/ then option = @face.options.find do |name| item =~ /^-+#{name.to_s.gsub(/[-_]/, '[-_]')}(?:[ =].*)?$/ end if option then option = @face.get_option(option) # If we have an inline argument, just carry on. We don't need to # care about optional vs mandatory in that case because we do a real # parse later, and that will totally take care of raising the error # when we get there. --daniel 2011-04-04 if option.takes_argument? and !item.index('=') then index += 1 unless (option.optional_argument? and command_line.args[index + 1] =~ /^-/) end elsif option = find_global_settings_argument(item) then unless Puppet.settings.boolean? option.name then # As far as I can tell, we treat non-bool options as always having # a mandatory argument. --daniel 2011-04-05 index += 1 # ...so skip the argument. end elsif option = find_application_argument(item) then index += 1 if (option[:argument] and not option[:optional]) else raise OptionParser::InvalidOption.new(item.sub(/=.*$/, '')) end else # Stash away the requested action name for later, and try to fetch the # action object it represents; if this is an invalid action name that # will be nil, and handled later. action_name = item.to_sym @action = Puppet::Face.find_action(@face.name, action_name) @face = @action.face if @action end end if @action.nil? if @action = @face.get_default_action() then @is_default_action = true else # REVISIT: ...and this horror thanks to our log setup, which doesn't # initialize destinations until the setup method, which we will never # reach. We could also just print here, but that is actually a little # uglier and nastier in the long term, in which we should do log setup # earlier if at all possible. --daniel 2011-05-31 Puppet::Util::Log.newdestination(:console) face = @face.name action = action_name.nil? ? 'default' : "'#{action_name}'" msg = "'#{face}' has no #{action} action. See `puppet help #{face}`." Puppet.err(msg) exit false end end # Now we can interact with the default option code to build behaviour # around the full set of options we now know we support. @action.options.each do |option| option = @action.get_option(option) # make it the object. self.class.option(*option.optparse) # ...and make the CLI parse it. end # ...and invoke our parent to parse all the command line options. super end def find_global_settings_argument(item) Puppet.settings.each do |name, object| object.optparse_args.each do |arg| next unless arg =~ /^-/ # sadly, we have to emulate some of optparse here... pattern = /^#{arg.sub('[no-]', '').sub(/[ =].*$/, '')}(?:[ =].*)?$/ pattern.match item and return object end end return nil # nothing found. end def find_application_argument(item) self.class.option_parser_commands.each do |options, function| options.each do |option| next unless option =~ /^-/ pattern = /^#{option.sub('[no-]', '').sub(/[ =].*$/, '')}(?:[ =].*)?$/ next unless pattern.match(item) return { :argument => option =~ /[ =]/, :optional => option =~ /[ =]\[/ } end end return nil # not found end def setup Puppet::Util::Log.newdestination :console @arguments = command_line.args # Note: because of our definition of where the action is set, we end up # with it *always* being the first word of the remaining set of command # line arguments. So, strip that off when we construct the arguments to # pass down to the face action. --daniel 2011-04-04 # Of course, now that we have default actions, we should leave the # "action" name on if we didn't actually consume it when we found our # action. @arguments.delete_at(0) unless @is_default_action # We copy all of the app options to the end of the call; This allows each # action to read in the options. This replaces the older model where we # would invoke the action with options set as global state in the # interface object. --daniel 2011-03-28 @arguments << options # If we don't have a rendering format, set one early. self.render_as ||= (@action.render_as || :console) end def main status = false # Call the method associated with the provided action (e.g., 'find'). unless @action puts Puppet::Face[:help, :current].help(@face.name) raise "#{face} does not respond to action #{arguments.first}" end # We need to do arity checking here because this is generic code # calling generic methods – that have argument defaulting. We need to # make sure we don't accidentally pass the options as the first # argument to a method that takes one argument. eg: # # puppet facts find # => options => {} # @arguments => [{}] # => @face.send :bar, {} # # def face.bar(argument, options = {}) # => bar({}, {}) # oops! we thought the options were the # # positional argument!! # # We could also fix this by making it mandatory to pass the options on # every call, but that would make the Ruby API much more annoying to # work with; having the defaulting is a much nicer convention to have. # # We could also pass the arguments implicitly, by having a magic # 'options' method that was visible in the scope of the action, which # returned the right stuff. # # That sounds attractive, but adds complications to all sorts of # things, especially when you think about how to pass options when you # are writing Ruby code that calls multiple faces. Especially if # faces are involved in that. ;) # # --daniel 2011-04-27 if (arity = @action.positional_arg_count) > 0 unless (count = arguments.length) == arity then s = arity == 2 ? '' : 's' raise ArgumentError, "puppet #{@face.name} #{@action.name} takes #{arity-1} argument#{s}, but you gave #{count-1}" end end result = @face.send(@action.name, *arguments) - puts render(result) unless result.nil? + puts render(result, arguments) unless result.nil? status = true rescue Exception => detail puts detail.backtrace if Puppet[:trace] Puppet.err detail.to_s Puppet.err "Try 'puppet help #{@face.name} #{@action.name}' for usage" ensure exit status end end diff --git a/lib/puppet/application/kick.rb b/lib/puppet/application/kick.rb index 72f0608f6..bf6178e7a 100644 --- a/lib/puppet/application/kick.rb +++ b/lib/puppet/application/kick.rb @@ -1,335 +1,337 @@ require 'puppet/application' class Puppet::Application::Kick < Puppet::Application should_not_parse_config attr_accessor :hosts, :tags, :classes option("--all","-a") option("--foreground","-f") option("--debug","-d") option("--ping","-P") option("--test") option("--host HOST") do |arg| @hosts << arg end option("--tag TAG", "-t") do |arg| @tags << arg end option("--class CLASS", "-c") do |arg| @classes << arg end option("--no-fqdn", "-n") do |arg| options[:fqdn] = false end option("--parallel PARALLEL", "-p") do |arg| begin options[:parallel] = Integer(arg) rescue $stderr.puts "Could not convert #{arg.inspect} to an integer" exit(23) end end def help <<-HELP puppet-kick(8) -- Remotely control puppet agent ======== SYNOPSIS -------- Trigger a puppet agent run on a set of hosts. USAGE ----- puppet kick [-a|--all] [-c|--class ] [-d|--debug] [-f|--foreground] [-h|--help] [--host ] [--no-fqdn] [--ignoreschedules] [-t|--tag ] [--test] [-p|--ping] [ [...]] DESCRIPTION ----------- This script can be used to connect to a set of machines running 'puppet agent' and trigger them to run their configurations. The most common usage would be to specify a class of hosts and a set of tags, and 'puppet kick' would look up in LDAP all of the hosts matching that class, then connect to each host and trigger a run of all of the objects with the specified tags. If you are not storing your host configurations in LDAP, you can specify hosts manually. You will most likely have to run 'puppet kick' as root to get access to the SSL certificates. 'puppet kick' reads 'puppet master''s configuration file, so that it can copy things like LDAP settings. USAGE NOTES ----------- Puppet kick is useless unless puppet agent is listening for incoming connections and allowing access to the `run` endpoint. This entails starting the agent with `listen = true` in its puppet.conf file, and allowing access to the `/run` path in its auth.conf file; see `http://docs.puppetlabs.com/guides/rest_auth_conf.html` for more details. Additionally, due to a known bug, you must make sure a namespaceauth.conf file exists in puppet agent's $confdir. This file will not be consulted, and may be left empty. 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 ' as an argument. See the configuration file documentation at http://docs.puppetlabs.com/references/latest/configuration.html for the full list of acceptable parameters. A commented list of all configuration options can also be generated by running puppet master with '--genconfig'. * --all: Connect to all available hosts. Requires LDAP support at this point. * --class: Specify a class of machines to which to connect. This only works if you have LDAP configured, at the moment. * --debug: Enable full debugging. * --foreground: Run each configuration in the foreground; that is, when connecting to a host, do not return until the host has finished its run. The default is false. * --help: Print this help message * --host: A specific host to which to connect. This flag can be specified more than once. * --ignoreschedules: Whether the client should ignore schedules when running its configuration. This can be used to force the client to perform work it would not normally perform so soon. The default is false. * --parallel: How parallel to make the connections. Parallelization is provided by forking for each client to which to connect. The default is 1, meaning serial execution. * --tag: Specify a tag for selecting the objects to apply. Does not work with the --test option. * --test: Print the hosts you would connect to but do not actually connect. This option requires LDAP support at this point. * --ping: Do a ICMP echo against the target host. Skip hosts that don't respond to ping. EXAMPLE ------- $ sudo puppet kick -p 10 -t remotefile -t webserver host1 host2 AUTHOR ------ Luke Kanies COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end def run_command @hosts += command_line.args options[:test] ? test : main end def test puts "Skipping execution in test mode" exit(0) end def main Puppet.warning "Failed to load ruby LDAP library. LDAP functionality will not be available" unless Puppet.features.ldap? require 'puppet/util/ldap/connection' todo = @hosts.dup failures = [] # Now do the actual work go = true while go # If we don't have enough children in process and we still have hosts left to # do, then do the next host. if @children.length < options[:parallel] and ! todo.empty? host = todo.shift pid = fork do run_for_host(host) end @children[pid] = host else # Else, see if we can reap a process. begin pid = Process.wait if host = @children[pid] # Remove our host from the list of children, so the parallelization # continues working. @children.delete(pid) failures << host if $CHILD_STATUS.exitstatus != 0 print "#{host} finished with exit code #{$CHILD_STATUS.exitstatus}\n" else $stderr.puts "Could not find host for PID #{pid} with status #{$CHILD_STATUS.exitstatus}" end rescue Errno::ECHILD # There are no children left, so just exit unless there are still # children left to do. next unless todo.empty? if failures.empty? puts "Finished" exit(0) else puts "Failed: #{failures.join(", ")}" exit(3) end end end end end def run_for_host(host) if options[:ping] out = %x{ping -c 1 #{host}} unless $CHILD_STATUS == 0 $stderr.print "Could not contact #{host}\n" exit($CHILD_STATUS) end end require 'puppet/run' Puppet::Run.indirection.terminus_class = :rest port = Puppet[:puppetport] url = ["https://#{host}:#{port}", "production", "run", host].join('/') print "Triggering #{host}\n" begin run_options = { :tags => @tags, :background => ! options[:foreground], :ignoreschedules => options[:ignoreschedules] } run = Puppet::Run.indirection.save(Puppet::Run.new( run_options ), url) puts "Getting status" result = run.status puts "status is #{result}" rescue => detail puts detail.backtrace if Puppet[:trace] $stderr.puts "Host #{host} failed: #{detail}\n" exit(2) end case result when "success"; exit(0) when "running" $stderr.puts "Host #{host} is already running" exit(3) else $stderr.puts "Host #{host} returned unknown answer '#{result}'" exit(12) end end def initialize(*args) super @hosts = [] @classes = [] @tags = [] end def preinit [:INT, :TERM].each do |signal| Signal.trap(signal) do $stderr.puts "Cancelling" exit(1) end end options[:parallel] = 1 options[:verbose] = true options[:fqdn] = true options[:ignoreschedules] = false options[:foreground] = false end def setup + raise Puppet::Error.new("Puppet kick is not supported on Microsoft Windows") if Puppet.features.microsoft_windows? + if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end # Now parse the config Puppet.parse_config if Puppet[:node_terminus] == "ldap" and (options[:all] or @classes) if options[:all] @hosts = Puppet::Node.indirection.search("whatever", :fqdn => options[:fqdn]).collect { |node| node.name } puts "all: #{@hosts.join(", ")}" else @hosts = [] @classes.each do |klass| list = Puppet::Node.indirection.search("whatever", :fqdn => options[:fqdn], :class => klass).collect { |node| node.name } puts "#{klass}: #{list.join(", ")}" @hosts += list end end elsif ! @classes.empty? $stderr.puts "You must be using LDAP to specify host classes" exit(24) end @children = {} # If we get a signal, then kill all of our children and get out. [:INT, :TERM].each do |signal| Signal.trap(signal) do Puppet.notice "Caught #{signal}; shutting down" @children.each do |pid, host| Process.kill("INT", pid) end waitall exit(1) end end end end diff --git a/lib/puppet/face/module/list.rb b/lib/puppet/face/module/list.rb index 772990069..7162dfe46 100644 --- a/lib/puppet/face/module/list.rb +++ b/lib/puppet/face/module/list.rb @@ -1,64 +1,84 @@ Puppet::Face.define(:module, '1.0.0') do action(:list) do summary "List installed modules" description <<-HEREDOC List puppet modules from a specific environment, specified modulepath or - default to listing modules in the default modulepath: + default to listing modules in the default modulepath. The output will + include information about unmet module dependencies based on information + from module metadata. #{Puppet.settings[:modulepath]} HEREDOC returns "hash of paths to module objects" option "--env ENVIRONMENT" do summary "Which environments' modules to list" end option "--modulepath MODULEPATH" do summary "Which directories to look for modules in" end examples <<-EOT List installed modules: $ puppet module list /etc/puppet/modules bacula (0.0.2) /usr/share/puppet/modules apache (0.0.3) bacula (0.0.1) List installed modules from a specified environment: $ puppet module list --env 'test' + Missing dependency `stdlib`: + `rrd` (0.0.2) requires `puppetlabs/stdlib` (>= 2.2.0) + /tmp/puppet/modules rrd (0.0.2) List installed modules from a specified modulepath: $ puppet module list --modulepath /tmp/facts1:/tmp/facts2 /tmp/facts1 stdlib /tmp/facts2 nginx (1.0.0) EOT when_invoked do |options| Puppet[:modulepath] = options[:modulepath] if options[:modulepath] environment = Puppet::Node::Environment.new(options[:env]) environment.modules_by_path end - when_rendering :console do |modules_by_path| + when_rendering :console do |modules_by_path, options| output = '' + + Puppet[:modulepath] = options[:modulepath] if options[:modulepath] + environment = Puppet::Node::Environment.new(options[:env]) + + dependency_errors = false + + environment.modules.sort_by {|mod| mod.name}.each do |mod| + mod.unmet_dependencies.sort_by {|dep| dep[:name]}.each do |dep| + dependency_errors = true + $stderr.puts dep[:error] + end + end + + output << "\n" if dependency_errors + modules_by_path.each do |path, modules| output << "#{path}\n" - modules.each do |mod| + modules.sort_by {|mod| mod.name }.each do |mod| version_string = mod.version ? "(#{mod.version})" : '' output << " #{mod.name} #{version_string}\n" end end output end end end diff --git a/lib/puppet/file_serving/configuration/parser.rb b/lib/puppet/file_serving/configuration/parser.rb index 83b75e28f..d581bbf5d 100644 --- a/lib/puppet/file_serving/configuration/parser.rb +++ b/lib/puppet/file_serving/configuration/parser.rb @@ -1,115 +1,115 @@ require 'puppet/file_serving/configuration' require 'puppet/util/loadedfile' class Puppet::FileServing::Configuration::Parser < Puppet::Util::LoadedFile Mount = Puppet::FileServing::Mount MODULES = 'modules' # Parse our configuration file. def parse raise("File server configuration #{self.file} does not exist") unless FileTest.exists?(self.file) raise("Cannot read file server configuration #{self.file}") unless FileTest.readable?(self.file) @mounts = {} @count = 0 File.open(self.file) { |f| mount = nil - f.each { |line| + f.each_line { |line| # Have the count increment at the top, in case we throw exceptions. @count += 1 case line when /^\s*#/; next # skip comments when /^\s*$/; next # skip blank lines when /\[([-\w]+)\]/ mount = newmount($1) when /^\s*(\w+)\s+(.+?)(\s*#.*)?$/ var = $1 value = $2 value.strip! raise(ArgumentError, "Fileserver configuration file does not use '=' as a separator") if value =~ /^=/ case var when "path" path(mount, value) when "allow" allow(mount, value) when "deny" deny(mount, value) else raise ArgumentError.new("Invalid argument '#{var}'", @count, file) end else raise ArgumentError.new("Invalid line '#{line.chomp}'", @count, file) end } } validate @mounts end private # Allow a given pattern access to a mount. def allow(mount, value) # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] x = value.split(/\s*,\s*/).each { |val| begin mount.info "allowing #{val} access" mount.allow(val) rescue Puppet::AuthStoreError => detail raise ArgumentError.new(detail.to_s, @count, file) end } end # Deny a given pattern access to a mount. def deny(mount, value) # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] x = value.split(/\s*,\s*/).each { |val| begin mount.info "denying #{val} access" mount.deny(val) rescue Puppet::AuthStoreError => detail raise ArgumentError.new(detail.to_s, @count, file) end } end # Create a new mount. def newmount(name) raise ArgumentError, "#{@mounts[name]} is already mounted at #{name}", @count, file if @mounts.include?(name) case name when "modules" mount = Mount::Modules.new(name) when "plugins" mount = Mount::Plugins.new(name) else mount = Mount::File.new(name) end @mounts[name] = mount mount end # Set the path for a mount. def path(mount, value) if mount.respond_to?(:path=) begin mount.path = value rescue ArgumentError => detail Puppet.err "Removing mount #{mount.name}: #{detail}" @mounts.delete(mount.name) end else Puppet.warning "The '#{mount.name}' module can not have a path. Ignoring attempt to set it" end end # Make sure all of our mounts are valid. We have to do this after the fact # because details are added over time as the file is parsed. def validate @mounts.each { |name, mount| mount.validate } end end diff --git a/lib/puppet/indirector/file_bucket_file/file.rb b/lib/puppet/indirector/file_bucket_file/file.rb index 6f6b6ff6f..231940d11 100644 --- a/lib/puppet/indirector/file_bucket_file/file.rb +++ b/lib/puppet/indirector/file_bucket_file/file.rb @@ -1,136 +1,136 @@ require 'puppet/indirector/code' require 'puppet/file_bucket/file' require 'puppet/util/checksums' require 'fileutils' module Puppet::FileBucketFile class File < Puppet::Indirector::Code include Puppet::Util::Checksums desc "Store files in a directory set based on their checksums." def initialize Puppet.settings.use(:filebucket) end def find( request ) checksum, files_original_path = request_to_checksum_and_path( request ) dir_path = path_for(request.options[:bucket_path], checksum) file_path = ::File.join(dir_path, 'contents') return nil unless ::File.exists?(file_path) return nil unless path_match(dir_path, files_original_path) if request.options[:diff_with] hash_protocol = sumtype(checksum) file2_path = path_for(request.options[:bucket_path], request.options[:diff_with], 'contents') raise "could not find diff_with #{request.options[:diff_with]}" unless ::File.exists?(file2_path) return `diff #{file_path.inspect} #{file2_path.inspect}` else contents = IO.binread(file_path) Puppet.info "FileBucket read #{checksum}" model.new(contents) end end def head(request) checksum, files_original_path = request_to_checksum_and_path(request) dir_path = path_for(request.options[:bucket_path], checksum) ::File.exists?(::File.join(dir_path, 'contents')) and path_match(dir_path, files_original_path) end def save( request ) instance = request.instance checksum, files_original_path = request_to_checksum_and_path(request) save_to_disk(instance, files_original_path) instance.to_s end private def path_match(dir_path, files_original_path) return true unless files_original_path # if no path was provided, it's a match paths_path = ::File.join(dir_path, 'paths') return false unless ::File.exists?(paths_path) ::File.open(paths_path) do |f| - f.each do |line| + f.each_line do |line| return true if line.chomp == files_original_path end end return false end def save_to_disk( bucket_file, files_original_path ) filename = path_for(bucket_file.bucket_path, bucket_file.checksum_data, 'contents') dir_path = path_for(bucket_file.bucket_path, bucket_file.checksum_data) paths_path = ::File.join(dir_path, 'paths') # If the file already exists, do nothing. if ::File.exist?(filename) verify_identical_file!(bucket_file) else # Make the directories if necessary. unless ::File.directory?(dir_path) Puppet::Util.withumask(0007) do ::FileUtils.mkdir_p(dir_path) end end Puppet.info "FileBucket adding #{bucket_file.checksum}" # Write the file to disk. Puppet::Util.withumask(0007) do ::File.open(filename, ::File::WRONLY|::File::CREAT, 0440) do |of| of.binmode of.print bucket_file.contents end ::File.open(paths_path, ::File::WRONLY|::File::CREAT, 0640) do |of| # path will be written below end end end unless path_match(dir_path, files_original_path) ::File.open(paths_path, 'a') do |f| f.puts(files_original_path) end end end def request_to_checksum_and_path( request ) checksum_type, checksum, path = request.key.split(/\//, 3) if path == '' # Treat "md5//" like "md5/" path = nil end raise "Unsupported checksum type #{checksum_type.inspect}" if checksum_type != 'md5' raise "Invalid checksum #{checksum.inspect}" if checksum !~ /^[0-9a-f]{32}$/ [checksum, path] end def path_for(bucket_path, digest, subfile = nil) bucket_path ||= Puppet[:bucketdir] dir = ::File.join(digest[0..7].split("")) basedir = ::File.join(bucket_path, dir, digest) return basedir unless subfile ::File.join(basedir, subfile) end # If conflict_check is enabled, verify that the passed text is # the same as the text in our file. def verify_identical_file!(bucket_file) disk_contents = IO.binread(path_for(bucket_file.bucket_path, bucket_file.checksum_data, 'contents')) # If the contents don't match, then we've found a conflict. # Unlikely, but quite bad. if disk_contents != bucket_file.contents raise Puppet::FileBucket::BucketError, "Got passed new contents for sum #{bucket_file.checksum}" else Puppet.info "FileBucket got a duplicate file #{bucket_file.checksum}" end end end end diff --git a/lib/puppet/interface/action.rb b/lib/puppet/interface/action.rb index 60ddb2ca3..3f2f41273 100644 --- a/lib/puppet/interface/action.rb +++ b/lib/puppet/interface/action.rb @@ -1,318 +1,322 @@ require 'puppet/interface' require 'puppet/interface/documentation' require 'prettyprint' class Puppet::Interface::Action extend Puppet::Interface::DocGen include Puppet::Interface::FullDocs def initialize(face, name, attrs = {}) raise "#{name.inspect} is an invalid action name" unless name.to_s =~ /^[a-z]\w*$/ @face = face @name = name.to_sym # The few bits of documentation we actually demand. The default license # is a favour to our end users; if you happen to get that in a core face # report it as a bug, please. --daniel 2011-04-26 @authors = [] @license = 'All Rights Reserved' attrs.each do |k, v| send("#{k}=", v) end # @options collects the added options in the order they're declared. # @options_hash collects the options keyed by alias for quick lookups. @options = [] @options_hash = {} @when_rendering = {} end # This is not nice, but it is the easiest way to make us behave like the # Ruby Method object rather than UnboundMethod. Duplication is vaguely # annoying, but at least we are a shallow clone. --daniel 2011-04-12 def __dup_and_rebind_to(to) bound_version = self.dup bound_version.instance_variable_set(:@face, to) return bound_version end def to_s() "#{@face}##{@name}" end attr_reader :name attr_reader :face attr_accessor :default def default? !!@default end ######################################################################## # Documentation... attr_doc :returns attr_doc :arguments def synopsis build_synopsis(@face.name, default? ? nil : name, arguments) end ######################################################################## # Support for rendering formats and all. def when_rendering(type) unless type.is_a? Symbol raise ArgumentError, "The rendering format must be a symbol, not #{type.class.name}" end # Do we have a rendering hook for this name? return @when_rendering[type].bind(@face) if @when_rendering.has_key? type # How about by another name? alt = type.to_s.sub(/^to_/, '').to_sym return @when_rendering[alt].bind(@face) if @when_rendering.has_key? alt # Guess not, nothing to run. return nil end def set_rendering_method_for(type, proc) unless proc.is_a? Proc msg = "The second argument to set_rendering_method_for must be a Proc" msg += ", not #{proc.class.name}" unless proc.nil? raise ArgumentError, msg end - if proc.arity != 1 then - msg = "when_rendering methods take one argument, the result, not " + + if proc.arity != 1 and proc.arity != (@positional_arg_count + 1) + msg = "the when_rendering method for the #{@face.name} face #{name} action " + msg += "takes either just one argument, the result of when_invoked, " + msg += "or the result plus the #{@positional_arg_count} arguments passed " + msg += "to the when_invoked block, not " if proc.arity < 0 then msg += "a variable number" else msg += proc.arity.to_s end raise ArgumentError, msg end unless type.is_a? Symbol raise ArgumentError, "The rendering format must be a symbol, not #{type.class.name}" end if @when_rendering.has_key? type then raise ArgumentError, "You can't define a rendering method for #{type} twice" end # Now, the ugly bit. We add the method to our interface object, and # retrieve it, to rotate through the dance of getting a suitable method # object out of the whole process. --daniel 2011-04-18 @when_rendering[type] = @face.__send__( :__add_method, __render_method_name_for(type), proc) end def __render_method_name_for(type) :"#{name}_when_rendering_#{type}" end private :__render_method_name_for attr_accessor :render_as def render_as=(value) @render_as = value.to_sym end ######################################################################## # Initially, this was defined to allow the @action.invoke pattern, which is # a very natural way to invoke behaviour given our introspection # capabilities. Heck, our initial plan was to have the faces delegate to # the action object for invocation and all. # # It turns out that we have a binding problem to solve: @face was bound to # the parent class, not the subclass instance, and we don't pass the # appropriate context or change the binding enough to make this work. # # We could hack around it, by either mandating that you pass the context in # to invoke, or try to get the binding right, but that has probably got # subtleties that we don't instantly think of – especially around threads. # # So, we are pulling this method for now, and will return it to life when we # have the time to resolve the problem. For now, you should replace... # # @action = @face.get_action(name) # @action.invoke(arg1, arg2, arg3) # # ...with... # # @action = @face.get_action(name) # @face.send(@action.name, arg1, arg2, arg3) # # I understand that is somewhat cumbersome, but it functions as desired. # --daniel 2011-03-31 # # PS: This code is left present, but commented, to support this chunk of # documentation, for the benefit of the reader. # # def invoke(*args, &block) # @face.send(name, *args, &block) # end # We need to build an instance method as a wrapper, using normal code, to be # able to expose argument defaulting between the caller and definer in the # Ruby API. An extra method is, sadly, required for Ruby 1.8 to work since # it doesn't expose bind on a block. # # Hopefully we can improve this when we finally shuffle off the last of Ruby # 1.8 support, but that looks to be a few "enterprise" release eras away, so # we are pretty stuck with this for now. # # Patches to make this work more nicely with Ruby 1.9 using runtime version # checking and all are welcome, provided that they don't change anything # outside this little ol' bit of code and all. # # Incidentally, we though about vendoring evil-ruby and actually adjusting # the internal C structure implementation details under the hood to make # this stuff work, because it would have been cleaner. Which gives you an # idea how motivated we were to make this cleaner. Sorry. # --daniel 2011-03-31 attr_reader :positional_arg_count attr_accessor :when_invoked def when_invoked=(block) internal_name = "#{@name} implementation, required on Ruby 1.8".to_sym arity = @positional_arg_count = block.arity if arity == 0 then # This will never fire on 1.8.7, which treats no arguments as "*args", # but will on 1.9.2, which treats it as "no arguments". Which bites, # because this just begs for us to wind up in the horrible situation # where a 1.8 vs 1.9 error bites our end users. --daniel 2011-04-19 raise ArgumentError, "when_invoked requires at least one argument (options) for action #{@name}" elsif arity > 0 then range = Range.new(1, arity - 1) decl = range.map { |x| "arg#{x}" } << "options = {}" optn = "" args = "[" + (range.map { |x| "arg#{x}" } << "options").join(", ") + "]" else range = Range.new(1, arity.abs - 1) decl = range.map { |x| "arg#{x}" } << "*rest" optn = "rest << {} unless rest.last.is_a?(Hash)" if arity == -1 then args = "rest" else args = "[" + range.map { |x| "arg#{x}" }.join(", ") + "] + rest" end end file = __FILE__ + "+eval[wrapper]" line = __LINE__ + 2 # <== points to the same line as 'def' in the wrapper. wrapper = <