diff --git a/puppet/mysql/.fixtures.yml b/puppet/mysql/.fixtures.yml new file mode 100644 index 000000000..cecf6f508 --- /dev/null +++ b/puppet/mysql/.fixtures.yml @@ -0,0 +1,5 @@ +fixtures: + repositories: + "stdlib": "git://github.com/puppetlabs/puppetlabs-stdlib" + symlinks: + "mysql": "#{source_dir}" diff --git a/puppet/mysql/.gemfile b/puppet/mysql/.gemfile new file mode 100644 index 000000000..9aad840c0 --- /dev/null +++ b/puppet/mysql/.gemfile @@ -0,0 +1,5 @@ +source :rubygems + +puppetversion = ENV.key?('PUPPET_VERSION') ? "= #{ENV['PUPPET_VERSION']}" : ['>= 2.7'] +gem 'puppet', puppetversion +gem 'puppetlabs_spec_helper', '>= 0.1.0' diff --git a/puppet/mysql/.travis.yml b/puppet/mysql/.travis.yml new file mode 100644 index 000000000..066317e14 --- /dev/null +++ b/puppet/mysql/.travis.yml @@ -0,0 +1,17 @@ +language: ruby +rvm: + - 1.8.7 +before_script: + - "[ '2.6.9' = $PUPPET_VERSION ] && git clone git://github.com/puppetlabs/puppetlabs-create_resources.git spec/fixtures/modules/create_resources || true" +after_script: +script: "rake spec" +branches: + only: + - master +env: + - PUPPET_VERSION=2.7.13 + - PUPPET_VERSION=2.7.6 + - PUPPET_VERSION=2.6.9 +notifications: + email: false +gemfile: .gemfile diff --git a/puppet/mysql/CHANGELOG b/puppet/mysql/CHANGELOG new file mode 100644 index 000000000..05bca7500 --- /dev/null +++ b/puppet/mysql/CHANGELOG @@ -0,0 +1,128 @@ +2012-08-23 - Version 0.5.0 +* Add puppetlabs/stdlib as requirement +* Add validation for mysql privs in provider +* Add `pidfile` parameter to mysql::config +* Add `ensure` parameter to mysql::db +* Add Amazon linux support +* Change `bind_address` parameter to be optional in my.cnf template +* Fix quoting root passwords + +2012-07-24 - Version 0.4.0 +* Fix various bugs regarding database names +* FreeBSD support +* Allow specifying the storage engine +* Add a backup class +* Add a security class to purge default accounts + +2012-05-03 - Version 0.3.0 +* #14218 Query the database for available privileges +* Add mysql::java class for java connector installation +* Use correct error log location on different distros +* Fix set_mysql_rootpw to properly depend on my.cnf + +2012-04-11 - Version 0.2.0 + +2012-03-19 - William Van Hevelingen +* (#13203) Add ssl support (f7e0ea5) + +2012-03-18 - Nan Liu +* Travis ci before script needs success exit code. (0ea463b) + +2012-03-18 - Nan Liu +* Fix Puppet 2.6 compilation issues. (9ebbbc4) + +2012-03-16 - Nan Liu +* Add travis.ci for testing multiple puppet versions. (33c72ef) + +2012-03-15 - William Van Hevelingen +* (#13163) Datadir should be configurable (f353fc6) + +2012-03-16 - Nan Liu +* Document create_resources dependency. (558a59c) + +2012-03-16 - Nan Liu +* Fix spec test issues related to error message. (eff79b5) + +2012-03-16 - Nan Liu +* Fix mysql service on Ubuntu. (72da2c5) + +2012-03-16 - Dan Bode +* Add more spec test coverage (55e399d) + +2012-03-16 - Nan Liu +* (#11963) Fix spec test due to path changes. (1700349) + +2012-03-07 - François Charlier +* Add a test to check path for 'mysqld-restart' (b14c7d1) + +2012-03-07 - François Charlier +* Fix path for 'mysqld-restart' (1a9ae6b) + +2012-03-15 - Dan Bode +* Add rspec-puppet tests for mysql::config (907331a) + +2012-03-15 - Dan Bode +* Moved class dependency between sever and config to server (da62ad6) + +2012-03-14 - Dan Bode +* Notify mysql restart from set_mysql_rootpw exec (0832a2c) + +2012-03-15 - Nan Liu +* Add documentation related to osfamily fact. (8265d28) + +2012-03-14 - Dan Bode +* Mention osfamily value in failure message (e472d3b) + +2012-03-14 - Dan Bode +* Fix bug when querying for all database users (015490c) + +2012-02-09 - Nan Liu +* Major refactor of mysql module. (b1f90fd) + +2012-01-11 - Justin Ellison +* Ruby and Python's MySQL libraries are named differently on different distros. (1e926b4) + +2012-01-11 - Justin Ellison +* Per @ghoneycutt, we should fail explicitly and explain why. (09af083) + +2012-01-11 - Justin Ellison +* Removing duplicate declaration (7513d03) + +2012-01-10 - Justin Ellison +* Use socket value from params class instead of hardcoding. (663e97c) + +2012-01-10 - Justin Ellison +* Instead of hardcoding the config file target, pull it from mysql::params (031a47d) + +2012-01-10 - Justin Ellison +* Moved $socket to within the case to toggle between distros. Added a $config_file variable to allow per-distro config file destinations. (360eacd) + +2012-01-10 - Justin Ellison +* Pretty sure this is a bug, 99% of Linux distros out there won't ever hit the default. (3462e6b) + +2012-02-09 - William Van Hevelingen +* Changed the README to use markdown (3b7dfeb) + +2012-02-04 - Daniel Black +* (#12412) mysqltuner.pl update (b809e6f) + +2011-11-17 - Matthias Pigulla +* (#11363) Add two missing privileges to grant: event_priv, trigger_priv (d15c9d1) + +2011-12-20 - Jeff McCune +* (minor) Fixup typos in Modulefile metadata (a0ed6a1) + +2011-12-19 - Carl Caum +* Only notify Exec to import sql if sql is given (0783c74) + +2011-12-19 - Carl Caum +* (#11508) Only load sql_scripts on DB creation (e3b9fd9) + +2011-12-13 - Justin Ellison +* Require not needed due to implicit dependencies (3058feb) + +2011-12-13 - Justin Ellison +* Bug #11375: puppetlabs-mysql fails on CentOS/RHEL (a557b8d) + +2011-06-03 - Dan Bode - 0.0.1 +* initial commit diff --git a/puppet/mysql/LICENSE b/puppet/mysql/LICENSE new file mode 100644 index 000000000..8d968b6cb --- /dev/null +++ b/puppet/mysql/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/puppet/mysql/Modulefile b/puppet/mysql/Modulefile new file mode 100644 index 000000000..f807daf0a --- /dev/null +++ b/puppet/mysql/Modulefile @@ -0,0 +1,9 @@ +name 'puppetlabs-mysql' +version '0.5.0' +source 'git://github.com/puppetlabs/puppetlabs-mysql.git' +author 'Puppet Labs' +license 'Apache 2.0' +summary 'Mysql module' +description 'Mysql module' +project_page 'http://github.com/puppetlabs/puppetlabs-mysql' +dependency 'puppetlabs/stdlib', '>= 2.2.1' diff --git a/puppet/mysql/README.md b/puppet/mysql/README.md new file mode 100644 index 000000000..98ad9ce28 --- /dev/null +++ b/puppet/mysql/README.md @@ -0,0 +1,126 @@ +# Mysql module for Puppet + +This module manages mysql on Linux (RedHat/Debian) distros. A native mysql provider implements database resource type to handle database, database user, and database permission. + +## Description + +This module uses the fact osfamily which is supported by Facter 1.6.1+. If you do not have facter 1.6.1 in your environment, the following manifests will provide the same functionality in site.pp (before declaring any node): + + if ! $::osfamily { + case $::operatingsystem { + 'RedHat', 'Fedora', 'CentOS', 'Scientific', 'SLC', 'Ascendos', 'CloudLinux', 'PSBM', 'OracleLinux', 'OVS', 'OEL': { + $osfamily = 'RedHat' + } + 'ubuntu', 'debian': { + $osfamily = 'Debian' + } + 'SLES', 'SLED', 'OpenSuSE', 'SuSE': { + $osfamily = 'Suse' + } + 'Solaris', 'Nexenta': { + $osfamily = 'Solaris' + } + default: { + $osfamily = $::operatingsystem + } + } + } + +This module depends on creates_resources function which is introduced in Puppet 2.7. Users on puppet 2.6 can use the following module which provides this functionality: + +[http://github.com/puppetlabs/puppetlabs-create_resources](http://github.com/puppetlabs/puppetlabs-create_resources) + +This module is based on work by David Schmitt. The following contributor have contributed patches to this module (beyond Puppet Labs): + +* Christian G. Warden +* Daniel Black +* Justin Ellison +* Lowe Schmidt +* Matthias Pigulla +* William Van Hevelingen +* Michael Arnold + +## Usage + +### mysql +Installs the mysql-client package. + + class { 'mysql': } + +### mysql::java +Installs mysql bindings for java. + + class { 'mysql::java': } + +### mysql::python +Installs mysql bindings for python. + + class { 'mysql::python': } + +### mysql::ruby +Installs mysql bindings for ruby. + + class { 'mysql::ruby': } + +### mysql::server +Installs mysql-server packages, configures my.cnf and starts mysqld service: + + class { 'mysql::server': + config_hash => { 'root_password' => 'foo' } + } + +Database login information stored in `/root/.my.cnf`. + +### mysql::db +Creates a database with a user and assign some privileges. + + mysql::db { 'mydb': + user => 'myuser', + password => 'mypass', + host => 'localhost', + grant => ['all'], + } + +### mysql::backup +Installs a mysql backup script, cronjob, and priviledged backup user. + + class { 'mysql::backup': + backupuser => 'myuser', + backuppassword => 'mypassword', + backupdir => '/tmp/backups', + } + +### Providers for database types: +MySQL provider supports puppet resources command: + + $ puppet resource database + database { 'information_schema': + ensure => 'present', + charset => 'utf8', + } + database { 'mysql': + ensure => 'present', + charset => 'latin1', + } + +The custom resources can be used in any other manifests: + + database { 'mydb': + charset => 'latin1', + } + + database_user { 'bob@localhost': + password_hash => mysql_password('foo') + } + + database_grant { 'user@localhost/database': + privileges => ['all'] , + # Or specify individual privileges with columns from the mysql.db table: + # privileges => ['Select_priv', 'Insert_priv', 'Update_priv', 'Delete_priv'] + } + +A resource default can be specified to handle dependency: + + Database { + require => Class['mysql::server'], + } diff --git a/puppet/mysql/Rakefile b/puppet/mysql/Rakefile new file mode 100644 index 000000000..cd3d37995 --- /dev/null +++ b/puppet/mysql/Rakefile @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/rake_tasks' diff --git a/puppet/mysql/TODO b/puppet/mysql/TODO new file mode 100644 index 000000000..391329393 --- /dev/null +++ b/puppet/mysql/TODO @@ -0,0 +1,8 @@ +The best that I can tell is that this code traces back to David Schmitt. It has been forked many times since then :) + +1. you cannot add databases to an instance that has a root password +2. you have to specify username as USER@BLAH or it cannot be found +3. mysql_grant does not complain if user does not exist +4. Needs support for pre-seeding on debian +5. the types may need to take user/password +6. rather or not to configure /etc/.my.cnf should be configurable diff --git a/puppet/mysql/files/mysqltuner.pl b/puppet/mysql/files/mysqltuner.pl new file mode 100644 index 000000000..f61881ce8 --- /dev/null +++ b/puppet/mysql/files/mysqltuner.pl @@ -0,0 +1,966 @@ +#!/usr/bin/perl -w +# mysqltuner.pl - Version 1.2.0 +# High Performance MySQL Tuning Script +# Copyright (C) 2006-2011 Major Hayden - major@mhtx.net +# +# For the latest updates, please visit http://mysqltuner.com/ +# Git repository available at http://github.com/rackerhacker/MySQLTuner-perl +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# This project would not be possible without help from: +# Matthew Montgomery Paul Kehrer Dave Burgess +# Jonathan Hinds Mike Jackson Nils Breunese +# Shawn Ashlee Luuk Vosslamber Ville Skytta +# Trent Hornibrook Jason Gill Mark Imbriaco +# Greg Eden Aubin Galinotti Giovanni Bechis +# Bill Bradford Ryan Novosielski Michael Scheidell +# Blair Christensen Hans du Plooy Victor Trac +# Everett Barnes Tom Krouper Gary Barrueto +# Simon Greenaway Adam Stein Isart Montane +# Baptiste M. +# +# Inspired by Matthew Montgomery's tuning-primer.sh script: +# http://forge.mysql.com/projects/view.php?id=44 +# +use strict; +use warnings; +use diagnostics; +use File::Spec; +use Getopt::Long; + +# Set up a few variables for use in the script +my $tunerversion = "1.2.0"; +my (@adjvars, @generalrec); + +# Set defaults +my %opt = ( + "nobad" => 0, + "nogood" => 0, + "noinfo" => 0, + "nocolor" => 0, + "forcemem" => 0, + "forceswap" => 0, + "host" => 0, + "socket" => 0, + "port" => 0, + "user" => 0, + "pass" => 0, + "skipsize" => 0, + "checkversion" => 0, + ); + +# Gather the options from the command line +GetOptions(\%opt, + 'nobad', + 'nogood', + 'noinfo', + 'nocolor', + 'forcemem=i', + 'forceswap=i', + 'host=s', + 'socket=s', + 'port=i', + 'user=s', + 'pass=s', + 'skipsize', + 'checkversion', + 'help', + ); + +if (defined $opt{'help'} && $opt{'help'} == 1) { usage(); } + +sub usage { + # Shown with --help option passed + print "\n". + " MySQLTuner $tunerversion - MySQL High Performance Tuning Script\n". + " Bug reports, feature requests, and downloads at http://mysqltuner.com/\n". + " Maintained by Major Hayden (major\@mhtx.net) - Licensed under GPL\n". + "\n". + " Important Usage Guidelines:\n". + " To run the script with the default options, run the script without arguments\n". + " Allow MySQL server to run for at least 24-48 hours before trusting suggestions\n". + " Some routines may require root level privileges (script will provide warnings)\n". + " You must provide the remote server's total memory when connecting to other servers\n". + "\n". + " Connection and Authentication\n". + " --host Connect to a remote host to perform tests (default: localhost)\n". + " --socket Use a different socket for a local connection\n". + " --port Port to use for connection (default: 3306)\n". + " --user Username to use for authentication\n". + " --pass Password to use for authentication\n". + "\n". + " Performance and Reporting Options\n". + " --skipsize Don't enumerate tables and their types/sizes (default: on)\n". + " (Recommended for servers with many tables)\n". + " --checkversion Check for updates to MySQLTuner (default: don't check)\n". + " --forcemem Amount of RAM installed in megabytes\n". + " --forceswap Amount of swap memory configured in megabytes\n". + "\n". + " Output Options:\n". + " --nogood Remove OK responses\n". + " --nobad Remove negative/suggestion responses\n". + " --noinfo Remove informational responses\n". + " --nocolor Don't print output in color\n". + "\n"; + exit; +} + +my $devnull = File::Spec->devnull(); + +# Setting up the colors for the print styles +my $good = ($opt{nocolor} == 0)? "[\e[0;32mOK\e[0m]" : "[OK]" ; +my $bad = ($opt{nocolor} == 0)? "[\e[0;31m!!\e[0m]" : "[!!]" ; +my $info = ($opt{nocolor} == 0)? "[\e[0;34m--\e[0m]" : "[--]" ; + +# Functions that handle the print styles +sub goodprint { print $good." ".$_[0] unless ($opt{nogood} == 1); } +sub infoprint { print $info." ".$_[0] unless ($opt{noinfo} == 1); } +sub badprint { print $bad." ".$_[0] unless ($opt{nobad} == 1); } +sub redwrap { return ($opt{nocolor} == 0)? "\e[0;31m".$_[0]."\e[0m" : $_[0] ; } +sub greenwrap { return ($opt{nocolor} == 0)? "\e[0;32m".$_[0]."\e[0m" : $_[0] ; } + +# Calculates the parameter passed in bytes, and then rounds it to one decimal place +sub hr_bytes { + my $num = shift; + if ($num >= (1024**3)) { #GB + return sprintf("%.1f",($num/(1024**3)))."G"; + } elsif ($num >= (1024**2)) { #MB + return sprintf("%.1f",($num/(1024**2)))."M"; + } elsif ($num >= 1024) { #KB + return sprintf("%.1f",($num/1024))."K"; + } else { + return $num."B"; + } +} + +# Calculates the parameter passed in bytes, and then rounds it to the nearest integer +sub hr_bytes_rnd { + my $num = shift; + if ($num >= (1024**3)) { #GB + return int(($num/(1024**3)))."G"; + } elsif ($num >= (1024**2)) { #MB + return int(($num/(1024**2)))."M"; + } elsif ($num >= 1024) { #KB + return int(($num/1024))."K"; + } else { + return $num."B"; + } +} + +# Calculates the parameter passed to the nearest power of 1000, then rounds it to the nearest integer +sub hr_num { + my $num = shift; + if ($num >= (1000**3)) { # Billions + return int(($num/(1000**3)))."B"; + } elsif ($num >= (1000**2)) { # Millions + return int(($num/(1000**2)))."M"; + } elsif ($num >= 1000) { # Thousands + return int(($num/1000))."K"; + } else { + return $num; + } +} + +# Calculates uptime to display in a more attractive form +sub pretty_uptime { + my $uptime = shift; + my $seconds = $uptime % 60; + my $minutes = int(($uptime % 3600) / 60); + my $hours = int(($uptime % 86400) / (3600)); + my $days = int($uptime / (86400)); + my $uptimestring; + if ($days > 0) { + $uptimestring = "${days}d ${hours}h ${minutes}m ${seconds}s"; + } elsif ($hours > 0) { + $uptimestring = "${hours}h ${minutes}m ${seconds}s"; + } elsif ($minutes > 0) { + $uptimestring = "${minutes}m ${seconds}s"; + } else { + $uptimestring = "${seconds}s"; + } + return $uptimestring; +} + +# Retrieves the memory installed on this machine +my ($physical_memory,$swap_memory,$duflags); +sub os_setup { + sub memerror { + badprint "Unable to determine total memory/swap; use '--forcemem' and '--forceswap'\n"; + exit; + } + my $os = `uname`; + $duflags = ($os =~ /Linux/) ? '-b' : ''; + if ($opt{'forcemem'} > 0) { + $physical_memory = $opt{'forcemem'} * 1048576; + infoprint "Assuming $opt{'forcemem'} MB of physical memory\n"; + if ($opt{'forceswap'} > 0) { + $swap_memory = $opt{'forceswap'} * 1048576; + infoprint "Assuming $opt{'forceswap'} MB of swap space\n"; + } else { + $swap_memory = 0; + badprint "Assuming 0 MB of swap space (use --forceswap to specify)\n"; + } + } else { + if ($os =~ /Linux/) { + $physical_memory = `free -b | grep Mem | awk '{print \$2}'` or memerror; + $swap_memory = `free -b | grep Swap | awk '{print \$2}'` or memerror; + } elsif ($os =~ /Darwin/) { + $physical_memory = `sysctl -n hw.memsize` or memerror; + $swap_memory = `sysctl -n vm.swapusage | awk '{print \$3}' | sed 's/\..*\$//'` or memerror; + } elsif ($os =~ /NetBSD|OpenBSD/) { + $physical_memory = `sysctl -n hw.physmem` or memerror; + if ($physical_memory < 0) { + $physical_memory = `sysctl -n hw.physmem64` or memerror; + } + $swap_memory = `swapctl -l | grep '^/' | awk '{ s+= \$2 } END { print s }'` or memerror; + } elsif ($os =~ /BSD/) { + $physical_memory = `sysctl -n hw.realmem`; + $swap_memory = `swapinfo | grep '^/' | awk '{ s+= \$2 } END { print s }'`; + } elsif ($os =~ /SunOS/) { + $physical_memory = `/usr/sbin/prtconf | grep Memory | cut -f 3 -d ' '` or memerror; + chomp($physical_memory); + $physical_memory = $physical_memory*1024*1024; + } elsif ($os =~ /AIX/) { + $physical_memory = `lsattr -El sys0 | grep realmem | awk '{print \$2}'` or memerror; + chomp($physical_memory); + $physical_memory = $physical_memory*1024; + $swap_memory = `lsps -as | awk -F"(MB| +)" '/MB /{print \$2}'` or memerror; + chomp($swap_memory); + $swap_memory = $swap_memory*1024*1024; + } + } + chomp($physical_memory); +} + +# Checks to see if a MySQL login is possible +my ($mysqllogin,$doremote,$remotestring); +sub mysql_setup { + $doremote = 0; + $remotestring = ''; + my $command = `which mysqladmin`; + chomp($command); + if (! -e $command) { + badprint "Unable to find mysqladmin in your \$PATH. Is MySQL installed?\n"; + exit; + } + # Are we being asked to connect via a socket? + if ($opt{socket} ne 0) { + $remotestring = " -S $opt{socket}"; + } + # Are we being asked to connect to a remote server? + if ($opt{host} ne 0) { + chomp($opt{host}); + $opt{port} = ($opt{port} eq 0)? 3306 : $opt{port} ; + # If we're doing a remote connection, but forcemem wasn't specified, we need to exit + if ($opt{'forcemem'} eq 0) { + badprint "The --forcemem option is required for remote connections\n"; + exit; + } + infoprint "Performing tests on $opt{host}:$opt{port}\n"; + $remotestring = " -h $opt{host} -P $opt{port}"; + $doremote = 1; + } + # Did we already get a username and password passed on the command line? + if ($opt{user} ne 0 and $opt{pass} ne 0) { + $mysqllogin = "-u $opt{user} -p'$opt{pass}'".$remotestring; + my $loginstatus = `mysqladmin ping $mysqllogin 2>&1`; + if ($loginstatus =~ /mysqld is alive/) { + goodprint "Logged in using credentials passed on the command line\n"; + return 1; + } else { + badprint "Attempted to use login credentials, but they were invalid\n"; + exit 0; + } + } + if ( -r "/etc/psa/.psa.shadow" and $doremote == 0 ) { + # It's a Plesk box, use the available credentials + $mysqllogin = "-u admin -p`cat /etc/psa/.psa.shadow`"; + my $loginstatus = `mysqladmin ping $mysqllogin 2>&1`; + unless ($loginstatus =~ /mysqld is alive/) { + badprint "Attempted to use login credentials from Plesk, but they failed.\n"; + exit 0; + } + } elsif ( -r "/etc/mysql/debian.cnf" and $doremote == 0 ){ + # We have a debian maintenance account, use it + $mysqllogin = "--defaults-file=/etc/mysql/debian.cnf"; + my $loginstatus = `mysqladmin $mysqllogin ping 2>&1`; + if ($loginstatus =~ /mysqld is alive/) { + goodprint "Logged in using credentials from debian maintenance account.\n"; + return 1; + } else { + badprint "Attempted to use login credentials from debian maintenance account, but they failed.\n"; + exit 0; + } + } else { + # It's not Plesk or debian, we should try a login + my $loginstatus = `mysqladmin $remotestring ping 2>&1`; + if ($loginstatus =~ /mysqld is alive/) { + # Login went just fine + $mysqllogin = " $remotestring "; + # Did this go well because of a .my.cnf file or is there no password set? + my $userpath = `printenv HOME`; + if (length($userpath) > 0) { + chomp($userpath); + } + unless ( -e "${userpath}/.my.cnf" ) { + badprint "Successfully authenticated with no password - SECURITY RISK!\n"; + } + return 1; + } else { + print STDERR "Please enter your MySQL administrative login: "; + my $name = <>; + print STDERR "Please enter your MySQL administrative password: "; + system("stty -echo >$devnull 2>&1"); + my $password = <>; + system("stty echo >$devnull 2>&1"); + chomp($password); + chomp($name); + $mysqllogin = "-u $name"; + if (length($password) > 0) { + $mysqllogin .= " -p'$password'"; + } + $mysqllogin .= $remotestring; + my $loginstatus = `mysqladmin ping $mysqllogin 2>&1`; + if ($loginstatus =~ /mysqld is alive/) { + print STDERR "\n"; + if (! length($password)) { + # Did this go well because of a .my.cnf file or is there no password set? + my $userpath = `ls -d ~`; + chomp($userpath); + unless ( -e "$userpath/.my.cnf" ) { + badprint "Successfully authenticated with no password - SECURITY RISK!\n"; + } + } + return 1; + } else { + print "\n".$bad." Attempted to use login credentials, but they were invalid.\n"; + exit 0; + } + exit 0; + } + } +} + +# Populates all of the variable and status hashes +my (%mystat,%myvar,$dummyselect); +sub get_all_vars { + # We need to initiate at least one query so that our data is useable + $dummyselect = `mysql $mysqllogin -Bse "SELECT VERSION();"`; + my @mysqlvarlist = `mysql $mysqllogin -Bse "SHOW /*!50000 GLOBAL */ VARIABLES;"`; + foreach my $line (@mysqlvarlist) { + $line =~ /([a-zA-Z_]*)\s*(.*)/; + $myvar{$1} = $2; + } + my @mysqlstatlist = `mysql $mysqllogin -Bse "SHOW /*!50000 GLOBAL */ STATUS;"`; + foreach my $line (@mysqlstatlist) { + $line =~ /([a-zA-Z_]*)\s*(.*)/; + $mystat{$1} = $2; + } + # Workaround for MySQL bug #59393 wrt. ignore-builtin-innodb + if (($myvar{'ignore_builtin_innodb'} || "") eq "ON") { + $myvar{'have_innodb'} = "NO"; + } + # have_* for engines is deprecated and will be removed in MySQL 5.6; + # check SHOW ENGINES and set corresponding old style variables. + # Also works around MySQL bug #59393 wrt. skip-innodb + my @mysqlenginelist = `mysql $mysqllogin -Bse "SHOW ENGINES;" 2>$devnull`; + foreach my $line (@mysqlenginelist) { + if ($line =~ /^([a-zA-Z_]+)\s+(\S+)/) { + my $engine = lc($1); + if ($engine eq "federated" || $engine eq "blackhole") { + $engine .= "_engine"; + } elsif ($engine eq "berkeleydb") { + $engine = "bdb"; + } + my $val = ($2 eq "DEFAULT") ? "YES" : $2; + $myvar{"have_$engine"} = $val; + } + } +} + +sub security_recommendations { + print "\n-------- Security Recommendations -------------------------------------------\n"; + my @mysqlstatlist = `mysql $mysqllogin -Bse "SELECT CONCAT(user, '\@', host) FROM mysql.user WHERE password = '' OR password IS NULL;"`; + if (@mysqlstatlist) { + foreach my $line (sort @mysqlstatlist) { + chomp($line); + badprint "User '".$line."' has no password set.\n"; + } + } else { + goodprint "All database users have passwords assigned\n"; + } +} + +sub get_replication_status { + my $slave_status = `mysql $mysqllogin -Bse "show slave status\\G"`; + my ($io_running) = ($slave_status =~ /slave_io_running\S*\s+(\S+)/i); + my ($sql_running) = ($slave_status =~ /slave_sql_running\S*\s+(\S+)/i); + if ($io_running eq 'Yes' && $sql_running eq 'Yes') { + if ($myvar{'read_only'} eq 'OFF') { + badprint "This replication slave is running with the read_only option disabled."; + } else { + goodprint "This replication slave is running with the read_only option enabled."; + } + } +} + +# Checks for updates to MySQLTuner +sub validate_tuner_version { + print "\n-------- General Statistics --------------------------------------------------\n"; + if ($opt{checkversion} eq 0) { + infoprint "Skipped version check for MySQLTuner script\n"; + return; + } + my $update; + my $url = "http://mysqltuner.com/versioncheck.php?v=$tunerversion"; + if (-e "/usr/bin/curl") { + $update = `/usr/bin/curl --connect-timeout 5 '$url' 2>$devnull`; + chomp($update); + } elsif (-e "/usr/bin/wget") { + $update = `/usr/bin/wget -e timestamping=off -T 5 -O - '$url' 2>$devnull`; + chomp($update); + } + if ($update eq 1) { + badprint "There is a new version of MySQLTuner available\n"; + } elsif ($update eq 0) { + goodprint "You have the latest version of MySQLTuner\n"; + } else { + infoprint "Unable to check for the latest MySQLTuner version\n"; + } +} + +# Checks for supported or EOL'ed MySQL versions +my ($mysqlvermajor,$mysqlverminor); +sub validate_mysql_version { + ($mysqlvermajor,$mysqlverminor) = $myvar{'version'} =~ /(\d)\.(\d)/; + if (!mysql_version_ge(5)) { + badprint "Your MySQL version ".$myvar{'version'}." is EOL software! Upgrade soon!\n"; + } elsif (mysql_version_ge(6)) { + badprint "Currently running unsupported MySQL version ".$myvar{'version'}."\n"; + } else { + goodprint "Currently running supported MySQL version ".$myvar{'version'}."\n"; + } +} + +# Checks if MySQL version is greater than equal to (major, minor) +sub mysql_version_ge { + my ($maj, $min) = @_; + return $mysqlvermajor > $maj || ($mysqlvermajor == $maj && $mysqlverminor >= ($min || 0)); +} + +# Checks for 32-bit boxes with more than 2GB of RAM +my ($arch); +sub check_architecture { + if ($doremote eq 1) { return; } + if (`uname` =~ /SunOS/ && `isainfo -b` =~ /64/) { + $arch = 64; + goodprint "Operating on 64-bit architecture\n"; + } elsif (`uname` !~ /SunOS/ && `uname -m` =~ /64/) { + $arch = 64; + goodprint "Operating on 64-bit architecture\n"; + } elsif (`uname` =~ /AIX/ && `bootinfo -K` =~ /64/) { + $arch = 64; + goodprint "Operating on 64-bit architecture\n"; + } else { + $arch = 32; + if ($physical_memory > 2147483648) { + badprint "Switch to 64-bit OS - MySQL cannot currently use all of your RAM\n"; + } else { + goodprint "Operating on 32-bit architecture with less than 2GB RAM\n"; + } + } +} + +# Start up a ton of storage engine counts/statistics +my (%enginestats,%enginecount,$fragtables); +sub check_storage_engines { + if ($opt{skipsize} eq 1) { + print "\n-------- Storage Engine Statistics -------------------------------------------\n"; + infoprint "Skipped due to --skipsize option\n"; + return; + } + print "\n-------- Storage Engine Statistics -------------------------------------------\n"; + infoprint "Status: "; + my $engines; + $engines .= (defined $myvar{'have_archive'} && $myvar{'have_archive'} eq "YES")? greenwrap "+Archive " : redwrap "-Archive " ; + $engines .= (defined $myvar{'have_bdb'} && $myvar{'have_bdb'} eq "YES")? greenwrap "+BDB " : redwrap "-BDB " ; + $engines .= (defined $myvar{'have_federated_engine'} && $myvar{'have_federated_engine'} eq "YES")? greenwrap "+Federated " : redwrap "-Federated " ; + $engines .= (defined $myvar{'have_innodb'} && $myvar{'have_innodb'} eq "YES")? greenwrap "+InnoDB " : redwrap "-InnoDB " ; + $engines .= (defined $myvar{'have_isam'} && $myvar{'have_isam'} eq "YES")? greenwrap "+ISAM " : redwrap "-ISAM " ; + $engines .= (defined $myvar{'have_ndbcluster'} && $myvar{'have_ndbcluster'} eq "YES")? greenwrap "+NDBCluster " : redwrap "-NDBCluster " ; + print "$engines\n"; + if (mysql_version_ge(5)) { + # MySQL 5 servers can have table sizes calculated quickly from information schema + my @templist = `mysql $mysqllogin -Bse "SELECT ENGINE,SUM(DATA_LENGTH),COUNT(ENGINE) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema','mysql') AND ENGINE IS NOT NULL GROUP BY ENGINE ORDER BY ENGINE ASC;"`; + foreach my $line (@templist) { + my ($engine,$size,$count); + ($engine,$size,$count) = $line =~ /([a-zA-Z_]*)\s+(\d+)\s+(\d+)/; + if (!defined($size)) { next; } + $enginestats{$engine} = $size; + $enginecount{$engine} = $count; + } + $fragtables = `mysql $mysqllogin -Bse "SELECT COUNT(TABLE_NAME) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema','mysql') AND Data_free > 0 AND NOT ENGINE='MEMORY';"`; + chomp($fragtables); + } else { + # MySQL < 5 servers take a lot of work to get table sizes + my @tblist; + # Now we build a database list, and loop through it to get storage engine stats for tables + my @dblist = `mysql $mysqllogin -Bse "SHOW DATABASES"`; + foreach my $db (@dblist) { + chomp($db); + if ($db eq "information_schema") { next; } + my @ixs = (1, 6, 9); + if (!mysql_version_ge(4, 1)) { + # MySQL 3.23/4.0 keeps Data_Length in the 5th (0-based) column + @ixs = (1, 5, 8); + } + push(@tblist, map { [ (split)[@ixs] ] } `mysql $mysqllogin -Bse "SHOW TABLE STATUS FROM \\\`$db\\\`"`); + } + # Parse through the table list to generate storage engine counts/statistics + $fragtables = 0; + foreach my $tbl (@tblist) { + my ($engine, $size, $datafree) = @$tbl; + if (defined $enginestats{$engine}) { + $enginestats{$engine} += $size; + $enginecount{$engine} += 1; + } else { + $enginestats{$engine} = $size; + $enginecount{$engine} = 1; + } + if ($datafree > 0) { + $fragtables++; + } + } + } + while (my ($engine,$size) = each(%enginestats)) { + infoprint "Data in $engine tables: ".hr_bytes_rnd($size)." (Tables: ".$enginecount{$engine}.")"."\n"; + } + # If the storage engine isn't being used, recommend it to be disabled + if (!defined $enginestats{'InnoDB'} && defined $myvar{'have_innodb'} && $myvar{'have_innodb'} eq "YES") { + badprint "InnoDB is enabled but isn't being used\n"; + push(@generalrec,"Add skip-innodb to MySQL configuration to disable InnoDB"); + } + if (!defined $enginestats{'BerkeleyDB'} && defined $myvar{'have_bdb'} && $myvar{'have_bdb'} eq "YES") { + badprint "BDB is enabled but isn't being used\n"; + push(@generalrec,"Add skip-bdb to MySQL configuration to disable BDB"); + } + if (!defined $enginestats{'ISAM'} && defined $myvar{'have_isam'} && $myvar{'have_isam'} eq "YES") { + badprint "ISAM is enabled but isn't being used\n"; + push(@generalrec,"Add skip-isam to MySQL configuration to disable ISAM (MySQL > 4.1.0)"); + } + # Fragmented tables + if ($fragtables > 0) { + badprint "Total fragmented tables: $fragtables\n"; + push(@generalrec,"Run OPTIMIZE TABLE to defragment tables for better performance"); + } else { + goodprint "Total fragmented tables: $fragtables\n"; + } +} + +my %mycalc; +sub calculations { + if ($mystat{'Questions'} < 1) { + badprint "Your server has not answered any queries - cannot continue..."; + exit 0; + } + # Per-thread memory + if (mysql_version_ge(4)) { + $mycalc{'per_thread_buffers'} = $myvar{'read_buffer_size'} + $myvar{'read_rnd_buffer_size'} + $myvar{'sort_buffer_size'} + $myvar{'thread_stack'} + $myvar{'join_buffer_size'}; + } else { + $mycalc{'per_thread_buffers'} = $myvar{'record_buffer'} + $myvar{'record_rnd_buffer'} + $myvar{'sort_buffer'} + $myvar{'thread_stack'} + $myvar{'join_buffer_size'}; + } + $mycalc{'total_per_thread_buffers'} = $mycalc{'per_thread_buffers'} * $myvar{'max_connections'}; + $mycalc{'max_total_per_thread_buffers'} = $mycalc{'per_thread_buffers'} * $mystat{'Max_used_connections'}; + + # Server-wide memory + $mycalc{'max_tmp_table_size'} = ($myvar{'tmp_table_size'} > $myvar{'max_heap_table_size'}) ? $myvar{'max_heap_table_size'} : $myvar{'tmp_table_size'} ; + $mycalc{'server_buffers'} = $myvar{'key_buffer_size'} + $mycalc{'max_tmp_table_size'}; + $mycalc{'server_buffers'} += (defined $myvar{'innodb_buffer_pool_size'}) ? $myvar{'innodb_buffer_pool_size'} : 0 ; + $mycalc{'server_buffers'} += (defined $myvar{'innodb_additional_mem_pool_size'}) ? $myvar{'innodb_additional_mem_pool_size'} : 0 ; + $mycalc{'server_buffers'} += (defined $myvar{'innodb_log_buffer_size'}) ? $myvar{'innodb_log_buffer_size'} : 0 ; + $mycalc{'server_buffers'} += (defined $myvar{'query_cache_size'}) ? $myvar{'query_cache_size'} : 0 ; + + # Global memory + $mycalc{'max_used_memory'} = $mycalc{'server_buffers'} + $mycalc{"max_total_per_thread_buffers"}; + $mycalc{'total_possible_used_memory'} = $mycalc{'server_buffers'} + $mycalc{'total_per_thread_buffers'}; + $mycalc{'pct_physical_memory'} = int(($mycalc{'total_possible_used_memory'} * 100) / $physical_memory); + + # Slow queries + $mycalc{'pct_slow_queries'} = int(($mystat{'Slow_queries'}/$mystat{'Questions'}) * 100); + + # Connections + $mycalc{'pct_connections_used'} = int(($mystat{'Max_used_connections'}/$myvar{'max_connections'}) * 100); + $mycalc{'pct_connections_used'} = ($mycalc{'pct_connections_used'} > 100) ? 100 : $mycalc{'pct_connections_used'} ; + + # Key buffers + if (mysql_version_ge(4, 1)) { + $mycalc{'pct_key_buffer_used'} = sprintf("%.1f",(1 - (($mystat{'Key_blocks_unused'} * $myvar{'key_cache_block_size'}) / $myvar{'key_buffer_size'})) * 100); + } + if ($mystat{'Key_read_requests'} > 0) { + $mycalc{'pct_keys_from_mem'} = sprintf("%.1f",(100 - (($mystat{'Key_reads'} / $mystat{'Key_read_requests'}) * 100))); + } else { + $mycalc{'pct_keys_from_mem'} = 0; + } + if ($doremote eq 0 and !mysql_version_ge(5)) { + my $size = 0; + $size += (split)[0] for `find $myvar{'datadir'} -name "*.MYI" 2>&1 | xargs du -L $duflags 2>&1`; + $mycalc{'total_myisam_indexes'} = $size; + } elsif (mysql_version_ge(5)) { + $mycalc{'total_myisam_indexes'} = `mysql $mysqllogin -Bse "SELECT IFNULL(SUM(INDEX_LENGTH),0) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('information_schema') AND ENGINE = 'MyISAM';"`; + } + if (defined $mycalc{'total_myisam_indexes'} and $mycalc{'total_myisam_indexes'} == 0) { + $mycalc{'total_myisam_indexes'} = "fail"; + } elsif (defined $mycalc{'total_myisam_indexes'}) { + chomp($mycalc{'total_myisam_indexes'}); + } + + # Query cache + if (mysql_version_ge(4)) { + $mycalc{'query_cache_efficiency'} = sprintf("%.1f",($mystat{'Qcache_hits'} / ($mystat{'Com_select'} + $mystat{'Qcache_hits'})) * 100); + if ($myvar{'query_cache_size'}) { + $mycalc{'pct_query_cache_used'} = sprintf("%.1f",100 - ($mystat{'Qcache_free_memory'} / $myvar{'query_cache_size'}) * 100); + } + if ($mystat{'Qcache_lowmem_prunes'} == 0) { + $mycalc{'query_cache_prunes_per_day'} = 0; + } else { + $mycalc{'query_cache_prunes_per_day'} = int($mystat{'Qcache_lowmem_prunes'} / ($mystat{'Uptime'}/86400)); + } + } + + # Sorting + $mycalc{'total_sorts'} = $mystat{'Sort_scan'} + $mystat{'Sort_range'}; + if ($mycalc{'total_sorts'} > 0) { + $mycalc{'pct_temp_sort_table'} = int(($mystat{'Sort_merge_passes'} / $mycalc{'total_sorts'}) * 100); + } + + # Joins + $mycalc{'joins_without_indexes'} = $mystat{'Select_range_check'} + $mystat{'Select_full_join'}; + $mycalc{'joins_without_indexes_per_day'} = int($mycalc{'joins_without_indexes'} / ($mystat{'Uptime'}/86400)); + + # Temporary tables + if ($mystat{'Created_tmp_tables'} > 0) { + if ($mystat{'Created_tmp_disk_tables'} > 0) { + $mycalc{'pct_temp_disk'} = int(($mystat{'Created_tmp_disk_tables'} / ($mystat{'Created_tmp_tables'} + $mystat{'Created_tmp_disk_tables'})) * 100); + } else { + $mycalc{'pct_temp_disk'} = 0; + } + } + + # Table cache + if ($mystat{'Opened_tables'} > 0) { + $mycalc{'table_cache_hit_rate'} = int($mystat{'Open_tables'}*100/$mystat{'Opened_tables'}); + } else { + $mycalc{'table_cache_hit_rate'} = 100; + } + + # Open files + if ($myvar{'open_files_limit'} > 0) { + $mycalc{'pct_files_open'} = int($mystat{'Open_files'}*100/$myvar{'open_files_limit'}); + } + + # Table locks + if ($mystat{'Table_locks_immediate'} > 0) { + if ($mystat{'Table_locks_waited'} == 0) { + $mycalc{'pct_table_locks_immediate'} = 100; + } else { + $mycalc{'pct_table_locks_immediate'} = int($mystat{'Table_locks_immediate'}*100/($mystat{'Table_locks_waited'} + $mystat{'Table_locks_immediate'})); + } + } + + # Thread cache + $mycalc{'thread_cache_hit_rate'} = int(100 - (($mystat{'Threads_created'} / $mystat{'Connections'}) * 100)); + + # Other + if ($mystat{'Connections'} > 0) { + $mycalc{'pct_aborted_connections'} = int(($mystat{'Aborted_connects'}/$mystat{'Connections'}) * 100); + } + if ($mystat{'Questions'} > 0) { + $mycalc{'total_reads'} = $mystat{'Com_select'}; + $mycalc{'total_writes'} = $mystat{'Com_delete'} + $mystat{'Com_insert'} + $mystat{'Com_update'} + $mystat{'Com_replace'}; + if ($mycalc{'total_reads'} == 0) { + $mycalc{'pct_reads'} = 0; + $mycalc{'pct_writes'} = 100; + } else { + $mycalc{'pct_reads'} = int(($mycalc{'total_reads'}/($mycalc{'total_reads'}+$mycalc{'total_writes'})) * 100); + $mycalc{'pct_writes'} = 100-$mycalc{'pct_reads'}; + } + } + + # InnoDB + if ($myvar{'have_innodb'} eq "YES") { + $mycalc{'innodb_log_size_pct'} = ($myvar{'innodb_log_file_size'} * 100 / $myvar{'innodb_buffer_pool_size'}); + } +} + +sub mysql_stats { + print "\n-------- Performance Metrics -------------------------------------------------\n"; + # Show uptime, queries per second, connections, traffic stats + my $qps; + if ($mystat{'Uptime'} > 0) { $qps = sprintf("%.3f",$mystat{'Questions'}/$mystat{'Uptime'}); } + if ($mystat{'Uptime'} < 86400) { push(@generalrec,"MySQL started within last 24 hours - recommendations may be inaccurate"); } + infoprint "Up for: ".pretty_uptime($mystat{'Uptime'})." (".hr_num($mystat{'Questions'}). + " q [".hr_num($qps)." qps], ".hr_num($mystat{'Connections'})." conn,". + " TX: ".hr_num($mystat{'Bytes_sent'}).", RX: ".hr_num($mystat{'Bytes_received'}).")\n"; + infoprint "Reads / Writes: ".$mycalc{'pct_reads'}."% / ".$mycalc{'pct_writes'}."%\n"; + + # Memory usage + infoprint "Total buffers: ".hr_bytes($mycalc{'server_buffers'})." global + ".hr_bytes($mycalc{'per_thread_buffers'})." per thread ($myvar{'max_connections'} max threads)\n"; + if ($mycalc{'total_possible_used_memory'} > 2*1024*1024*1024 && $arch eq 32) { + badprint "Allocating > 2GB RAM on 32-bit systems can cause system instability\n"; + badprint "Maximum possible memory usage: ".hr_bytes($mycalc{'total_possible_used_memory'})." ($mycalc{'pct_physical_memory'}% of installed RAM)\n"; + } elsif ($mycalc{'pct_physical_memory'} > 85) { + badprint "Maximum possible memory usage: ".hr_bytes($mycalc{'total_possible_used_memory'})." ($mycalc{'pct_physical_memory'}% of installed RAM)\n"; + push(@generalrec,"Reduce your overall MySQL memory footprint for system stability"); + } else { + goodprint "Maximum possible memory usage: ".hr_bytes($mycalc{'total_possible_used_memory'})." ($mycalc{'pct_physical_memory'}% of installed RAM)\n"; + } + + # Slow queries + if ($mycalc{'pct_slow_queries'} > 5) { + badprint "Slow queries: $mycalc{'pct_slow_queries'}% (".hr_num($mystat{'Slow_queries'})."/".hr_num($mystat{'Questions'}).")\n"; + } else { + goodprint "Slow queries: $mycalc{'pct_slow_queries'}% (".hr_num($mystat{'Slow_queries'})."/".hr_num($mystat{'Questions'}).")\n"; + } + if ($myvar{'long_query_time'} > 10) { push(@adjvars,"long_query_time (<= 10)"); } + if (defined($myvar{'log_slow_queries'})) { + if ($myvar{'log_slow_queries'} eq "OFF") { push(@generalrec,"Enable the slow query log to troubleshoot bad queries"); } + } + + # Connections + if ($mycalc{'pct_connections_used'} > 85) { + badprint "Highest connection usage: $mycalc{'pct_connections_used'}% ($mystat{'Max_used_connections'}/$myvar{'max_connections'})\n"; + push(@adjvars,"max_connections (> ".$myvar{'max_connections'}.")"); + push(@adjvars,"wait_timeout (< ".$myvar{'wait_timeout'}.")","interactive_timeout (< ".$myvar{'interactive_timeout'}.")"); + push(@generalrec,"Reduce or eliminate persistent connections to reduce connection usage") + } else { + goodprint "Highest usage of available connections: $mycalc{'pct_connections_used'}% ($mystat{'Max_used_connections'}/$myvar{'max_connections'})\n"; + } + + # Key buffer + if (!defined($mycalc{'total_myisam_indexes'}) and $doremote == 1) { + push(@generalrec,"Unable to calculate MyISAM indexes on remote MySQL server < 5.0.0"); + } elsif ($mycalc{'total_myisam_indexes'} =~ /^fail$/) { + badprint "Cannot calculate MyISAM index size - re-run script as root user\n"; + } elsif ($mycalc{'total_myisam_indexes'} == "0") { + badprint "None of your MyISAM tables are indexed - add indexes immediately\n"; + } else { + if ($myvar{'key_buffer_size'} < $mycalc{'total_myisam_indexes'} && $mycalc{'pct_keys_from_mem'} < 95) { + badprint "Key buffer size / total MyISAM indexes: ".hr_bytes($myvar{'key_buffer_size'})."/".hr_bytes($mycalc{'total_myisam_indexes'})."\n"; + push(@adjvars,"key_buffer_size (> ".hr_bytes($mycalc{'total_myisam_indexes'}).")"); + } else { + goodprint "Key buffer size / total MyISAM indexes: ".hr_bytes($myvar{'key_buffer_size'})."/".hr_bytes($mycalc{'total_myisam_indexes'})."\n"; + } + if ($mystat{'Key_read_requests'} > 0) { + if ($mycalc{'pct_keys_from_mem'} < 95) { + badprint "Key buffer hit rate: $mycalc{'pct_keys_from_mem'}% (".hr_num($mystat{'Key_read_requests'})." cached / ".hr_num($mystat{'Key_reads'})." reads)\n"; + } else { + goodprint "Key buffer hit rate: $mycalc{'pct_keys_from_mem'}% (".hr_num($mystat{'Key_read_requests'})." cached / ".hr_num($mystat{'Key_reads'})." reads)\n"; + } + } else { + # No queries have run that would use keys + } + } + + # Query cache + if (!mysql_version_ge(4)) { + # MySQL versions < 4.01 don't support query caching + push(@generalrec,"Upgrade MySQL to version 4+ to utilize query caching"); + } elsif ($myvar{'query_cache_size'} < 1) { + badprint "Query cache is disabled\n"; + push(@adjvars,"query_cache_size (>= 8M)"); + } elsif ($mystat{'Com_select'} == 0) { + badprint "Query cache cannot be analyzed - no SELECT statements executed\n"; + } else { + if ($mycalc{'query_cache_efficiency'} < 20) { + badprint "Query cache efficiency: $mycalc{'query_cache_efficiency'}% (".hr_num($mystat{'Qcache_hits'})." cached / ".hr_num($mystat{'Qcache_hits'}+$mystat{'Com_select'})." selects)\n"; + push(@adjvars,"query_cache_limit (> ".hr_bytes_rnd($myvar{'query_cache_limit'}).", or use smaller result sets)"); + } else { + goodprint "Query cache efficiency: $mycalc{'query_cache_efficiency'}% (".hr_num($mystat{'Qcache_hits'})." cached / ".hr_num($mystat{'Qcache_hits'}+$mystat{'Com_select'})." selects)\n"; + } + if ($mycalc{'query_cache_prunes_per_day'} > 98) { + badprint "Query cache prunes per day: $mycalc{'query_cache_prunes_per_day'}\n"; + if ($myvar{'query_cache_size'} > 128*1024*1024) { + push(@generalrec,"Increasing the query_cache size over 128M may reduce performance"); + push(@adjvars,"query_cache_size (> ".hr_bytes_rnd($myvar{'query_cache_size'}).") [see warning above]"); + } else { + push(@adjvars,"query_cache_size (> ".hr_bytes_rnd($myvar{'query_cache_size'}).")"); + } + } else { + goodprint "Query cache prunes per day: $mycalc{'query_cache_prunes_per_day'}\n"; + } + } + + # Sorting + if ($mycalc{'total_sorts'} == 0) { + # For the sake of space, we will be quiet here + # No sorts have run yet + } elsif ($mycalc{'pct_temp_sort_table'} > 10) { + badprint "Sorts requiring temporary tables: $mycalc{'pct_temp_sort_table'}% (".hr_num($mystat{'Sort_merge_passes'})." temp sorts / ".hr_num($mycalc{'total_sorts'})." sorts)\n"; + push(@adjvars,"sort_buffer_size (> ".hr_bytes_rnd($myvar{'sort_buffer_size'}).")"); + push(@adjvars,"read_rnd_buffer_size (> ".hr_bytes_rnd($myvar{'read_rnd_buffer_size'}).")"); + } else { + goodprint "Sorts requiring temporary tables: $mycalc{'pct_temp_sort_table'}% (".hr_num($mystat{'Sort_merge_passes'})." temp sorts / ".hr_num($mycalc{'total_sorts'})." sorts)\n"; + } + + # Joins + if ($mycalc{'joins_without_indexes_per_day'} > 250) { + badprint "Joins performed without indexes: $mycalc{'joins_without_indexes'}\n"; + push(@adjvars,"join_buffer_size (> ".hr_bytes($myvar{'join_buffer_size'}).", or always use indexes with joins)"); + push(@generalrec,"Adjust your join queries to always utilize indexes"); + } else { + # For the sake of space, we will be quiet here + # No joins have run without indexes + } + + # Temporary tables + if ($mystat{'Created_tmp_tables'} > 0) { + if ($mycalc{'pct_temp_disk'} > 25 && $mycalc{'max_tmp_table_size'} < 256*1024*1024) { + badprint "Temporary tables created on disk: $mycalc{'pct_temp_disk'}% (".hr_num($mystat{'Created_tmp_disk_tables'})." on disk / ".hr_num($mystat{'Created_tmp_disk_tables'} + $mystat{'Created_tmp_tables'})." total)\n"; + push(@adjvars,"tmp_table_size (> ".hr_bytes_rnd($myvar{'tmp_table_size'}).")"); + push(@adjvars,"max_heap_table_size (> ".hr_bytes_rnd($myvar{'max_heap_table_size'}).")"); + push(@generalrec,"When making adjustments, make tmp_table_size/max_heap_table_size equal"); + push(@generalrec,"Reduce your SELECT DISTINCT queries without LIMIT clauses"); + } elsif ($mycalc{'pct_temp_disk'} > 25 && $mycalc{'max_tmp_table_size'} >= 256) { + badprint "Temporary tables created on disk: $mycalc{'pct_temp_disk'}% (".hr_num($mystat{'Created_tmp_disk_tables'})." on disk / ".hr_num($mystat{'Created_tmp_disk_tables'} + $mystat{'Created_tmp_tables'})." total)\n"; + push(@generalrec,"Temporary table size is already large - reduce result set size"); + push(@generalrec,"Reduce your SELECT DISTINCT queries without LIMIT clauses"); + } else { + goodprint "Temporary tables created on disk: $mycalc{'pct_temp_disk'}% (".hr_num($mystat{'Created_tmp_disk_tables'})." on disk / ".hr_num($mystat{'Created_tmp_disk_tables'} + $mystat{'Created_tmp_tables'})." total)\n"; + } + } else { + # For the sake of space, we will be quiet here + # No temporary tables have been created + } + + # Thread cache + if ($myvar{'thread_cache_size'} eq 0) { + badprint "Thread cache is disabled\n"; + push(@generalrec,"Set thread_cache_size to 4 as a starting value"); + push(@adjvars,"thread_cache_size (start at 4)"); + } else { + if ($mycalc{'thread_cache_hit_rate'} <= 50) { + badprint "Thread cache hit rate: $mycalc{'thread_cache_hit_rate'}% (".hr_num($mystat{'Threads_created'})." created / ".hr_num($mystat{'Connections'})." connections)\n"; + push(@adjvars,"thread_cache_size (> $myvar{'thread_cache_size'})"); + } else { + goodprint "Thread cache hit rate: $mycalc{'thread_cache_hit_rate'}% (".hr_num($mystat{'Threads_created'})." created / ".hr_num($mystat{'Connections'})." connections)\n"; + } + } + + # Table cache + if ($mystat{'Open_tables'} > 0) { + if ($mycalc{'table_cache_hit_rate'} < 20) { + badprint "Table cache hit rate: $mycalc{'table_cache_hit_rate'}% (".hr_num($mystat{'Open_tables'})." open / ".hr_num($mystat{'Opened_tables'})." opened)\n"; + if (mysql_version_ge(5, 1)) { + push(@adjvars,"table_cache (> ".$myvar{'table_open_cache'}.")"); + } else { + push(@adjvars,"table_cache (> ".$myvar{'table_cache'}.")"); + } + push(@generalrec,"Increase table_cache gradually to avoid file descriptor limits"); + } else { + goodprint "Table cache hit rate: $mycalc{'table_cache_hit_rate'}% (".hr_num($mystat{'Open_tables'})." open / ".hr_num($mystat{'Opened_tables'})." opened)\n"; + } + } + + # Open files + if (defined $mycalc{'pct_files_open'}) { + if ($mycalc{'pct_files_open'} > 85) { + badprint "Open file limit used: $mycalc{'pct_files_open'}% (".hr_num($mystat{'Open_files'})."/".hr_num($myvar{'open_files_limit'}).")\n"; + push(@adjvars,"open_files_limit (> ".$myvar{'open_files_limit'}.")"); + } else { + goodprint "Open file limit used: $mycalc{'pct_files_open'}% (".hr_num($mystat{'Open_files'})."/".hr_num($myvar{'open_files_limit'}).")\n"; + } + } + + # Table locks + if (defined $mycalc{'pct_table_locks_immediate'}) { + if ($mycalc{'pct_table_locks_immediate'} < 95) { + badprint "Table locks acquired immediately: $mycalc{'pct_table_locks_immediate'}%\n"; + push(@generalrec,"Optimize queries and/or use InnoDB to reduce lock wait"); + } else { + goodprint "Table locks acquired immediately: $mycalc{'pct_table_locks_immediate'}% (".hr_num($mystat{'Table_locks_immediate'})." immediate / ".hr_num($mystat{'Table_locks_waited'}+$mystat{'Table_locks_immediate'})." locks)\n"; + } + } + + # Performance options + if (!mysql_version_ge(4, 1)) { + push(@generalrec,"Upgrade to MySQL 4.1+ to use concurrent MyISAM inserts"); + } elsif ($myvar{'concurrent_insert'} eq "OFF") { + push(@generalrec,"Enable concurrent_insert by setting it to 'ON'"); + } elsif ($myvar{'concurrent_insert'} eq 0) { + push(@generalrec,"Enable concurrent_insert by setting it to 1"); + } + if ($mycalc{'pct_aborted_connections'} > 5) { + badprint "Connections aborted: ".$mycalc{'pct_aborted_connections'}."%\n"; + push(@generalrec,"Your applications are not closing MySQL connections properly"); + } + + # InnoDB + if (defined $myvar{'have_innodb'} && $myvar{'have_innodb'} eq "YES" && defined $enginestats{'InnoDB'}) { + if ($myvar{'innodb_buffer_pool_size'} > $enginestats{'InnoDB'}) { + goodprint "InnoDB data size / buffer pool: ".hr_bytes($enginestats{'InnoDB'})."/".hr_bytes($myvar{'innodb_buffer_pool_size'})."\n"; + } else { + badprint "InnoDB data size / buffer pool: ".hr_bytes($enginestats{'InnoDB'})."/".hr_bytes($myvar{'innodb_buffer_pool_size'})."\n"; + push(@adjvars,"innodb_buffer_pool_size (>= ".hr_bytes_rnd($enginestats{'InnoDB'}).")"); + } + } +} + +# Take the two recommendation arrays and display them at the end of the output +sub make_recommendations { + print "\n-------- Recommendations -----------------------------------------------------\n"; + if (@generalrec > 0) { + print "General recommendations:\n"; + foreach (@generalrec) { print " ".$_."\n"; } + } + if (@adjvars > 0) { + print "Variables to adjust:\n"; + if ($mycalc{'pct_physical_memory'} > 90) { + print " *** MySQL's maximum memory usage is dangerously high ***\n". + " *** Add RAM before increasing MySQL buffer variables ***\n"; + } + foreach (@adjvars) { print " ".$_."\n"; } + } + if (@generalrec == 0 && @adjvars ==0) { + print "No additional performance recommendations are available.\n" + } + print "\n"; +} + +# --------------------------------------------------------------------------- +# BEGIN 'MAIN' +# --------------------------------------------------------------------------- +print "\n >> MySQLTuner $tunerversion - Major Hayden \n". + " >> Bug reports, feature requests, and downloads at http://mysqltuner.com/\n". + " >> Run with '--help' for additional options and output filtering\n"; +mysql_setup; # Gotta login first +os_setup; # Set up some OS variables +get_all_vars; # Toss variables/status into hashes +validate_tuner_version; # Check current MySQLTuner version +validate_mysql_version; # Check current MySQL version +check_architecture; # Suggest 64-bit upgrade +check_storage_engines; # Show enabled storage engines +security_recommendations; # Display some security recommendations +calculations; # Calculate everything we need +mysql_stats; # Print the server stats +make_recommendations; # Make recommendations based on stats +# --------------------------------------------------------------------------- +# END 'MAIN' +# --------------------------------------------------------------------------- + +# Local variables: +# indent-tabs-mode: t +# cperl-indent-level: 8 +# perl-indent-level: 8 +# End: diff --git a/puppet/mysql/lib/puppet/parser/functions/mysql_password.rb b/puppet/mysql/lib/puppet/parser/functions/mysql_password.rb new file mode 100644 index 000000000..74281b827 --- /dev/null +++ b/puppet/mysql/lib/puppet/parser/functions/mysql_password.rb @@ -0,0 +1,15 @@ +# hash a string as mysql's "PASSWORD()" function would do it +require 'digest/sha1' + +module Puppet::Parser::Functions + newfunction(:mysql_password, :type => :rvalue, :doc => <<-EOS + Returns the mysql password hash from the clear text password. + EOS + ) do |args| + + raise(Puppet::ParseError, "mysql_password(): Wrong number of arguments " + + "given (#{args.size} for 1)") if args.size != 1 + + '*' + Digest::SHA1.hexdigest(Digest::SHA1.digest(args[0])).upcase + end +end diff --git a/puppet/mysql/lib/puppet/provider/database/mysql.rb b/puppet/mysql/lib/puppet/provider/database/mysql.rb new file mode 100644 index 000000000..30bc72ee8 --- /dev/null +++ b/puppet/mysql/lib/puppet/provider/database/mysql.rb @@ -0,0 +1,42 @@ +Puppet::Type.type(:database).provide(:mysql) do + + desc "Manages MySQL database." + + defaultfor :kernel => 'Linux' + + optional_commands :mysql => 'mysql' + optional_commands :mysqladmin => 'mysqladmin' + + def self.instances + mysql("--defaults-file=#{Facter.value(:root_home)}/.my.cnf", '-NBe', "show databases").split("\n").collect do |name| + new(:name => name) + end + end + + def create + mysql("--defaults-file=#{Facter.value(:root_home)}/.my.cnf", '-NBe', "create database `#{@resource[:name]}` character set #{resource[:charset]}") + end + + def destroy + mysqladmin("--defaults-file=#{Facter.value(:root_home)}/.my.cnf", '-f', 'drop', @resource[:name]) + end + + def charset + mysql("--defaults-file=#{Facter.value(:root_home)}/.my.cnf", '-NBe', "show create database `#{resource[:name]}`").match(/.*?(\S+)\s\*\//)[1] + end + + def charset=(value) + mysql("--defaults-file=#{Facter.value(:root_home)}/.my.cnf", '-NBe', "alter database `#{resource[:name]}` CHARACTER SET #{value}") + end + + def exists? + begin + mysql("--defaults-file=#{Facter.value(:root_home)}/.my.cnf", '-NBe', "show databases").match(/^#{@resource[:name]}$/) + rescue => e + debug(e.message) + return nil + end + end + +end + diff --git a/puppet/mysql/lib/puppet/provider/database_grant/mysql.rb b/puppet/mysql/lib/puppet/provider/database_grant/mysql.rb new file mode 100644 index 000000000..865153976 --- /dev/null +++ b/puppet/mysql/lib/puppet/provider/database_grant/mysql.rb @@ -0,0 +1,198 @@ +# A grant is either global or per-db. This can be distinguished by the syntax +# of the name: +# user@host => global +# user@host/db => per-db + +Puppet::Type.type(:database_grant).provide(:mysql) do + + desc "Uses mysql as database." + + defaultfor :kernel => 'Linux' + + optional_commands :mysql => 'mysql' + optional_commands :mysqladmin => 'mysqladmin' + + def self.prefetch(resources) + @user_privs = query_user_privs + @db_privs = query_db_privs + end + + def self.user_privs + @user_privs || query_user_privs + end + + def self.db_privs + @db_privs || query_db_privs + end + + def user_privs + self.class.user_privs + end + + def db_privs + self.class.db_privs + end + + def self.query_user_privs + results = mysql("--defaults-file=#{Facter.value(:root_home)}/.my.cnf", "mysql", "-Be", "describe user") + column_names = results.split(/\n/).map { |l| l.chomp.split(/\t/)[0] } + @user_privs = column_names.delete_if { |e| !( e =~/_priv$/) } + end + + def self.query_db_privs + results = mysql("--defaults-file=#{Facter.value(:root_home)}/.my.cnf", "mysql", "-Be", "describe db") + column_names = results.split(/\n/).map { |l| l.chomp.split(/\t/)[0] } + @db_privs = column_names.delete_if { |e| !(e =~/_priv$/) } + end + + def mysql_flush + mysqladmin "--defaults-file=#{Facter.value(:root_home)}/.my.cnf", "flush-privileges" + end + + # this parses the + def split_name(string) + matches = /^([^@]*)@([^\/]*)(\/(.*))?$/.match(string).captures.compact + case matches.length + when 2 + { + :type => :user, + :user => matches[0], + :host => matches[1] + } + when 4 + { + :type => :db, + :user => matches[0], + :host => matches[1], + :db => matches[3] + } + end + end + + def create_row + unless @resource.should(:privileges).empty? + name = split_name(@resource[:name]) + case name[:type] + when :user + mysql "--defaults-file=#{Facter.value(:root_home)}/.my.cnf", "mysql", "-e", "INSERT INTO user (host, user) VALUES ('%s', '%s')" % [ + name[:host], name[:user], + ] + when :db + mysql "--defaults-file=#{Facter.value(:root_home)}/.my.cnf", "mysql", "-e", "INSERT INTO db (host, user, db) VALUES ('%s', '%s', '%s')" % [ + name[:host], name[:user], name[:db], + ] + end + mysql_flush + end + end + + def destroy + mysql "--defaults-file=#{Facter.value(:root_home)}/.my.cnf", "mysql", "-e", "REVOKE ALL ON '%s'.* FROM '%s@%s'" % [ @resource[:privileges], @resource[:database], @resource[:name], @resource[:host] ] + end + + def row_exists? + name = split_name(@resource[:name]) + fields = [:user, :host] + if name[:type] == :db + fields << :db + end + not mysql("--defaults-file=#{Facter.value(:root_home)}/.my.cnf", "mysql", '-NBe', 'SELECT "1" FROM %s WHERE %s' % [ name[:type], fields.map do |f| "%s=\"%s\"" % [f, name[f]] end.join(' AND ')]).empty? + end + + def all_privs_set? + all_privs = case split_name(@resource[:name])[:type] + when :user + user_privs + when :db + db_privs + end + all_privs = all_privs.collect do |p| p.downcase end.sort.join("|") + privs = privileges.collect do |p| p.downcase end.sort.join("|") + + all_privs == privs + end + + def privileges + name = split_name(@resource[:name]) + privs = "" + + case name[:type] + when :user + privs = mysql "--defaults-file=#{Facter.value(:root_home)}/.my.cnf", "mysql", "-Be", 'select * from mysql.user where user="%s" and host="%s"' % [ name[:user], name[:host] ] + when :db + privs = mysql "--defaults-file=#{Facter.value(:root_home)}/.my.cnf", "mysql", "-Be", 'select * from mysql.db where user="%s" and host="%s" and db="%s"' % [ name[:user], name[:host], name[:db] ] + end + + if privs.match(/^$/) + privs = [] # no result, no privs + else + # returns a line with field names and a line with values, each tab-separated + privs = privs.split(/\n/).map! do |l| l.chomp.split(/\t/) end + # transpose the lines, so we have key/value pairs + privs = privs[0].zip(privs[1]) + privs = privs.select do |p| p[0].match(/_priv$/) and p[1] == 'Y' end + end + + privs.collect do |p| p[0] end + end + + def privileges=(privs) + unless row_exists? + create_row + end + + # puts "Setting privs: ", privs.join(", ") + name = split_name(@resource[:name]) + stmt = '' + where = '' + all_privs = [] + case name[:type] + when :user + stmt = 'update user set ' + where = ' where user="%s" and host="%s"' % [ name[:user], name[:host] ] + all_privs = user_privs + when :db + stmt = 'update db set ' + where = ' where user="%s" and host="%s" and db="%s"' % [ name[:user], name[:host], name[:db] ] + all_privs = db_privs + end + + if privs[0].downcase == 'all' + privs = all_privs + end + + # Downcase the requested priviliges for case-insensitive selection + # we don't map! here because the all_privs object has to remain in + # the same case the DB gave it to us in + privs = privs.map { |p| p.downcase } + + # puts "stmt:", stmt + set = all_privs.collect do |p| "%s = '%s'" % [p, privs.include?(p.downcase) ? 'Y' : 'N'] end.join(', ') + # puts "set:", set + stmt = stmt << set << where + + validate_privs privs, all_privs + mysql "--defaults-file=#{Facter.value(:root_home)}/.my.cnf", "mysql", "-Be", stmt + mysql_flush + end + + def validate_privs(set_privs, all_privs) + all_privs = all_privs.collect { |p| p.downcase } + set_privs = set_privs.collect { |p| p.downcase } + invalid_privs = Array.new + hints = Array.new + # Test each of the user provided privs to see if they exist in all_privs + set_privs.each do |priv| + invalid_privs << priv unless all_privs.include?(priv) + hints << "#{priv}_priv" if all_privs.include?("#{priv}_priv") + end + unless invalid_privs.empty? + # Print a decently helpful and gramatically correct error message + hints = "Did you mean '#{hints.join(',')}'?" unless hints.empty? + p = invalid_privs.size > 1 ? ['s', 'are not valid'] : ['', 'is not valid'] + detail = ["The privilege#{p[0]} '#{invalid_privs.join(',')}' #{p[1]}."] + fail [detail, hints].join(' ') + end + end + +end diff --git a/puppet/mysql/lib/puppet/provider/database_user/mysql.rb b/puppet/mysql/lib/puppet/provider/database_user/mysql.rb new file mode 100644 index 000000000..387cbba4a --- /dev/null +++ b/puppet/mysql/lib/puppet/provider/database_user/mysql.rb @@ -0,0 +1,42 @@ +Puppet::Type.type(:database_user).provide(:mysql) do + + desc "manage users for a mysql database." + + defaultfor :kernel => 'Linux' + + optional_commands :mysql => 'mysql' + optional_commands :mysqladmin => 'mysqladmin' + + def self.instances + users = mysql("--defaults-file=#{Facter.value(:root_home)}/.my.cnf", "mysql", '-BNe' "select concat(User, '@',Host) as User from mysql.user").split("\n") + users.select{ |user| user =~ /.+@/ }.collect do |name| + new(:name => name) + end + end + + def create + mysql("--defaults-file=#{Facter.value(:root_home)}/.my.cnf", "mysql", "-e", "create user '%s' identified by PASSWORD '%s'" % [ @resource[:name].sub("@", "'@'"), @resource.value(:password_hash) ]) + end + + def destroy + mysql("--defaults-file=#{Facter.value(:root_home)}/.my.cnf", "mysql", "-e", "drop user '%s'" % @resource.value(:name).sub("@", "'@'") ) + end + + def password_hash + mysql("--defaults-file=#{Facter.value(:root_home)}/.my.cnf", "mysql", "-NBe", "select password from mysql.user where CONCAT(user, '@', host) = '%s'" % @resource.value(:name)).chomp + end + + def password_hash=(string) + mysql("--defaults-file=#{Facter.value(:root_home)}/.my.cnf", "mysql", "-e", "SET PASSWORD FOR '%s' = '%s'" % [ @resource[:name].sub("@", "'@'"), string ] ) + end + + def exists? + not mysql("--defaults-file=#{Facter.value(:root_home)}/.my.cnf", "mysql", "-NBe", "select '1' from mysql.user where CONCAT(user, '@', host) = '%s'" % @resource.value(:name)).empty? + end + + def flush + @property_hash.clear + mysqladmin "--defaults-file=#{Facter.value(:root_home)}/.my.cnf", "flush-privileges" + end + +end diff --git a/puppet/mysql/lib/puppet/type/database.rb b/puppet/mysql/lib/puppet/type/database.rb new file mode 100644 index 000000000..a27fc007e --- /dev/null +++ b/puppet/mysql/lib/puppet/type/database.rb @@ -0,0 +1,17 @@ +# This has to be a separate type to enable collecting +Puppet::Type.newtype(:database) do + @doc = "Manage databases." + + ensurable + + newparam(:name, :namevar=>true) do + desc "The name of the database." + end + + newproperty(:charset) do + desc "The characterset to use for a database" + defaultto :utf8 + newvalue(/^\S+$/) + end + +end diff --git a/puppet/mysql/lib/puppet/type/database_grant.rb b/puppet/mysql/lib/puppet/type/database_grant.rb new file mode 100644 index 000000000..965695bff --- /dev/null +++ b/puppet/mysql/lib/puppet/type/database_grant.rb @@ -0,0 +1,75 @@ +# This has to be a separate type to enable collecting +Puppet::Type.newtype(:database_grant) do + @doc = "Manage a database user's rights." + #ensurable + + autorequire :database do + # puts "Starting db autoreq for %s" % self[:name] + reqs = [] + matches = self[:name].match(/^([^@]+)@([^\/]+)\/(.+)$/) + unless matches.nil? + reqs << matches[3] + end + # puts "Autoreq: '%s'" % reqs.join(" ") + reqs + end + + autorequire :database_user do + # puts "Starting user autoreq for %s" % self[:name] + reqs = [] + matches = self[:name].match(/^([^@]+)@([^\/]+).*$/) + unless matches.nil? + reqs << "%s@%s" % [ matches[1], matches[2] ] + end + # puts "Autoreq: '%s'" % reqs.join(" ") + reqs + end + + newparam(:name, :namevar=>true) do + desc "The primary key: either user@host for global privilges or user@host/database for database specific privileges" + end + + newproperty(:privileges, :array_matching => :all) do + desc "The privileges the user should have. The possible values are implementation dependent." + + def should_to_s(newvalue = @should) + if newvalue + unless newvalue.is_a?(Array) + newvalue = [ newvalue ] + end + newvalue.collect do |v| v.downcase end.sort.join ", " + else + nil + end + end + + def is_to_s(currentvalue = @is) + if currentvalue + unless currentvalue.is_a?(Array) + currentvalue = [ currentvalue ] + end + currentvalue.collect do |v| v.downcase end.sort.join ", " + else + nil + end + end + + # use the sorted outputs for comparison + def insync?(is) + if defined? @should and @should + case self.should_to_s + when "all" + self.provider.all_privs_set? + when self.is_to_s(is) + true + else + false + end + else + true + end + end + end + +end + diff --git a/puppet/mysql/lib/puppet/type/database_user.rb b/puppet/mysql/lib/puppet/type/database_user.rb new file mode 100644 index 000000000..23af104fa --- /dev/null +++ b/puppet/mysql/lib/puppet/type/database_user.rb @@ -0,0 +1,25 @@ +# This has to be a separate type to enable collecting +Puppet::Type.newtype(:database_user) do + @doc = "Manage a database user. This includes management of users password as well as priveleges" + + ensurable + + newparam(:name, :namevar=>true) do + desc "The name of the user. This uses the 'username@hostname' or username@hostname." + validate do |value| + # https://dev.mysql.com/doc/refman/5.1/en/account-names.html + # Regex should problably be more like this: /^[`'"]?[^`'"]*[`'"]?@[`'"]?[\w%\.]+[`'"]?$/ + raise(ArgumentError, "Invalid database user #{value}") unless value =~ /[\w-]*@[\w%\.:]+/ + username = value.split('@')[0] + if username.size > 16 + raise ArgumentError, "MySQL usernames are limited to a maximum of 16 characters" + end + end + end + + newproperty(:password_hash) do + desc "The password hash of the user. Use mysql_password() for creating such a hash." + newvalue(/\w+/) + end + +end diff --git a/puppet/mysql/manifests/backup.pp b/puppet/mysql/manifests/backup.pp new file mode 100644 index 000000000..5e8056be4 --- /dev/null +++ b/puppet/mysql/manifests/backup.pp @@ -0,0 +1,68 @@ +# Class: mysql::backup +# +# This module handles ... +# +# Parameters: +# [*backupuser*] - The name of the mysql backup user. +# [*backuppassword*] - The password of the mysql backup user. +# [*backupdir*] - The target directory of the mysqldump. +# +# Actions: +# GRANT SELECT, RELOAD, LOCK TABLES ON *.* TO 'user'@'localhost' +# IDENTIFIED BY 'password'; +# +# Requires: +# Class['mysql::config'] +# +# Sample Usage: +# class { 'mysql::backup': +# backupuser => 'myuser', +# backuppassword => 'mypassword', +# backupdir => '/tmp/backups', +# } +# +class mysql::backup ( + $backupuser, + $backuppassword, + $backupdir, + $ensure = 'present' +) { + + database_user { "${backupuser}@localhost": + ensure => $ensure, + password_hash => mysql_password($backuppassword), + provider => 'mysql', + require => Class['mysql::config'], + } + + database_grant { "${backupuser}@localhost": + privileges => [ 'Select_priv', 'Reload_priv', 'Lock_tables_priv', 'Show_view_priv' ], + require => Database_user["${backupuser}@localhost"], + } + + cron { 'mysql-backup': + ensure => $ensure, + command => '/usr/local/sbin/mysqlbackup.sh', + user => 'root', + hour => 23, + minute => 5, + require => File['mysqlbackup.sh'], + } + + file { 'mysqlbackup.sh': + ensure => $ensure, + path => '/usr/local/sbin/mysqlbackup.sh', + mode => '0700', + owner => 'root', + group => 'root', + content => template('mysql/mysqlbackup.sh.erb'), + } + + file { 'mysqlbackupdir': + ensure => 'directory', + path => $backupdir, + mode => '0700', + owner => 'root', + group => 'root', + } +} diff --git a/puppet/mysql/manifests/config.pp b/puppet/mysql/manifests/config.pp new file mode 100644 index 000000000..509534094 --- /dev/null +++ b/puppet/mysql/manifests/config.pp @@ -0,0 +1,134 @@ +# Class: mysql::config +# +# Parameters: +# +# [*root_password*] - root user password. +# [*old_root_password*] - previous root user password, +# [*bind_address*] - address to bind service. +# [*port*] - port to bind service. +# [*etc_root_password*] - whether to save /etc/my.cnf. +# [*service_name*] - mysql service name. +# [*config_file*] - my.cnf configuration file path. +# [*socket*] - mysql socket. +# [*datadir*] - path to datadir. +# [*ssl] - enable ssl +# [*ssl_ca] - path to ssl-ca +# [*ssl_cert] - path to ssl-cert +# [*ssl_key] - path to ssl-key +# [*log_error] - path to mysql error log +# [*default_engine] - configure a default table engine +# [*root_group] - use specified group for root-owned files +# [*restart] - whether to restart mysqld (true/false) +# +# Actions: +# +# Requires: +# +# class mysql::server +# +# Usage: +# +# class { 'mysql::config': +# root_password => 'changeme', +# bind_address => $::ipaddress, +# } +# +class mysql::config( + $root_password = 'UNSET', + $old_root_password = '', + $bind_address = $mysql::params::bind_address, + $port = $mysql::params::port, + $etc_root_password = $mysql::params::etc_root_password, + $service_name = $mysql::params::service_name, + $config_file = $mysql::params::config_file, + $socket = $mysql::params::socket, + $pidfile = $mysql::params::pidfile, + $datadir = $mysql::params::datadir, + $ssl = $mysql::params::ssl, + $ssl_ca = $mysql::params::ssl_ca, + $ssl_cert = $mysql::params::ssl_cert, + $ssl_key = $mysql::params::ssl_key, + $log_error = $mysql::params::log_error, + $default_engine = 'UNSET', + $root_group = $mysql::params::root_group, + $restart = $mysql::params::restart +) inherits mysql::params { + + File { + owner => 'root', + group => $root_group, + mode => '0400', + notify => $restart ? { + true => Exec['mysqld-restart'], + false => undef, + }, + } + + if $ssl and $ssl_ca == undef { + fail('The ssl_ca parameter is required when ssl is true') + } + + if $ssl and $ssl_cert == undef { + fail('The ssl_cert parameter is required when ssl is true') + } + + if $ssl and $ssl_key == undef { + fail('The ssl_key parameter is required when ssl is true') + } + + # This kind of sucks, that I have to specify a difference resource for + # restart. the reason is that I need the service to be started before mods + # to the config file which can cause a refresh + exec { 'mysqld-restart': + command => "service ${service_name} restart", + logoutput => on_failure, + refreshonly => true, + path => '/sbin/:/usr/sbin/:/usr/bin/:/bin/', + } + + # manage root password if it is set + if $root_password != 'UNSET' { + case $old_root_password { + '': { $old_pw='' } + default: { $old_pw="-p'${old_root_password}'" } + } + + exec { 'set_mysql_rootpw': + command => "mysqladmin -u root ${old_pw} password '${root_password}'", + logoutput => true, + unless => "mysqladmin -u root -p'${root_password}' status > /dev/null", + path => '/usr/local/sbin:/usr/bin:/usr/local/bin', + notify => $restart ? { + true => Exec['mysqld-restart'], + false => undef, + }, + require => File['/etc/mysql/conf.d'], + } + + file { '/root/.my.cnf': + content => template('mysql/my.cnf.pass.erb'), + require => Exec['set_mysql_rootpw'], + } + + if $etc_root_password { + file{ '/etc/my.cnf': + content => template('mysql/my.cnf.pass.erb'), + require => Exec['set_mysql_rootpw'], + } + } + } + + file { '/etc/mysql': + ensure => directory, + mode => '0755', + } + file { '/etc/mysql/conf.d': + ensure => directory, + mode => '0755', + } + file { $config_file: + content => template('mysql/my.cnf.erb'), + mode => '0644', + } + +} diff --git a/puppet/mysql/manifests/db.pp b/puppet/mysql/manifests/db.pp new file mode 100644 index 000000000..350252c08 --- /dev/null +++ b/puppet/mysql/manifests/db.pp @@ -0,0 +1,83 @@ +# Define: mysql::db +# +# This module creates database instances, a user, and grants that user +# privileges to the database. It can also import SQL from a file in order to, +# for example, initialize a database schema. +# +# Since it requires class mysql::server, we assume to run all commands as the +# root mysql user against the local mysql server. +# +# Parameters: +# [*title*] - mysql database name. +# [*user*] - username to create and grant access. +# [*password*] - user's password. +# [*charset*] - database charset. +# [*host*] - host for assigning privileges to user. +# [*grant*] - array of privileges to grant user. +# [*enforce_sql*] - whether to enforce or conditionally run sql on creation. +# [*sql*] - sql statement to run. +# [*ensure*] - specifies if a database is present or absent. +# +# Actions: +# +# Requires: +# +# class mysql::server +# +# Sample Usage: +# +# mysql::db { 'mydb': +# user => 'my_user', +# password => 'password', +# host => $::hostname, +# grant => ['all'] +# } +# +define mysql::db ( + $user, + $password, + $charset = 'utf8', + $host = 'localhost', + $grant = 'all', + $sql = '', + $enforce_sql = false, + $ensure = 'present' +) { + + validate_re($ensure, '^(present|absent)$', + "${ensure} is not supported for ensure. Allowed values are 'present' and 'absent'.") + + database { $name: + ensure => $ensure, + charset => $charset, + provider => 'mysql', + require => Class['mysql::server'], + } + + database_user { "${user}@${host}": + ensure => $ensure, + password_hash => mysql_password($password), + provider => 'mysql', + require => Database[$name], + } + + if $ensure == 'present' { + database_grant { "${user}@${host}/${name}": + privileges => $grant, + provider => 'mysql', + require => Database_user["${user}@${host}"], + } + + $refresh = ! $enforce_sql + + if $sql { + exec{ "${name}-import": + command => "/usr/bin/mysql ${name} < ${sql}", + logoutput => true, + refreshonly => $refresh, + require => Database_grant["${user}@${host}/${name}"], + subscribe => Database[$name], + } + } + } +} diff --git a/puppet/mysql/manifests/init.pp b/puppet/mysql/manifests/init.pp new file mode 100644 index 000000000..f415436d8 --- /dev/null +++ b/puppet/mysql/manifests/init.pp @@ -0,0 +1,24 @@ +# Class: mysql +# +# This class installs mysql client software. +# +# Parameters: +# [*client_package_name*] - The name of the mysql client package. +# +# Actions: +# +# Requires: +# +# Sample Usage: +# +class mysql ( + $package_name = $mysql::params::client_package_name, + $package_ensure = 'present' +) inherits mysql::params { + + package { 'mysql_client': + ensure => $package_ensure, + name => $package_name, + } + +} diff --git a/puppet/mysql/manifests/java.pp b/puppet/mysql/manifests/java.pp new file mode 100644 index 000000000..2713c058a --- /dev/null +++ b/puppet/mysql/manifests/java.pp @@ -0,0 +1,24 @@ +# Class: mysql::java +# +# This class installs the mysql-java-connector. +# +# Parameters: +# [*java_package_name*] - The name of the mysql java package. +# +# Actions: +# +# Requires: +# +# Sample Usage: +# +class mysql::java ( + $package_name = $mysql::params::java_package_name, + $package_ensure = 'present' +) inherits mysql::params { + + package { 'mysql-connector-java': + ensure => $package_ensure, + name => $package_name, + } + +} diff --git a/puppet/mysql/manifests/params.pp b/puppet/mysql/manifests/params.pp new file mode 100644 index 000000000..95787dbb9 --- /dev/null +++ b/puppet/mysql/manifests/params.pp @@ -0,0 +1,118 @@ +# Class: mysql::params +# +# The mysql configuration settings. +# +# Parameters: +# +# Actions: +# +# Requires: +# +# Sample Usage: +# +class mysql::params { + + $bind_address = '127.0.0.1' + $port = 3306 + $etc_root_password = false + $ssl = false + $restart = true + + case $::operatingsystem { + 'Ubuntu': { + $service_provider = upstart + } + default: { + $service_provider = undef + } + } + + case $::osfamily { + 'RedHat': { + $basedir = '/usr' + $datadir = '/var/lib/mysql' + $service_name = 'mysqld' + $client_package_name = 'mysql' + $server_package_name = 'mysql-server' + $socket = '/var/lib/mysql/mysql.sock' + $pidfile = '/var/run/mysqld/mysqld.pid' + $config_file = '/etc/my.cnf' + $log_error = '/var/log/mysqld.log' + $ruby_package_name = 'ruby-mysql' + $ruby_package_provider = 'gem' + $python_package_name = 'MySQL-python' + $java_package_name = 'mysql-connector-java' + $root_group = 'root' + $ssl_ca = '/etc/mysql/cacert.pem' + $ssl_cert = '/etc/mysql/server-cert.pem' + $ssl_key = '/etc/mysql/server-key.pem' + } + + 'Debian': { + $basedir = '/usr' + $datadir = '/var/lib/mysql' + $service_name = 'mysql' + $client_package_name = 'mysql-client' + $server_package_name = 'mysql-server' + $socket = '/var/run/mysqld/mysqld.sock' + $pidfile = '/var/run/mysqld/mysqld.pid' + $config_file = '/etc/mysql/my.cnf' + $log_error = '/var/log/mysql/error.log' + $ruby_package_name = 'libmysql-ruby' + $python_package_name = 'python-mysqldb' + $java_package_name = 'libmysql-java' + $root_group = 'root' + $ssl_ca = '/etc/mysql/cacert.pem' + $ssl_cert = '/etc/mysql/server-cert.pem' + $ssl_key = '/etc/mysql/server-key.pem' + } + + 'FreeBSD': { + $basedir = '/usr/local' + $datadir = '/var/db/mysql' + $service_name = 'mysql-server' + $client_package_name = 'databases/mysql55-client' + $server_package_name = 'databases/mysql55-server' + $socket = '/tmp/mysql.sock' + $pidfile = '/var/db/mysql/mysql.pid' + $config_file = '/var/db/mysql/my.cnf' + $log_error = "/var/db/mysql/${::hostname}.err" + $ruby_package_name = 'ruby-mysql' + $ruby_package_provider = 'gem' + $python_package_name = 'databases/py-MySQLdb' + $java_package_name = 'databases/mysql-connector-java' + $root_group = 'wheel' + $ssl_ca = undef + $ssl_cert = undef + $ssl_key = undef + } + + default: { + case $::operatingsystem { + 'Amazon': { + $basedir = '/usr' + $datadir = '/var/lib/mysql' + $service_name = 'mysqld' + $client_package_name = 'mysql' + $server_package_name = 'mysql-server' + $socket = '/var/lib/mysql/mysql.sock' + $config_file = '/etc/my.cnf' + $log_error = '/var/log/mysqld.log' + $ruby_package_name = 'ruby-mysql' + $ruby_package_provider = 'gem' + $python_package_name = 'MySQL-python' + $java_package_name = 'mysql-connector-java' + $root_group = 'root' + $ssl_ca = '/etc/mysql/cacert.pem' + $ssl_cert = '/etc/mysql/server-cert.pem' + $ssl_key = '/etc/mysql/server-key.pem' + } + + default: { + fail("Unsupported osfamily: ${::osfamily} operatingsystem: ${::operatingsystem}, module ${module_name} only support osfamily RedHat, Debian, and FreeBSD, or operatingsystem Amazon") + } + } + } + } + +} diff --git a/puppet/mysql/manifests/python.pp b/puppet/mysql/manifests/python.pp new file mode 100644 index 000000000..0a22da8f1 --- /dev/null +++ b/puppet/mysql/manifests/python.pp @@ -0,0 +1,26 @@ +# Class: mysql::python +# +# This class installs the python libs for mysql. +# +# Parameters: +# [*ensure*] - ensure state for package. +# can be specified as version. +# [*package_name*] - name of package +# +# Actions: +# +# Requires: +# +# Sample Usage: +# +class mysql::python( + $package_name = $mysql::params::python_package_name, + $package_ensure = 'present' +) inherits mysql::params { + + package { 'python-mysqldb': + ensure => $package_ensure, + name => $package_name, + } + +} diff --git a/puppet/mysql/manifests/ruby.pp b/puppet/mysql/manifests/ruby.pp new file mode 100644 index 000000000..6f8f46b41 --- /dev/null +++ b/puppet/mysql/manifests/ruby.pp @@ -0,0 +1,28 @@ +# Class: mysql::ruby +# +# installs the ruby bindings for mysql +# +# Parameters: +# [*ensure*] - ensure state for package. +# can be specified as version. +# [*package_name*] - name of package +# +# Actions: +# +# Requires: +# +# Sample Usage: +# +class mysql::ruby ( + $package_name = $mysql::params::ruby_package_name, + $package_provider = $mysql::params::ruby_package_provider, + $package_ensure = 'present' +) inherits mysql::params { + + package{ 'ruby_mysql': + ensure => $package_ensure, + name => $package_name, + provider => $package_provider, + } + +} diff --git a/puppet/mysql/manifests/server.pp b/puppet/mysql/manifests/server.pp new file mode 100644 index 000000000..b90014297 --- /dev/null +++ b/puppet/mysql/manifests/server.pp @@ -0,0 +1,53 @@ +# Class: mysql::server +# +# manages the installation of the mysql server. manages the package, service, +# my.cnf +# +# Parameters: +# [*package_name*] - name of package +# [*service_name*] - name of service +# [*config_hash*] - hash of config parameters that need to be set. +# +# Actions: +# +# Requires: +# +# Sample Usage: +# +class mysql::server ( + $package_name = $mysql::params::server_package_name, + $package_ensure = 'present', + $service_name = $mysql::params::service_name, + $service_provider = $mysql::params::service_provider, + $config_hash = {}, + $enabled = true, + $manage_service = true +) inherits mysql::params { + + Class['mysql::server'] -> Class['mysql::config'] + + $config_class = { 'mysql::config' => $config_hash } + + create_resources( 'class', $config_class ) + + package { 'mysql-server': + ensure => $package_ensure, + name => $package_name, + } + + if $enabled { + $service_ensure = 'running' + } else { + $service_ensure = 'stopped' + } + + if $manage_service { + service { 'mysqld': + ensure => $service_ensure, + name => $service_name, + enable => $enabled, + require => Package['mysql-server'], + provider => $service_provider, + } + } +} diff --git a/puppet/mysql/manifests/server/account_security.pp b/puppet/mysql/manifests/server/account_security.pp new file mode 100644 index 000000000..a7054dc54 --- /dev/null +++ b/puppet/mysql/manifests/server/account_security.pp @@ -0,0 +1,13 @@ +class mysql::server::account_security { + # Some installations have some default users which are not required. + # We remove them here. You can subclass this class to overwrite this behavior. + database_user { [ "root@${::fqdn}", "root@${::hostname}", 'root@127.0.0.1', 'root@::1', + "@${::fqdn}", "@${::hostname}", '@localhost', '@%' ]: + ensure => 'absent', + require => Class['mysql::config'], + } + database { 'test': + ensure => 'absent', + require => Class['mysql::config'], + } +} diff --git a/puppet/mysql/manifests/server/config.pp b/puppet/mysql/manifests/server/config.pp new file mode 100644 index 000000000..ddd034e11 --- /dev/null +++ b/puppet/mysql/manifests/server/config.pp @@ -0,0 +1,111 @@ +# Creates a my.cnf like config file in the conf.d/ directory. +# +# IMPORTANT: this should be used AFTER the inclusion of +# mysql::server because it needs some variables +# out of the mysql::config class which will be +# included! +# +# == Parameters: +# +# - name: is the name of the file +# - notify_service: whether to notify the mysql daemon or not (default: true) +# - settings: either a string which should be the content of the file +# or a hash with the following structure +# +# section => { +# => , +# ... +# }, +# ... +# +# +section+ means all these sections you can set in +# an configuration file like +mysqld+, +client+, +# +mysqldump+ and so on +# +key+ has to be a valid property which you can set like +# +datadir+, +socket+ or even flags like +read-only+ +# +# +value+ can be +# a) a string as the value +# b) +true+ or +false+ to set a flag like 'read-only' or leave +# it out (+false+ means, nothing will be done) +# c) an array of values which can be of type a) and/or b) +# +# +# == Examples: +# +# Easy one: +# +# mysql::server::config { 'basic_config': +# settings => "[mysqld]\nskip-external-locking\n" +# } +# +# This will create the file /etc/mysql/conf.d/basic_config.cnf with +# the following content: +# +# [mysqld] +# skip-external-locking +# +# +# More complex example: +# +# mysql::server::config { 'basic_config': +# settings => { +# 'mysqld' => { +# 'query_cache_limit' => '5M', +# 'query_cache_size' => '128M', +# 'port' => 3300, +# 'skip-external-locking' => true, +# 'replicate-ignore-db' => [ +# 'tmp_table', +# 'whateveryouwant' +# ] +# }, +# +# 'client' => { +# 'port' => 3300 +# } +# } +# } +# +# This will create the file /etc/mysql/conf.d/basic_config.cnf with +# the following content: +# +# [mysqld] +# query_cache_limit = 5M +# query_cache_size = 128M +# port = 3300 +# skip-external-locking +# replicate-ignore-db = tmp_table +# replicate-ignore-db = whateveryouwant +# +# [client] +# port = 3300 +# +define mysql::server::config ( + $settings, + $notify_service = true +) { + include mysql::config + + if is_hash($settings) { + $content = template('mysql/my.conf.cnf.erb') + } else { + $content = $settings + } + + file { "/etc/mysql/conf.d/${name}.cnf": + ensure => file, + content => $content, + owner => 'root', + group => $mysql::config::root_group, + mode => '0644', + require => Package['mysql-server'], + } + + if $notify_service { + File["/etc/mysql/conf.d/${name}.cnf"] { + # XXX notifying the Service gives us a dependency circle but I don't understand why + notify => Exec['mysqld-restart'] + } + } +} diff --git a/puppet/mysql/manifests/server/monitor.pp b/puppet/mysql/manifests/server/monitor.pp new file mode 100644 index 000000000..c7cad9fb8 --- /dev/null +++ b/puppet/mysql/manifests/server/monitor.pp @@ -0,0 +1,19 @@ +class mysql::server::monitor ( + $mysql_monitor_username, + $mysql_monitor_password, + $mysql_monitor_hostname +) { + + Class['mysql::server'] -> Class['mysql::server::monitor'] + + database_user{ "${mysql_monitor_username}@${mysql_monitor_hostname}": + ensure => present, + password_hash => mysql_password($mysql_monitor_password), + } + + database_grant { "${mysql_monitor_username}@${mysql_monitor_hostname}": + privileges => [ 'process_priv', 'super_priv' ], + require => Mysql_user["${mysql_monitor_username}@${mysql_monitor_hostname}"], + } + +} diff --git a/puppet/mysql/manifests/server/mysqltuner.pp b/puppet/mysql/manifests/server/mysqltuner.pp new file mode 100644 index 000000000..416a3dac5 --- /dev/null +++ b/puppet/mysql/manifests/server/mysqltuner.pp @@ -0,0 +1,22 @@ +# Copyright 2009 Larry Ludwig (larrylud@gmail.com) +# +# 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. +# +class mysql::server::mysqltuner { + # mysql performance tester + file { '/usr/bin/mysqltuner': + ensure => present, + mode => '0550', + source => 'puppet:///modules/mysql/mysqltuner.pl', + } +} diff --git a/puppet/mysql/spec/classes/mysql_backup_spec.rb b/puppet/mysql/spec/classes/mysql_backup_spec.rb new file mode 100644 index 000000000..ed45ba363 --- /dev/null +++ b/puppet/mysql/spec/classes/mysql_backup_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe 'mysql::backup' do + + let(:params) { + { 'backupuser' => 'testuser', + 'backuppassword' => 'testpass', + 'backupdir' => '/tmp', + } + } + + it { should contain_database_user('testuser@localhost')} + + it { should contain_database_grant('testuser@localhost').with( + :privileges => [ 'Select_priv', 'Reload_priv', 'Lock_tables_priv', 'Show_view_priv' ] + )} + + it { should contain_cron('mysql-backup').with( + :command => '/usr/local/sbin/mysqlbackup.sh', + :ensure => 'present' + )} + + it { should contain_file('mysqlbackup.sh').with( + :path => '/usr/local/sbin/mysqlbackup.sh', + :ensure => 'present' + )} + + it { should contain_file('mysqlbackupdir').with( + :path => '/tmp', + :ensure => 'directory' + )} + +end diff --git a/puppet/mysql/spec/classes/mysql_config_spec.rb b/puppet/mysql/spec/classes/mysql_config_spec.rb new file mode 100644 index 000000000..fab889991 --- /dev/null +++ b/puppet/mysql/spec/classes/mysql_config_spec.rb @@ -0,0 +1,237 @@ +require 'spec_helper' +describe 'mysql::config' do + + let :constant_parameter_defaults do + { + :root_password => 'UNSET', + :old_root_password => '', + :bind_address => '127.0.0.1', + :port => '3306', + :etc_root_password => false, + :datadir => '/var/lib/mysql', + :default_engine => 'UNSET', + :ssl => false, + } + end + + describe 'with osfamily specific defaults' do + { + 'Debian' => { + :datadir => '/var/lib/mysql', + :service_name => 'mysql', + :config_file => '/etc/mysql/my.cnf', + :socket => '/var/run/mysqld/mysqld.sock', + :pidfile => '/var/run/mysqld/mysqld.pid', + :root_group => 'root', + :ssl_ca => '/etc/mysql/cacert.pem', + :ssl_cert => '/etc/mysql/server-cert.pem', + :ssl_key => '/etc/mysql/server-key.pem' + }, + 'FreeBSD' => { + :datadir => '/var/db/mysql', + :service_name => 'mysql-server', + :config_file => '/var/db/mysql/my.cnf', + :socket => '/tmp/mysql.sock', + :pidfile => '/var/db/mysql/mysql.pid', + :root_group => 'wheel', + }, + 'Redhat' => { + :datadir => '/var/lib/mysql', + :service_name => 'mysqld', + :config_file => '/etc/my.cnf', + :socket => '/var/lib/mysql/mysql.sock', + :pidfile => '/var/run/mysqld/mysqld.pid', + :root_group => 'root', + :ssl_ca => '/etc/mysql/cacert.pem', + :ssl_cert => '/etc/mysql/server-cert.pem', + :ssl_key => '/etc/mysql/server-key.pem' + } + }.each do |osfamily, osparams| + + + describe "when osfamily is #{osfamily}" do + + let :facts do + {:osfamily => osfamily} + end + + describe 'when root password is set' do + + let :params do + {:root_password => 'foo'} + end + + it { should contain_exec('set_mysql_rootpw').with( + 'command' => 'mysqladmin -u root password \'foo\'', + 'logoutput' => true, + 'unless' => "mysqladmin -u root -p\'foo\' status > /dev/null", + 'path' => '/usr/local/sbin:/usr/bin:/usr/local/bin' + )} + + it { should contain_file('/root/.my.cnf').with( + 'content' => "[client]\nuser=root\nhost=localhost\npassword=foo\n", + 'require' => 'Exec[set_mysql_rootpw]' + )} + + end + + describe 'when root password and old password are set' do + let :params do + {:root_password => 'foo', :old_root_password => 'bar'} + end + + it { should contain_exec('set_mysql_rootpw').with( + 'command' => 'mysqladmin -u root -p\'bar\' password \'foo\'', + 'logoutput' => true, + 'unless' => "mysqladmin -u root -p\'foo\' status > /dev/null", + 'path' => '/usr/local/sbin:/usr/bin:/usr/local/bin' + )} + + end + + [ + {}, + { + :service_name => 'dans_service', + :config_file => '/home/dan/mysql.conf', + :service_name => 'dans_mysql', + :pidfile => '/home/dan/mysql.pid', + :socket => '/home/dan/mysql.sock', + :bind_address => '0.0.0.0', + :port => '3306', + :datadir => '/path/to/datadir', + :default_engine => 'InnoDB', + :ssl => true, + :ssl_ca => '/path/to/cacert.pem', + :ssl_cert => '/path/to/server-cert.pem', + :ssl_key => '/path/to/server-key.pem' + } + ].each do |passed_params| + + describe "with #{passed_params == {} ? 'default' : 'specified'} parameters" do + + let :parameter_defaults do + constant_parameter_defaults.merge(osparams) + end + + let :params do + passed_params + end + + let :param_values do + parameter_defaults.merge(params) + end + + it { should contain_exec('mysqld-restart').with( + :refreshonly => true, + :path => '/sbin/:/usr/sbin/:/usr/bin/:/bin/', + :command => "service #{param_values[:service_name]} restart" + )} + + it { should_not contain_exec('set_mysql_rootpw') } + + it { should_not contain_file('/root/.my.cnf')} + + it { should contain_file('/etc/mysql').with( + 'owner' => 'root', + 'group' => param_values[:root_group], + 'notify' => 'Exec[mysqld-restart]', + 'ensure' => 'directory', + 'mode' => '0755' + )} + it { should contain_file('/etc/mysql/conf.d').with( + 'owner' => 'root', + 'group' => param_values[:root_group], + 'notify' => 'Exec[mysqld-restart]', + 'ensure' => 'directory', + 'mode' => '0755' + )} + it { should contain_file(param_values[:config_file]).with( + 'owner' => 'root', + 'group' => param_values[:root_group], + 'notify' => 'Exec[mysqld-restart]', + 'mode' => '0644' + )} + it 'should have a template with the correct contents' do + content = param_value(subject, 'file', param_values[:config_file], 'content') + expected_lines = [ + "port = #{param_values[:port]}", + "socket = #{param_values[:socket]}", + "pid-file = #{param_values[:pidfile]}", + "datadir = #{param_values[:datadir]}", + "bind-address = #{param_values[:bind_address]}" + ] + if param_values[:default_engine] != 'UNSET' + expected_lines = expected_lines | [ "default-storage-engine = #{param_values[:default_engine]}" ] + end + if param_values[:ssl] + expected_lines = expected_lines | + [ + "ssl-ca = #{param_values[:ssl_ca]}", + "ssl-cert = #{param_values[:ssl_cert]}", + "ssl-key = #{param_values[:ssl_key]}" + ] + end + (content.split("\n") & expected_lines).should == expected_lines + end + end + end + end + end + end + + describe 'when etc_root_password is set with password' do + + let :facts do + {:osfamily => 'Debian'} + end + + let :params do + {:root_password => 'foo', :old_root_password => 'bar', :etc_root_password => true} + end + + it { should contain_exec('set_mysql_rootpw').with( + 'command' => 'mysqladmin -u root -p\'bar\' password \'foo\'', + 'logoutput' => true, + 'unless' => "mysqladmin -u root -p\'foo\' status > /dev/null", + 'path' => '/usr/local/sbin:/usr/bin:/usr/local/bin' + )} + + it { should contain_file('/root/.my.cnf').with( + 'content' => "[client]\nuser=root\nhost=localhost\npassword=foo\n", + 'require' => 'Exec[set_mysql_rootpw]' + )} + + end + + describe 'setting etc_root_password should fail on redhat' do + let :facts do + {:osfamily => 'Redhat'} + end + + let :params do + {:root_password => 'foo', :old_root_password => 'bar', :etc_root_password => true} + end + + it 'should fail' do + expect { subject }.to raise_error(Puppet::Error, /Duplicate (declaration|definition)/) + end + + end + + describe 'unset ssl params should fail when ssl is true on freebsd' do + let :facts do + {:osfamily => 'FreeBSD'} + end + + let :params do + { :ssl => true } + end + + it 'should fail' do + expect { subject }.to raise_error(Puppet::Error, /required when ssl is true/) + end + + end + +end diff --git a/puppet/mysql/spec/classes/mysql_init_spec.rb b/puppet/mysql/spec/classes/mysql_init_spec.rb new file mode 100644 index 000000000..f2b51f8c9 --- /dev/null +++ b/puppet/mysql/spec/classes/mysql_init_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe 'mysql' do + + describe 'on a debian based os' do + let :facts do + { :osfamily => 'Debian'} + end + it { should contain_package('mysql_client').with( + :name => 'mysql-client', + :ensure => 'present' + )} + end + + describe 'on a freebsd based os' do + let :facts do + { :osfamily => 'FreeBSD'} + end + it { should contain_package('mysql_client').with( + :name => 'databases/mysql55-client', + :ensure => 'present' + )} + end + + describe 'on a redhat based os' do + let :facts do + {:osfamily => 'Redhat'} + end + it { should contain_package('mysql_client').with( + :name => 'mysql', + :ensure => 'present' + )} + describe 'when parameters are supplied' do + let :params do + {:package_ensure => 'latest', :package_name => 'mysql_client'} + end + it { should contain_package('mysql_client').with( + :name => 'mysql_client', + :ensure => 'latest' + )} + end + end + + describe 'on any other os' do + let :facts do + {:osfamily => 'foo'} + end + + it 'should fail' do + expect { subject }.to raise_error(/Unsupported osfamily: foo/) + end + end + +end diff --git a/puppet/mysql/spec/classes/mysql_java_spec.rb b/puppet/mysql/spec/classes/mysql_java_spec.rb new file mode 100644 index 000000000..bd7569135 --- /dev/null +++ b/puppet/mysql/spec/classes/mysql_java_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe 'mysql::java' do + + describe 'on a debian based os' do + let :facts do + { :osfamily => 'Debian'} + end + it { should contain_package('mysql-connector-java').with( + :name => 'libmysql-java', + :ensure => 'present' + )} + end + + describe 'on a freebsd based os' do + let :facts do + { :osfamily => 'FreeBSD'} + end + it { should contain_package('mysql-connector-java').with( + :name => 'databases/mysql-connector-java', + :ensure => 'present' + )} + end + + describe 'on a redhat based os' do + let :facts do + {:osfamily => 'Redhat'} + end + it { should contain_package('mysql-connector-java').with( + :name => 'mysql-connector-java', + :ensure => 'present' + )} + describe 'when parameters are supplied' do + let :params do + {:package_ensure => 'latest', :package_name => 'java-mysql'} + end + it { should contain_package('mysql-connector-java').with( + :name => 'java-mysql', + :ensure => 'latest' + )} + end + end + + describe 'on any other os' do + let :facts do + {:osfamily => 'foo'} + end + + it 'should fail' do + expect { subject }.to raise_error(/Unsupported osfamily: foo/) + end + end + +end diff --git a/puppet/mysql/spec/classes/mysql_python_spec.rb b/puppet/mysql/spec/classes/mysql_python_spec.rb new file mode 100644 index 000000000..ecf1fb31b --- /dev/null +++ b/puppet/mysql/spec/classes/mysql_python_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe 'mysql::python' do + + describe 'on a debian based os' do + let :facts do + { :osfamily => 'Debian'} + end + it { should contain_package('python-mysqldb').with( + :name => 'python-mysqldb', + :ensure => 'present' + )} + end + + describe 'on a freebsd based os' do + let :facts do + { :osfamily => 'FreeBSD'} + end + it { should contain_package('python-mysqldb').with( + :name => 'databases/py-MySQLdb', + :ensure => 'present' + )} + end + + describe 'on a redhat based os' do + let :facts do + {:osfamily => 'Redhat'} + end + it { should contain_package('python-mysqldb').with( + :name => 'MySQL-python', + :ensure => 'present' + )} + describe 'when parameters are supplied' do + let :params do + {:package_ensure => 'latest', :package_name => 'python-mysql'} + end + it { should contain_package('python-mysqldb').with( + :name => 'python-mysql', + :ensure => 'latest' + )} + end + end + + describe 'on any other os' do + let :facts do + {:osfamily => 'foo'} + end + + it 'should fail' do + expect { subject }.to raise_error(/Unsupported osfamily: foo/) + end + end + +end diff --git a/puppet/mysql/spec/classes/mysql_ruby_spec.rb b/puppet/mysql/spec/classes/mysql_ruby_spec.rb new file mode 100644 index 000000000..ceb9e979e --- /dev/null +++ b/puppet/mysql/spec/classes/mysql_ruby_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe 'mysql::ruby' do + + describe 'on a debian based os' do + let :facts do + { :osfamily => 'Debian'} + end + it { should contain_package('ruby_mysql').with( + :name => 'libmysql-ruby', + :ensure => 'present', + # TODO is this what we want? does this actually work + # if the provider is blank + :provider => '' + )} + end + + describe 'on a freebsd based os' do + let :facts do + { :osfamily => 'FreeBSD'} + end + it { should contain_package('ruby_mysql').with( + :name => 'ruby-mysql', + :ensure => 'present', + :provider => 'gem' + )} + end + + describe 'on a redhat based os' do + let :facts do + {:osfamily => 'Redhat'} + end + it { should contain_package('ruby_mysql').with( + :name => 'ruby-mysql', + :ensure => 'present', + :provider => 'gem' + )} + describe 'when parameters are supplied' do + let :params do + {:package_ensure => 'latest', + :package_provider => 'zypper', + :package_name => 'mysql_ruby'} + end + it { should contain_package('ruby_mysql').with( + :name => 'mysql_ruby', + :ensure => 'latest', + :provider => 'zypper' + )} + end + end + + describe 'on any other os' do + let :facts do + {:osfamily => 'foo'} + end + + it 'should fail' do + expect { subject }.to raise_error(/Unsupported osfamily: foo/) + end + end + +end diff --git a/puppet/mysql/spec/classes/mysql_server_account_security_spec.rb b/puppet/mysql/spec/classes/mysql_server_account_security_spec.rb new file mode 100644 index 000000000..98d4bb63e --- /dev/null +++ b/puppet/mysql/spec/classes/mysql_server_account_security_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe 'mysql::server::account_security' do + + let :facts do { + :fqdn => 'myhost.mydomain', + :hostname => 'myhost' + } + end + + it 'should remove Database_User[root@myhost.mydomain]' do + should contain_database_user('root@myhost.mydomain').with_ensure('absent') + end + it 'should remove Database_User[root@myhost]' do + should contain_database_user('root@myhost').with_ensure('absent') + end + it 'should remove Database_User[root@127.0.0.1]' do + should contain_database_user('root@127.0.0.1').with_ensure('absent') + end + it 'should remove Database_User[root@::1]' do + should contain_database_user('root@::1').with_ensure('absent') + end + it 'should remove Database_User[@myhost.mydomain]' do + should contain_database_user('@myhost.mydomain').with_ensure('absent') + end + it 'should remove Database_User[@myhost]' do + should contain_database_user('@myhost').with_ensure('absent') + end + it 'should remove Database_User[@localhost]' do + should contain_database_user('@localhost').with_ensure('absent') + end + it 'should remove Database_User[@%]' do + should contain_database_user('@%').with_ensure('absent') + end + + it 'should remove Database[test]' do + should contain_database('test').with_ensure('absent') + end + +end diff --git a/puppet/mysql/spec/classes/mysql_server_monitor_spec.rb b/puppet/mysql/spec/classes/mysql_server_monitor_spec.rb new file mode 100644 index 000000000..b7591cc13 --- /dev/null +++ b/puppet/mysql/spec/classes/mysql_server_monitor_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' +describe 'mysql::server::monitor' do + let :facts do + { :osfamily => 'Debian' } + end + let :pre_condition do + "include 'mysql::server'" + end + let :params do + { + :mysql_monitor_username => 'monitoruser', + :mysql_monitor_password => 'monitorpass', + :mysql_monitor_hostname => 'monitorhost' + } + end + + it { should contain_database_user("monitoruser@monitorhost")} +end diff --git a/puppet/mysql/spec/classes/mysql_server_spec.rb b/puppet/mysql/spec/classes/mysql_server_spec.rb new file mode 100644 index 000000000..8da66a730 --- /dev/null +++ b/puppet/mysql/spec/classes/mysql_server_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper' +describe 'mysql::server' do + + let :constant_parameter_defaults do + {:config_hash => {}, + :package_ensure => 'present', + :enabled => true, + :manage_service => true + } + end + + describe 'when ubuntu use upstart' do + let :facts do + { :osfamily => 'Debian', + :operatingsystem => 'Ubuntu', + } + end + + it { should contain_service('mysqld').with( + :name => 'mysql', + :ensure => 'running', + :enable => 'true', + :provider => 'upstart', + :require => 'Package[mysql-server]' + )} + end + + describe 'with osfamily specific defaults' do + { + 'Debian' => { + :service_name => 'mysql', + :package_name => 'mysql-server' + }, + 'FreeBSD' => { + :service_name => 'mysql-server', + :package_name => 'databases/mysql55-server' + }, + 'Redhat' => { + :service_name => 'mysqld', + :package_name => 'mysql-server' + } + }.each do |osfamily, osparams| + + describe "when osfamily is #{osfamily}" do + + let :facts do + { :osfamily => osfamily } + end + + [ + {}, + { + :package_name => 'dans_package', + :package_ensure => 'latest', + :service_name => 'dans_service', + :config_hash => {'root_password' => 'foo'}, + :enabled => false, + :manage_service => false + } + ].each do |passed_params| + + describe "with #{passed_params == {} ? 'default' : 'specified'} parameters" do + + let :parameter_defaults do + constant_parameter_defaults.merge(osparams) + end + + let :params do + passed_params + end + + let :param_values do + parameter_defaults.merge(params) + end + + it { should contain_package('mysql-server').with( + :name => param_values[:package_name], + :ensure => param_values[:package_ensure] + )} + + it { + if param_values[:manage_service] + should contain_service('mysqld').with( + :name => param_values[:service_name], + :ensure => param_values[:enabled] ? 'running' : 'stopped', + :enable => param_values[:enabled], + :require => 'Package[mysql-server]' + ).without_provider + else + should_not contain_service('mysqld') + end + } + end + end + end + end + end +end diff --git a/puppet/mysql/spec/defines/mysql_db_spec.rb b/puppet/mysql/spec/defines/mysql_db_spec.rb new file mode 100644 index 000000000..55f5d9d02 --- /dev/null +++ b/puppet/mysql/spec/defines/mysql_db_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe 'mysql::db', :type => :define do + let(:title) { 'test_db' } + + let(:params) { + { 'user' => 'testuser', + 'password' => 'testpass', + } + } + + it 'should report an error when ensure is not present or absent' do + params.merge!({'ensure' => 'invalid_val'}) + expect { subject }.to raise_error(Puppet::Error, + /invalid_val is not supported for ensure\. Allowed values are 'present' and 'absent'\./) + end + + it 'should not notify the import sql exec if no sql script was provided' do + should contain_database('test_db').without_notify + end + + it 'should subscribe to database if sql script is given' do + params.merge!({'sql' => 'test_sql'}) + should contain_exec('test_db-import').with_subscribe('Database[test_db]') + end + + it 'should only import sql script on creation if not enforcing' do + params.merge!({'sql' => 'test_sql', 'enforce_sql' => false}) + should contain_exec('test_db-import').with_refreshonly(true) + end + + it 'should import sql script on creation if enforcing' do + params.merge!({'sql' => 'test_sql', 'enforce_sql' => true}) + should contain_exec('test_db-import').with_refreshonly(false) + end + + it 'should not create database and database user' do + params.merge!({'ensure' => 'absent', 'host' => 'localhost'}) + should contain_database('test_db').with_ensure('absent') + should contain_database_user('testuser@localhost').with_ensure('absent') + end +end diff --git a/puppet/mysql/spec/defines/mysql_server_config_spec.rb b/puppet/mysql/spec/defines/mysql_server_config_spec.rb new file mode 100644 index 000000000..7d99e5bcf --- /dev/null +++ b/puppet/mysql/spec/defines/mysql_server_config_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe 'mysql::server::config', :type => :define do + filename = '/etc/mysql/conf.d/test_config.cnf' + + let :facts do + { :osfamily => 'Debian'} + end + + let(:title) { File.basename(filename, '.cnf') } + + let(:params) { + { 'settings' => { + 'mysqld' => { + 'bind-address' => '0.0.0.0' + } + } + } + } + + it 'should notify the mysql daemon' do + should contain_file(filename).with_notify('Exec[mysqld-restart]') + end + + it 'should contain config parameter in content' do + should contain_file(filename).with_content("### MANAGED BY PUPPET ###\n[mysqld]\nbind-address = 0.0.0.0\n\n") + end + + it 'should not notify the mysql daemon' do + params.merge!({ 'notify_service' => false }) + should contain_file(filename).without_notify + end + + it 'should require on the mysql-server package' do + should contain_file(filename).with_require('Package[mysql-server]') + end +end diff --git a/puppet/mysql/spec/spec.opts b/puppet/mysql/spec/spec.opts new file mode 100644 index 000000000..91cd6427e --- /dev/null +++ b/puppet/mysql/spec/spec.opts @@ -0,0 +1,6 @@ +--format +s +--colour +--loadby +mtime +--backtrace diff --git a/puppet/mysql/spec/spec_helper.rb b/puppet/mysql/spec/spec_helper.rb new file mode 100644 index 000000000..2c6f56649 --- /dev/null +++ b/puppet/mysql/spec/spec_helper.rb @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/module_spec_helper' diff --git a/puppet/mysql/spec/unit/mysql_password_spec.rb b/puppet/mysql/spec/unit/mysql_password_spec.rb new file mode 100644 index 000000000..4e67ed99e --- /dev/null +++ b/puppet/mysql/spec/unit/mysql_password_spec.rb @@ -0,0 +1,30 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +describe "the mysql_password function" do + before :all do + Puppet::Parser::Functions.autoloader.loadall + end + + before :each do + @scope = Puppet::Parser::Scope.new + end + + it "should exist" do + Puppet::Parser::Functions.function("mysql_password").should == "function_mysql_password" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { @scope.function_mysql_password([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should raise a ParseError if there is more than 1 arguments" do + lambda { @scope.function_mysql_password(['foo', 'bar']) }.should( raise_error(Puppet::ParseError)) + end + + it "should convert password into a hash" do + result = @scope.function_mysql_password(["password"]) + result.should(eq('*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19')) + end + +end diff --git a/puppet/mysql/spec/unit/puppet/provider/database_grant/mysql_spec.rb b/puppet/mysql/spec/unit/puppet/provider/database_grant/mysql_spec.rb new file mode 100644 index 000000000..db82ded39 --- /dev/null +++ b/puppet/mysql/spec/unit/puppet/provider/database_grant/mysql_spec.rb @@ -0,0 +1,86 @@ +require 'puppet' +require 'mocha' +require 'spec_helper' +RSpec.configure do |config| + config.mock_with :mocha +end +provider_class = Puppet::Type.type(:database_grant).provider(:mysql) +describe provider_class do + let(:root_home) { '/some/root/home' } + + before :each do + @resource = Puppet::Type::Database_grant.new( + { :privileges => 'all', :provider => 'mysql', :name => 'user@host'} + ) + @provider = provider_class.new(@resource) + Facter.stubs(:value).with(:root_home).returns(root_home) + end + + it 'should query privilegess from the database' do + provider_class.expects(:mysql) .with("--defaults-file=#{root_home}/.my.cnf", 'mysql', '-Be', 'describe user').returns <<-EOT +Field Type Null Key Default Extra +Host char(60) NO PRI +User char(16) NO PRI +Password char(41) NO +Select_priv enum('N','Y') NO N +Insert_priv enum('N','Y') NO N +Update_priv enum('N','Y') NO N +EOT + provider_class.expects(:mysql).with("--defaults-file=#{root_home}/.my.cnf", 'mysql', '-Be', 'describe db').returns <<-EOT +Field Type Null Key Default Extra +Host char(60) NO PRI +Db char(64) NO PRI +User char(16) NO PRI +Select_priv enum('N','Y') NO N +Insert_priv enum('N','Y') NO N +Update_priv enum('N','Y') NO N +EOT + provider_class.user_privs.should == [ 'Select_priv', 'Insert_priv', 'Update_priv' ] + provider_class.db_privs.should == [ 'Select_priv', 'Insert_priv', 'Update_priv' ] + end + + it 'should query set priviliges' do + provider_class.expects(:mysql).with("--defaults-file=#{root_home}/.my.cnf", 'mysql', '-Be', 'select * from mysql.user where user="user" and host="host"').returns <<-EOT +Host User Password Select_priv Insert_priv Update_priv +host user Y N Y +EOT + @provider.privileges.should == [ 'Select_priv', 'Update_priv' ] + end + + it 'should recognize when all priviliges are set' do + provider_class.expects(:mysql).with("--defaults-file=#{root_home}/.my.cnf", 'mysql', '-Be', 'select * from mysql.user where user="user" and host="host"').returns <<-EOT +Host User Password Select_priv Insert_priv Update_priv +host user Y Y Y +EOT + @provider.all_privs_set?.should == true + end + + it 'should recognize when all privileges are not set' do + provider_class.expects(:mysql).with("--defaults-file=#{root_home}/.my.cnf", 'mysql', '-Be', 'select * from mysql.user where user="user" and host="host"').returns <<-EOT +Host User Password Select_priv Insert_priv Update_priv +host user Y N Y +EOT + @provider.all_privs_set?.should == false + end + + it 'should be able to set all privileges' do + provider_class.expects(:mysql).with("--defaults-file=#{root_home}/.my.cnf", 'mysql', '-NBe', 'SELECT "1" FROM user WHERE user="user" AND host="host"').returns "1\n" + provider_class.expects(:mysql).with("--defaults-file=#{root_home}/.my.cnf", 'mysql', '-Be', "update user set Select_priv = 'Y', Insert_priv = 'Y', Update_priv = 'Y' where user=\"user\" and host=\"host\"") + provider_class.expects(:mysqladmin).with("--defaults-file=#{root_home}/.my.cnf", "flush-privileges") + @provider.privileges=(['all']) + end + + it 'should be able to set partial privileges' do + provider_class.expects(:mysql).with("--defaults-file=#{root_home}/.my.cnf", 'mysql', '-NBe', 'SELECT "1" FROM user WHERE user="user" AND host="host"').returns "1\n" + provider_class.expects(:mysql).with("--defaults-file=#{root_home}/.my.cnf", 'mysql', '-Be', "update user set Select_priv = 'Y', Insert_priv = 'N', Update_priv = 'Y' where user=\"user\" and host=\"host\"") + provider_class.expects(:mysqladmin).with("--defaults-file=#{root_home}/.my.cnf", "flush-privileges") + @provider.privileges=(['Select_priv', 'Update_priv']) + end + + it 'should be case insensitive' do + provider_class.expects(:mysql).with("--defaults-file=#{root_home}/.my.cnf", 'mysql', '-NBe', 'SELECT "1" FROM user WHERE user="user" AND host="host"').returns "1\n" + provider_class.expects(:mysql).with("--defaults-file=#{root_home}/.my.cnf", 'mysql', '-Be', "update user set Select_priv = 'Y', Insert_priv = 'Y', Update_priv = 'Y' where user=\"user\" and host=\"host\"") + provider_class.expects(:mysqladmin).with("--defaults-file=#{root_home}/.my.cnf", 'flush-privileges') + @provider.privileges=(['SELECT_PRIV', 'insert_priv', 'UpDaTe_pRiV']) + end +end diff --git a/puppet/mysql/templates/my.cnf.erb b/puppet/mysql/templates/my.cnf.erb new file mode 100644 index 000000000..580def1c9 --- /dev/null +++ b/puppet/mysql/templates/my.cnf.erb @@ -0,0 +1,47 @@ +[client] +port = <%= port %> +socket = <%= socket %> +[mysqld_safe] +socket = <%= socket %> +nice = 0 +[mysqld] +user = mysql +pid-file = <%= pidfile %> +socket = <%= socket %> +port = <%= port %> +basedir = <%= basedir %> +datadir = <%= datadir %> +tmpdir = /tmp +skip-external-locking + +<% if bind_address %> +bind-address = <%= bind_address %> +<% end %> + +key_buffer = 16M +max_allowed_packet = 16M +thread_stack = 192K +thread_cache_size = 8 +myisam-recover = BACKUP +query_cache_limit = 1M +query_cache_size = 16M +log_error = <%= log_error %> +expire_logs_days = 10 +max_binlog_size = 100M +<% if default_engine != 'UNSET' %> +default-storage-engine = <%= default_engine %> +<% end %> +<% if ssl == true %> +ssl-ca = <%= ssl_ca %> +ssl-cert = <%= ssl_cert %> +ssl-key = <%= ssl_key %> +<% end %> + +[mysqldump] +quick +quote-names +max_allowed_packet = 16M +[mysql] +[isamchk] +key_buffer = 16M +!includedir /etc/mysql/conf.d/ diff --git a/puppet/mysql/templates/my.cnf.pass.erb b/puppet/mysql/templates/my.cnf.pass.erb new file mode 100644 index 000000000..38a3a4aef --- /dev/null +++ b/puppet/mysql/templates/my.cnf.pass.erb @@ -0,0 +1,6 @@ +[client] +user=root +host=localhost +<% unless root_password == 'UNSET' -%> +password=<%= root_password %> +<% end -%> diff --git a/puppet/mysql/templates/my.conf.cnf.erb b/puppet/mysql/templates/my.conf.cnf.erb new file mode 100644 index 000000000..ad58af3cb --- /dev/null +++ b/puppet/mysql/templates/my.conf.cnf.erb @@ -0,0 +1,17 @@ +### MANAGED BY PUPPET ### +<% settings.sort.each do |section, content| -%> +[<%= section %>] +<% content.sort.each do |key, values| -%> +<% [values].flatten.sort.each do |value| -%> +<%= value == false ? '#' : '' %><%= key -%><%= + case value + when true, false + '' + else + " = #{value}" + end +%> +<% end -%> +<% end -%> + +<% end -%> diff --git a/puppet/mysql/templates/mysqlbackup.sh.erb b/puppet/mysql/templates/mysqlbackup.sh.erb new file mode 100644 index 000000000..a2dba331a --- /dev/null +++ b/puppet/mysql/templates/mysqlbackup.sh.erb @@ -0,0 +1,23 @@ +#!/bin/sh +# +# MySQL Backup Script +# Dumps mysql databases to a file for another backup tool to pick up. +# +# MySQL code: +# GRANT SELECT, RELOAD, LOCK TABLES ON *.* TO 'user'@'localhost' +# IDENTIFIED BY 'password'; +# FLUSH PRIVILEGES; +# +##### START CONFIG ################################################### + +USER=<%= backupuser %> +PASS=<%= backuppassword %> +DIR=<%= backupdir %> + +##### STOP CONFIG #################################################### +PATH=/usr/bin:/usr/sbin:/bin:/sbin + +find $DIR -mtime +30 -exec rm -f {} \; +mysqldump -u${USER} -p${PASS} --opt --flush-logs --single-transaction \ + --all-databases | bzcat -zc > ${DIR}/mysql_backup_`date +%Y%m%d-%H%M%S`.sql.bz2 + diff --git a/puppet/mysql/tests/backup.pp b/puppet/mysql/tests/backup.pp new file mode 100644 index 000000000..cb669e6db --- /dev/null +++ b/puppet/mysql/tests/backup.pp @@ -0,0 +1,8 @@ +class { 'mysql::server': + config_hash => {'root_password' => 'password'} +} +class { 'mysql::backup': + backupuser => 'myuser', + backuppassword => 'mypassword', + backupdir => '/tmp/backups', +} diff --git a/puppet/mysql/tests/init.pp b/puppet/mysql/tests/init.pp new file mode 100644 index 000000000..846121b7d --- /dev/null +++ b/puppet/mysql/tests/init.pp @@ -0,0 +1 @@ +include mysql diff --git a/puppet/mysql/tests/java.pp b/puppet/mysql/tests/java.pp new file mode 100644 index 000000000..0fc009a6d --- /dev/null +++ b/puppet/mysql/tests/java.pp @@ -0,0 +1 @@ +class { 'mysql::java':} diff --git a/puppet/mysql/tests/mysql_database.pp b/puppet/mysql/tests/mysql_database.pp new file mode 100644 index 000000000..8747f707d --- /dev/null +++ b/puppet/mysql/tests/mysql_database.pp @@ -0,0 +1,12 @@ +class { 'mysql::server': + config_hash => {'root_password' => 'password'} +} +database{ ['test1', 'test2', 'test3']: + ensure => present, + charset => 'utf8', + require => Class['mysql::server'], +} +database{ 'test4': + ensure => present, + charset => 'latin1', +} diff --git a/puppet/mysql/tests/mysql_grant.pp b/puppet/mysql/tests/mysql_grant.pp new file mode 100644 index 000000000..8d9654740 --- /dev/null +++ b/puppet/mysql/tests/mysql_grant.pp @@ -0,0 +1,3 @@ +database_grant{'test1@localhost/redmine': + privileges => [update], +} diff --git a/puppet/mysql/tests/mysql_user.pp b/puppet/mysql/tests/mysql_user.pp new file mode 100644 index 000000000..f63908431 --- /dev/null +++ b/puppet/mysql/tests/mysql_user.pp @@ -0,0 +1,23 @@ +$mysql_root_pw = 'password' + +class { 'mysql::server': + config_hash => { + root_password => 'password', + } +} + +database_user{ 'redmine@localhost': + ensure => present, + password_hash => mysql_password('redmine'), + require => Class['mysql::server'], +} + +database_user{ 'dan@localhost': + ensure => present, + password_hash => mysql_password('blah') +} + +database_user{ 'dan@%': + ensure => present, + password_hash => mysql_password('blah'), +} diff --git a/puppet/mysql/tests/python.pp b/puppet/mysql/tests/python.pp new file mode 100644 index 000000000..04f7ffa1a --- /dev/null +++ b/puppet/mysql/tests/python.pp @@ -0,0 +1 @@ +class { 'mysql::python':} diff --git a/puppet/mysql/tests/ruby.pp b/puppet/mysql/tests/ruby.pp new file mode 100644 index 000000000..e84c046a3 --- /dev/null +++ b/puppet/mysql/tests/ruby.pp @@ -0,0 +1 @@ +include mysql::ruby diff --git a/puppet/mysql/tests/server.pp b/puppet/mysql/tests/server.pp new file mode 100644 index 000000000..47e2fa301 --- /dev/null +++ b/puppet/mysql/tests/server.pp @@ -0,0 +1,3 @@ +class { 'mysql::server': + config_hash => { 'root_password' => 'password', }, +} diff --git a/puppet/mysql/tests/server/account_security.pp b/puppet/mysql/tests/server/account_security.pp new file mode 100644 index 000000000..de393cce4 --- /dev/null +++ b/puppet/mysql/tests/server/account_security.pp @@ -0,0 +1,4 @@ +class { 'mysql::server': + config_hash => { 'root_password' => 'password', }, +} +class { 'mysql::server::account_security': } diff --git a/puppet/mysql/tests/server/config.pp b/puppet/mysql/tests/server/config.pp new file mode 100644 index 000000000..fe8d86e90 --- /dev/null +++ b/puppet/mysql/tests/server/config.pp @@ -0,0 +1,11 @@ +mysql::server::config { 'testfile': + settings => { + 'mysqld' => { + 'bind-address' => '0.0.0.0', + 'read-only' => true, + }, + 'client' => { + 'port' => '3306' + } + } +} diff --git a/puppet/rabbitmq/.fixtures.yml b/puppet/rabbitmq/.fixtures.yml new file mode 100644 index 000000000..f63cba6e7 --- /dev/null +++ b/puppet/rabbitmq/.fixtures.yml @@ -0,0 +1,6 @@ +fixtures: + repositories: + "stdlib": "git://github.com/puppetlabs/puppetlabs-stdlib.git" + "apt": "git://github.com/puppetlabs/puppetlabs-apt.git" + symlinks: + "rabbitmq": "#{source_dir}" diff --git a/puppet/rabbitmq/.gemfile b/puppet/rabbitmq/.gemfile new file mode 100644 index 000000000..9aad840c0 --- /dev/null +++ b/puppet/rabbitmq/.gemfile @@ -0,0 +1,5 @@ +source :rubygems + +puppetversion = ENV.key?('PUPPET_VERSION') ? "= #{ENV['PUPPET_VERSION']}" : ['>= 2.7'] +gem 'puppet', puppetversion +gem 'puppetlabs_spec_helper', '>= 0.1.0' diff --git a/puppet/rabbitmq/.travis.yml b/puppet/rabbitmq/.travis.yml new file mode 100644 index 000000000..bf7829ef7 --- /dev/null +++ b/puppet/rabbitmq/.travis.yml @@ -0,0 +1,16 @@ +language: ruby +rvm: + - 1.8.7 +before_script: +after_script: +script: "rake spec" +branches: + only: + - master +env: + - PUPPET_VERSION=2.7.13 + - PUPPET_VERSION=2.7.6 + - PUPPET_VERSION=2.6.9 +notifications: + email: false +gemfile: .gemfile diff --git a/puppet/rabbitmq/CHANGELOG b/puppet/rabbitmq/CHANGELOG new file mode 100644 index 000000000..05582c867 --- /dev/null +++ b/puppet/rabbitmq/CHANGELOG @@ -0,0 +1,24 @@ +* 2012-05-03 2.0.0 +- added support for new-style admin users +- added support for rabbitmq 2.7.1 + +* 2011-06-14 Dan Bode 2.0.0rc1 +- Massive refactor: +- added native types for user/vhost/user_permissions +- added apt support for vendor packages +- added smoke tests + +* 2011-04-08 Jeff McCune 1.0.4 +- Update module for RabbitMQ 2.4.1 and rabbitmq-plugin-stomp package. + +2011-03-24 1.0.3 +- Initial release to the forge. Reviewed by Cody. Whitespace is good. + +2011-03-22 1.0.2 +- Whitespace only fix again... ack '\t' is my friend... + +2011-03-22 1.0.1 +- Whitespace only fix. + +2011-03-22 1.0.0 +- Initial Release. Manage the package, file and service. diff --git a/puppet/rabbitmq/LICENSE b/puppet/rabbitmq/LICENSE new file mode 100644 index 000000000..8d968b6cb --- /dev/null +++ b/puppet/rabbitmq/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/puppet/rabbitmq/Modulefile b/puppet/rabbitmq/Modulefile new file mode 100644 index 000000000..35d58a7be --- /dev/null +++ b/puppet/rabbitmq/Modulefile @@ -0,0 +1,12 @@ +name 'puppetlabs-rabbitmq' +version '2.0.1' +source 'git://github.com/puppetlabs/puppetlabs-rabbitmq.git' +author 'puppetlabs' +license 'Apache' +summary 'RabbitMQ Puppet Module' +description 'This module manages RabbitMQ. Tested on Debian/Ubuntu' +project_page 'http://github.com/puppetlabs/puppetlabs-rabbitmq' + +## Add dependencies, if any: +dependency 'puppetlabs/stdlib', '>= 2.0.0' +dependency 'puppetlabs/apt', '>= 0.0.3' diff --git a/puppet/rabbitmq/README.md b/puppet/rabbitmq/README.md new file mode 100644 index 000000000..af1d059b2 --- /dev/null +++ b/puppet/rabbitmq/README.md @@ -0,0 +1,71 @@ +# RabbitMQ Puppet Module +This module manages the RabbitMQ Middleware service. + +This module has been tested against 2.7.1 and is known to not support +all features against earlier versions. + +### Authors +* Jeff McCune +* Dan Bode + +## Classes + +This module provides its core functionality through two main classes: + +### rabbitmq::repo::apt +Sets up an apt repo source for the vendor rabbitmq packages + + class { 'rabbitmq::repo::apt': + pin => 900, + before => Class['rabbitmq::server'] + } + +### rabbitmq::server +Class for installing rabbitmq-server: + + class { 'rabbitmq::server': + port => '5673', + delete_guest_user => true, + } + + +## Native Types + +**NOTE:** Unfortunately, you must specify the provider explicitly for these types + +### rabbitmq_user + +query all current users: `$ puppet resource rabbitmq_user` + + rabbitmq_user { 'dan': + admin => true, + password => 'bar', + provider => 'rabbitmqctl', + } + +### rabbitmq_vhost + +query all current vhosts: `$ puppet resource rabbitmq_vhost` + + rabbitmq_vhost { 'myhost': + ensure => present, + provider => 'rabbitmqctl', + } + +### rabbitmq\_user\_permissions + + rabbitmq_user_permissions { 'dan@myhost': + configure_permission => '.*', + read_permission => '.*', + write_permission => '.*', + provider => 'rabbitmqctl', + } + +### rabbitmq_plugin + +query all currently enabled plugins `$ puppet resource rabbitmq_plugin` + + rabbitmq_plugin {'rabbitmq_stomp': + ensure => present, + provider => 'rabbitmqplugins', + } diff --git a/puppet/rabbitmq/Rakefile b/puppet/rabbitmq/Rakefile new file mode 100644 index 000000000..cd3d37995 --- /dev/null +++ b/puppet/rabbitmq/Rakefile @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/rake_tasks' diff --git a/puppet/rabbitmq/TODO b/puppet/rabbitmq/TODO new file mode 100644 index 000000000..8ae578e9e --- /dev/null +++ b/puppet/rabbitmq/TODO @@ -0,0 +1,10 @@ +provider TODO - + - password should be a property and not a param + - what if we tried to log in as that user? + - can permissions from list_user_permissions contain whitespace? + - what about defaultfor :true? + - prefetching for performance + - rabbit plugin should require rabbitmq class + - rabbitmq class should be renamed server?? + - service name should default to -server + - cannot find stomp package diff --git a/puppet/rabbitmq/files/README.markdown b/puppet/rabbitmq/files/README.markdown new file mode 100644 index 000000000..be52188c4 --- /dev/null +++ b/puppet/rabbitmq/files/README.markdown @@ -0,0 +1,22 @@ +Files +===== + +Puppet comes with both a client and server for copying files around. The file +serving function is provided as part of the central Puppet daemon, +puppetmasterd, and the client function is used through the source attribute of +file objects. Learn more at +http://projects.puppetlabs.com/projects/puppet/wiki/File_Serving_Configuration + +You can use managed files like this: + + class myclass { + package { mypackage: ensure => latest } + service { myservice: ensure => running } + file { "/etc/myfile": + source => "puppet://$servername/modules/mymodule/myfile" + } + } + +The files are searched for in: + + $modulepath/mymodule/files/myfile diff --git a/puppet/rabbitmq/files/plugins/amqp_client-2.3.1.ez b/puppet/rabbitmq/files/plugins/amqp_client-2.3.1.ez new file mode 100644 index 000000000..6ef6d4ff1 Binary files /dev/null and b/puppet/rabbitmq/files/plugins/amqp_client-2.3.1.ez differ diff --git a/puppet/rabbitmq/files/plugins/rabbit_stomp-2.3.1.ez b/puppet/rabbitmq/files/plugins/rabbit_stomp-2.3.1.ez new file mode 100644 index 000000000..4a28b72f8 Binary files /dev/null and b/puppet/rabbitmq/files/plugins/rabbit_stomp-2.3.1.ez differ diff --git a/puppet/rabbitmq/lib/puppet/provider/rabbitmq_plugin/default.rb b/puppet/rabbitmq/lib/puppet/provider/rabbitmq_plugin/default.rb new file mode 100644 index 000000000..9f71a9acd --- /dev/null +++ b/puppet/rabbitmq/lib/puppet/provider/rabbitmq_plugin/default.rb @@ -0,0 +1,22 @@ +Puppet::Type.type(:rabbitmq_plugin).provide(:default) do + + def self.instances + [] + end + + def create + default_fail + end + + def destroy + default_fail + end + + def exists? + default_fail + end + + def default_fail + fail('This is just the default provider for rabbitmq_plugin, all it does is fail') + end +end diff --git a/puppet/rabbitmq/lib/puppet/provider/rabbitmq_plugin/rabbitmqplugins.rb b/puppet/rabbitmq/lib/puppet/provider/rabbitmq_plugin/rabbitmqplugins.rb new file mode 100644 index 000000000..847c7bfa2 --- /dev/null +++ b/puppet/rabbitmq/lib/puppet/provider/rabbitmq_plugin/rabbitmqplugins.rb @@ -0,0 +1,30 @@ +Puppet::Type.type(:rabbitmq_plugin).provide(:rabbitmqplugins) do + + commands :rabbitmqplugins => 'rabbitmq-plugins' + defaultfor :feature => :posix + + def self.instances + rabbitmqplugins('list', '-E').split(/\n/).map do |line| + if line.split(/\s+/)[1] =~ /^(\S+)$/ + new(:name => $1) + else + raise Puppet::Error, "Cannot parse invalid plugins line: #{line}" + end + end + end + + def create + rabbitmqplugins('enable', resource[:name]) + end + + def destroy + rabbitmqplugins('disable', resource[:name]) + end + + def exists? + out = rabbitmqplugins('list', '-E').split(/\n/).detect do |line| + line.split(/\s+/)[1].match(/^#{resource[:name]}$/) + end + end + +end diff --git a/puppet/rabbitmq/lib/puppet/provider/rabbitmq_user/default.rb b/puppet/rabbitmq/lib/puppet/provider/rabbitmq_user/default.rb new file mode 100644 index 000000000..8915dd810 --- /dev/null +++ b/puppet/rabbitmq/lib/puppet/provider/rabbitmq_user/default.rb @@ -0,0 +1,22 @@ +Puppet::Type.type(:rabbitmq_user).provide(:default) do + + def self.instances + [] + end + + def create + default_fail + end + + def destroy + default_fail + end + + def exists? + default_fail + end + + def default_fail + fail('This is just the default provider for rabbitmq_user, all it does is fail') + end +end diff --git a/puppet/rabbitmq/lib/puppet/provider/rabbitmq_user/rabbitmqctl.rb b/puppet/rabbitmq/lib/puppet/provider/rabbitmq_user/rabbitmqctl.rb new file mode 100644 index 000000000..dbaeab43c --- /dev/null +++ b/puppet/rabbitmq/lib/puppet/provider/rabbitmq_user/rabbitmqctl.rb @@ -0,0 +1,59 @@ +require 'puppet' +Puppet::Type.type(:rabbitmq_user).provide(:rabbitmqctl) do + + optional_commands :rabbitmqctl => 'rabbitmqctl' + defaultfor :feature => :posix + + def self.instances + rabbitmqctl('list_users').split(/\n/)[1..-2].collect do |line| + if line =~ /^(\S+)(\s+\S+|)$/ + new(:name => $1) + else + raise Puppet::Error, "Cannot parse invalid user line: #{line}" + end + end + end + + def create + rabbitmqctl('add_user', resource[:name], resource[:password]) + if resource[:admin] == :true + make_user_admin() + end + end + + def destroy + rabbitmqctl('delete_user', resource[:name]) + end + + def exists? + out = rabbitmqctl('list_users').split(/\n/)[1..-2].detect do |line| + line.match(/^#{resource[:name]}(\s+\S+|)$/) + end + end + + # def password + # def password=() + def admin + match = rabbitmqctl('list_users').split(/\n/)[1..-2].collect do |line| + line.match(/^#{resource[:name]}\s+\[(administrator)?\]/) + end.compact.first + if match + (:true if match[1].to_s == 'administrator') || :false + else + raise Puppet::Error, "Could not match line '#{resource[:name]} (true|false)' from list_users (perhaps you are running on an older version of rabbitmq that does not support admin users?)" + end + end + + def admin=(state) + if state == :true + make_user_admin() + else + rabbitmqctl('set_user_tags', resource[:name]) + end + end + + def make_user_admin + rabbitmqctl('set_user_tags', resource[:name], 'administrator') + end + +end diff --git a/puppet/rabbitmq/lib/puppet/provider/rabbitmq_user_permissions/default.rb b/puppet/rabbitmq/lib/puppet/provider/rabbitmq_user_permissions/default.rb new file mode 100644 index 000000000..42ee0d91d --- /dev/null +++ b/puppet/rabbitmq/lib/puppet/provider/rabbitmq_user_permissions/default.rb @@ -0,0 +1,18 @@ +Puppet::Type.type(:rabbitmq_user_permissions).provide(:default) do + + def create + default_fail + end + + def destroy + default_fail + end + + def exists? + default_fail + end + + def default_fail + fail('This is just the default provider for rabbitmq_user, all it does is fail') + end +end diff --git a/puppet/rabbitmq/lib/puppet/provider/rabbitmq_user_permissions/rabbitmqctl.rb b/puppet/rabbitmq/lib/puppet/provider/rabbitmq_user_permissions/rabbitmqctl.rb new file mode 100644 index 000000000..912f94bbd --- /dev/null +++ b/puppet/rabbitmq/lib/puppet/provider/rabbitmq_user_permissions/rabbitmqctl.rb @@ -0,0 +1,102 @@ +Puppet::Type.type(:rabbitmq_user_permissions).provide(:rabbitmqctl) do + + optional_commands :rabbitmqctl => 'rabbitmqctl' + defaultfor :feature=> :posix + + #def self.instances + # + #end + + # cache users permissions + def self.users(name, vhost) + @users = {} unless @users + unless @users[name] + @users[name] = {} + out = rabbitmqctl('list_user_permissions', name).split(/\n/)[1..-2].each do |line| + if line =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/ + @users[name][$1] = + {:configure => $2, :read => $3, :write => $4} + else + raise Puppet::Error, "cannot parse line from list_user_permissions:#{line}" + end + end + end + @users[name][vhost] + end + + def users(name, vhost) + self.class.users(name, vhost) + end + + def should_user + if @should_user + @should_user + else + @should_user = resource[:name].split('@')[0] + end + end + + def should_vhost + if @should_vhost + @should_vhost + else + @should_vhost = resource[:name].split('@')[1] + end + end + + def create + resource[:configure_permission] ||= "''" + resource[:read_permission] ||= "''" + resource[:write_permission] ||= "''" + rabbitmqctl('set_permissions', '-p', should_vhost, should_user, resource[:configure_permission], resource[:read_permission], resource[:write_permission]) + end + + def destroy + rabbitmqctl('clear_permissions', '-p', should_vhost, should_user) + end + + # I am implementing prefetching in exists b/c I need to be sure + # that the rabbitmq package is installed before I make this call. + def exists? + users(should_user, should_vhost) + end + + def configure_permission + users(should_user, should_vhost)[:configure] + end + + def configure_permission=(perm) + set_permissions + end + + def read_permission + users(should_user, should_vhost)[:read] + end + + def read_permission=(perm) + set_permissions + end + + def write_permission + users(should_user, should_vhost)[:write] + end + + def write_permission=(perm) + set_permissions + end + + # implement memoization so that we only call set_permissions once + def set_permissions + unless @permissions_set + @permissions_set = true + resource[:configure_permission] ||= configure_permission + resource[:read_permission] ||= read_permission + resource[:write_permission] ||= write_permission + rabbitmqctl('set_permissions', '-p', should_vhost, should_user, + resource[:configure_permission], resource[:read_permission], + resource[:write_permission] + ) + end + end + +end diff --git a/puppet/rabbitmq/lib/puppet/provider/rabbitmq_vhost/default.rb b/puppet/rabbitmq/lib/puppet/provider/rabbitmq_vhost/default.rb new file mode 100644 index 000000000..c4118176a --- /dev/null +++ b/puppet/rabbitmq/lib/puppet/provider/rabbitmq_vhost/default.rb @@ -0,0 +1,22 @@ +Puppet::Type.type(:rabbitmq_vhost).provide(:default) do + + def self.instances + [] + end + + def create + default_fail + end + + def destroy + default_fail + end + + def exists? + default_fail + end + + def default_fail + fail('This is just the default provider for rabbitmq_vhost, all it does is fail') + end +end diff --git a/puppet/rabbitmq/lib/puppet/provider/rabbitmq_vhost/rabbitmqctl.rb b/puppet/rabbitmq/lib/puppet/provider/rabbitmq_vhost/rabbitmqctl.rb new file mode 100644 index 000000000..31ceefd24 --- /dev/null +++ b/puppet/rabbitmq/lib/puppet/provider/rabbitmq_vhost/rabbitmqctl.rb @@ -0,0 +1,30 @@ +Puppet::Type.type(:rabbitmq_vhost).provide(:rabbitmqctl) do + + optional_commands :rabbitmqctl => 'rabbitmqctl' + defaultfor :feature => :posix + + def self.instances + rabbitmqctl('list_vhosts').split(/\n/)[1..-2].map do |line| + if line =~ /^(\S+)$/ + new(:name => $1) + else + raise Puppet::Error, "Cannot parse invalid user line: #{line}" + end + end + end + + def create + rabbitmqctl('add_vhost', resource[:name]) + end + + def destroy + rabbitmqctl('delete_vhost', resource[:name]) + end + + def exists? + out = rabbitmqctl('list_vhosts').split(/\n/)[1..-2].detect do |line| + line.match(/^#{resource[:name]}$/) + end + end + +end diff --git a/puppet/rabbitmq/lib/puppet/type/rabbitmq_plugin.rb b/puppet/rabbitmq/lib/puppet/type/rabbitmq_plugin.rb new file mode 100644 index 000000000..497ced4e6 --- /dev/null +++ b/puppet/rabbitmq/lib/puppet/type/rabbitmq_plugin.rb @@ -0,0 +1,19 @@ +Puppet::Type.newtype(:rabbitmq_plugin) do + desc 'manages rabbitmq plugins' + + ensurable do + defaultto(:present) + newvalue(:present) do + provider.create + end + newvalue(:absent) do + provider.destroy + end + end + + newparam(:name, :namevar => true) do + 'name of the plugin to enable' + newvalues(/^\S+$/) + end + +end diff --git a/puppet/rabbitmq/lib/puppet/type/rabbitmq_user.rb b/puppet/rabbitmq/lib/puppet/type/rabbitmq_user.rb new file mode 100644 index 000000000..9ad9c0f7c --- /dev/null +++ b/puppet/rabbitmq/lib/puppet/type/rabbitmq_user.rb @@ -0,0 +1,40 @@ +Puppet::Type.newtype(:rabbitmq_user) do + desc 'Native type for managing rabbitmq users' + + ensurable do + defaultto(:present) + newvalue(:present) do + provider.create + end + newvalue(:absent) do + provider.destroy + end + end + + newparam(:name, :namevar => true) do + desc 'Name of user' + newvalues(/^\S+$/) + end + + # newproperty(:password) do + newparam(:password) do + desc 'User password to be set *on creation*' + end + + newproperty(:admin) do + desc 'rather or not user should be an admin' + newvalues(/true|false/) + munge do |value| + # converting to_s incase its a boolean + value.to_s.to_sym + end + defaultto :false + end + + validate do + if self[:ensure] == :present and ! self[:password] + raise ArgumentError, 'must set password when creating user' unless self[:password] + end + end + +end diff --git a/puppet/rabbitmq/lib/puppet/type/rabbitmq_user_permissions.rb b/puppet/rabbitmq/lib/puppet/type/rabbitmq_user_permissions.rb new file mode 100644 index 000000000..427da6caa --- /dev/null +++ b/puppet/rabbitmq/lib/puppet/type/rabbitmq_user_permissions.rb @@ -0,0 +1,57 @@ +Puppet::Type.newtype(:rabbitmq_user_permissions) do + desc 'Type for managing rabbitmq user permissions' + + ensurable do + defaultto(:present) + newvalue(:present) do + provider.create + end + newvalue(:absent) do + provider.destroy + end + end + + newparam(:name, :namevar => true) do + 'combination of user@vhost to grant privileges to' + newvalues(/^\S+@\S+$/) + end + + newproperty(:configure_permission) do + desc 'regexp representing configuration permissions' + validate do |value| + resource.validate_permissions(value) + end + end + + newproperty(:read_permission) do + desc 'regexp representing read permissions' + validate do |value| + resource.validate_permissions(value) + end + end + + newproperty(:write_permission) do + desc 'regexp representing write permissions' + validate do |value| + resource.validate_permissions(value) + end + end + + autorequire(:rabbitmq_vhost) do + [self[:name].split('@')[1]] + end + + autorequire(:rabbitmq_user) do + [self[:name].split('@')[0]] + end + + # I may want to dissalow whitespace + def validate_permissions(value) + begin + Regexp.new(value) + rescue RegexpError + raise ArgumentError, "Invalid regexp #{value}" + end + end + +end diff --git a/puppet/rabbitmq/lib/puppet/type/rabbitmq_vhost.rb b/puppet/rabbitmq/lib/puppet/type/rabbitmq_vhost.rb new file mode 100644 index 000000000..9dd0982dc --- /dev/null +++ b/puppet/rabbitmq/lib/puppet/type/rabbitmq_vhost.rb @@ -0,0 +1,19 @@ +Puppet::Type.newtype(:rabbitmq_vhost) do + desc 'manages rabbitmq vhosts' + + ensurable do + defaultto(:present) + newvalue(:present) do + provider.create + end + newvalue(:absent) do + provider.destroy + end + end + + newparam(:name, :namevar => true) do + 'name of the vhost to add' + newvalues(/^\S+$/) + end + +end diff --git a/puppet/rabbitmq/manifests/repo/apt.pp b/puppet/rabbitmq/manifests/repo/apt.pp new file mode 100644 index 000000000..065315776 --- /dev/null +++ b/puppet/rabbitmq/manifests/repo/apt.pp @@ -0,0 +1,28 @@ +# sets up the vmware hosted apt repo +# requires +# puppetlabs-apt +# puppetlabs-stdlib +class rabbitmq::repo::apt( + $pin = undef +) { + + Class['rabbitmq::repo::apt'] -> Package<| title == 'rabbitmq-server' |> + + apt::source { 'rabbitmq': + location => 'http://www.rabbitmq.com/debian/', + release => 'testing', + repos => 'main', + include_src => false, + key => 'RabbitMQ Release Signing Key ', + key_content => template('rabbitmq/rabbit.pub.key'), + pin => $pin, + } + + if ! ($pin == undef) { + validate_re($pin, '\d\d\d') + apt::pin { 'rabbitmq': + packages => 'rabbitmq-server', + priority => $pin, + } + } +} diff --git a/puppet/rabbitmq/manifests/server.pp b/puppet/rabbitmq/manifests/server.pp new file mode 100644 index 000000000..f20fd4f04 --- /dev/null +++ b/puppet/rabbitmq/manifests/server.pp @@ -0,0 +1,138 @@ +# Class: rabbitmq::server +# +# This module manages the installation and config of the rabbitmq server +# it has only been tested on certain version of debian-ish systems +# Parameters: +# [*port*] - port where rabbitmq server is hosted +# [*delete_guest_user*] - rather or not to delete the default user +# [*version*] - version of rabbitmq-server to install +# [*package_name*] - name of rabbitmq package +# [*service_name*] - name of rabbitmq service +# [*service_ensure*] - desired ensure state for service +# [*stomp_port*] - port stomp should be listening on +# [*node_ip_address*] - ip address for rabbitmq to bind to +# [*config*] - contents of config file +# [*env_config*] - contents of env-config file +# Requires: +# stdlib +# Sample Usage: +# +# +# +# +# [Remember: No empty lines between comments and class definition] +class rabbitmq::server( + $port = '5672', + $inet_dist_listen_min = '41055', + $inet_dist_listen_max = '41055', + $delete_guest_user = false, + $package_name = 'rabbitmq-server', + $version = 'UNSET', + $service_name = 'rabbitmq-server', + $service_ensure = 'running', + $config_stomp = false, + $stomp_port = '6163', + $config_cluster = false, + $cluster_disk_nodes = [], + $node_ip_address = 'UNSET', + $config='UNSET', + $env_config='UNSET' +) { + + validate_bool($delete_guest_user, $config_stomp) + validate_re($port, '\d+') + validate_re($stomp_port, '\d+') + + if $version == 'UNSET' { + $version_real = '2.4.1' + $pkg_ensure_real = 'present' + } else { + $version_real = $version + $pkg_ensure_real = $version + } + if $config == 'UNSET' { + $config_real = template("${module_name}/rabbitmq.config") + } else { + $config_real = $config + } + if $env_config == 'UNSET' { + $env_config_real = template("${module_name}/rabbitmq-env.conf.erb") + } else { + $env_config_real = $env_config + } + + $plugin_dir = "/usr/lib/rabbitmq/lib/rabbitmq_server-${version_real}/plugins" + $erlang_cookie_content = 'EOKOWXQREETZSHFNTPEY' + + if $::osfamily == 'RedHat' { + package { 'qpid-cpp-server': + ensure => 'purged', + before => Package[$package_name] + } + } + + package { $package_name: + ensure => $pkg_ensure_real, + notify => Class['rabbitmq::service'], + } + + file { '/etc/rabbitmq': + ensure => directory, + owner => '0', + group => '0', + mode => '0644', + require => Package[$package_name], + } + + file { 'rabbitmq.config': + ensure => file, + path => '/etc/rabbitmq/rabbitmq.config', + content => $config_real, + owner => '0', + group => '0', + mode => '0644', + notify => Class['rabbitmq::service'], + require => File['erlang_cookie'] + } + + file { 'rabbitmq-env.config': + ensure => file, + path => '/etc/rabbitmq/rabbitmq-env.conf', + content => $env_config_real, + owner => '0', + group => '0', + mode => '0644', + notify => Class['rabbitmq::service'], + } + + class { 'rabbitmq::service': + service_name => $service_name, + ensure => $service_ensure, + } + + exec { 'rabbitmq_stop': + command => '/etc/init.d/rabbitmq-server stop; /bin/rm -rf /var/lib/rabbitmq/mnesia', + require => Package[$package_name], + unless => "/bin/grep -qx ${erlang_cookie_content} /var/lib/rabbitmq/.erlang.cookie" + } + + file { 'erlang_cookie': + path =>"/var/lib/rabbitmq/.erlang.cookie", + owner => rabbitmq, + group => rabbitmq, + mode => '0400', + content => $erlang_cookie_content, + replace => true, + #notify => Class['rabbitmq::service'], + require => Exec['rabbitmq_stop'], + } + + if $delete_guest_user { + # delete the default guest user + rabbitmq_user{ 'guest': + ensure => absent, + provider => 'rabbitmqctl', + } + } + +} diff --git a/puppet/rabbitmq/manifests/service.pp b/puppet/rabbitmq/manifests/service.pp new file mode 100644 index 000000000..3ed55726a --- /dev/null +++ b/puppet/rabbitmq/manifests/service.pp @@ -0,0 +1,40 @@ +# Class: rabbitmq::service +# +# This class manages the rabbitmq server service itself. +# +# Jeff McCune +# +# +# Parameters: +# +# Actions: +# +# Requires: +# +# Sample Usage: +# +class rabbitmq::service( + $service_name = 'rabbitmq-server', + $ensure='running' +) { + + validate_re($ensure, '^(running|stopped)$') + if $ensure == 'running' { + Class['rabbitmq::service'] -> Rabbitmq_user<| |> + Class['rabbitmq::service'] -> Rabbitmq_vhost<| |> + Class['rabbitmq::service'] -> Rabbitmq_user_permissions<| |> + $ensure_real = 'running' + $enable_real = true + } else { + $ensure_real = 'stopped' + $enable_real = false + } + + service { $service_name: + ensure => $ensure_real, + enable => $enable_real, + hasstatus => true, + hasrestart => true, + } + +} diff --git a/puppet/rabbitmq/spec/README.markdown b/puppet/rabbitmq/spec/README.markdown new file mode 100644 index 000000000..286d3417d --- /dev/null +++ b/puppet/rabbitmq/spec/README.markdown @@ -0,0 +1,7 @@ +Specs +===== + +The Puppet project uses RSpec for testing. + +For more information on RSpec, see http://rspec.info/ + diff --git a/puppet/rabbitmq/spec/classes/rabbitmq_server_spec.rb b/puppet/rabbitmq/spec/classes/rabbitmq_server_spec.rb new file mode 100644 index 000000000..133915f29 --- /dev/null +++ b/puppet/rabbitmq/spec/classes/rabbitmq_server_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe 'rabbitmq::server' do + + let :facts do + # Needed for statement in rabbitmq.config template. + { :puppetversion => '2.7.14' } + end + + describe 'package with default params' do + it { should contain_package('rabbitmq-server').with( + 'ensure' => 'present' + ) } + end + + describe 'package with specified ensure' do + let :params do + { :version => "2.3.0" } + end + it { should contain_package('rabbitmq-server').with( + 'ensure' => '2.3.0' + ) } + end + + describe 'not deleting guest user by default' do + it { should_not contain_rabbitmq_user('guest') } + end + + describe 'deleting guest user' do + let :params do + { :delete_guest_user => true } + end + it { should contain_rabbitmq_user('guest').with( + 'ensure' => 'absent', + 'provider' => 'rabbitmqctl' + ) } + end + + describe 'default service include' do + it { should contain_class('rabbitmq::service').with( + 'service_name' => 'rabbitmq-server', + 'ensure' => 'running' + ) } + end + + describe 'overriding service paramters' do + let :params do + { 'service_name' => 'custom-rabbitmq-server', + 'service_ensure' => 'stopped' + } + end + it { should contain_class('rabbitmq::service').with( + 'service_name' => 'custom-rabbitmq-server', + 'ensure' => 'stopped' + ) } + end + + describe 'specifing node_ip_address' do + let :params do + { :node_ip_address => '172.0.0.1' } + end + it 'should set RABBITMQ_NODE_IP_ADDRESS to specified value' do + verify_contents(subject, 'rabbitmq-env.config', + ['RABBITMQ_NODE_IP_ADDRESS=172.0.0.1']) + end + end + + describe 'not configuring stomp by default' do + it 'should not specify stomp parameters in rabbitmq.config' do + verify_contents(subject, 'rabbitmq.config', + ['[].']) + end + end + + describe 'configuring stomp' do + let :params do + { :config_stomp => true, + :stomp_port => 5679 + } + end + it 'should specify stomp port in rabbitmq.config' do + verify_contents(subject, 'rabbitmq.config', + ['[ {rabbitmq_stomp, [{tcp_listeners, [5679]} ]} ].']) + end + + end + +end diff --git a/puppet/rabbitmq/spec/fixtures/manifests/site.pp b/puppet/rabbitmq/spec/fixtures/manifests/site.pp new file mode 100644 index 000000000..e69de29bb diff --git a/puppet/rabbitmq/spec/spec.opts b/puppet/rabbitmq/spec/spec.opts new file mode 100644 index 000000000..91cd6427e --- /dev/null +++ b/puppet/rabbitmq/spec/spec.opts @@ -0,0 +1,6 @@ +--format +s +--colour +--loadby +mtime +--backtrace diff --git a/puppet/rabbitmq/spec/spec_helper.rb b/puppet/rabbitmq/spec/spec_helper.rb new file mode 100644 index 000000000..2c6f56649 --- /dev/null +++ b/puppet/rabbitmq/spec/spec_helper.rb @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/module_spec_helper' diff --git a/puppet/rabbitmq/spec/unit/puppet/provider/rabbitmq_user/rabbitmqctl_spec.rb b/puppet/rabbitmq/spec/unit/puppet/provider/rabbitmq_user/rabbitmqctl_spec.rb new file mode 100644 index 000000000..4cfa95111 --- /dev/null +++ b/puppet/rabbitmq/spec/unit/puppet/provider/rabbitmq_user/rabbitmqctl_spec.rb @@ -0,0 +1,103 @@ +require 'puppet' +require 'mocha' +RSpec.configure do |config| + config.mock_with :mocha +end +provider_class = Puppet::Type.type(:rabbitmq_user).provider(:rabbitmqctl) +describe provider_class do + before :each do + @resource = Puppet::Type::Rabbitmq_user.new( + {:name => 'foo', :password => 'bar'} + ) + @provider = provider_class.new(@resource) + end + it 'should match user names' do + @provider.expects(:rabbitmqctl).with('list_users').returns <<-EOT +Listing users ... +foo +...done. +EOT + @provider.exists?.should == 'foo' + end + it 'should match user names with 2.4.1 syntax' do + @provider.expects(:rabbitmqctl).with('list_users').returns <<-EOT +Listing users ... +foo bar +...done. +EOT + @provider.exists?.should == 'foo bar' + end + it 'should not match if no users on system' do + @provider.expects(:rabbitmqctl).with('list_users').returns <<-EOT +Listing users ... +...done. +EOT + @provider.exists?.should be_nil + end + it 'should not match if no matching users on system' do + @provider.expects(:rabbitmqctl).with('list_users').returns <<-EOT +Listing users ... +fooey +...done. +EOT + @provider.exists?.should be_nil + end + it 'should match user names from list' do + @provider.expects(:rabbitmqctl).with('list_users').returns <<-EOT +Listing users ... +one +two three +foo +bar +...done. +EOT + @provider.exists?.should == 'foo' + end + it 'should create user and set password' do + @resource[:password] = 'bar' + @provider.expects(:rabbitmqctl).with('add_user', 'foo', 'bar') + @provider.create + end + it 'shoud create user, set password and set to admin' do + @resource[:password] = 'bar' + @resource[:admin] = 'true' + @provider.expects(:rabbitmqctl).with('add_user', 'foo', 'bar') + @provider.expects(:rabbitmqctl).with('set_user_tags', 'foo', 'administrator') + @provider.create + end + it 'should call rabbitmqctl to delete' do + @provider.expects(:rabbitmqctl).with('delete_user', 'foo') + @provider.destroy + end + it 'should be able to retrieve admin value' do + @provider.expects(:rabbitmqctl).with('list_users').returns <<-EOT +Listing users ... +foo [administrator] +...done. +EOT + @provider.admin.should == :true + @provider.expects(:rabbitmqctl).with('list_users').returns <<-EOT +Listing users ... +one [administrator] +foo [] +...done. +EOT + @provider.admin.should == :false + end + it 'should fail if admin value is invalid' do + @provider.expects(:rabbitmqctl).with('list_users').returns <<-EOT +Listing users ... +foo fail +...done. +EOT + expect { @provider.admin }.should raise_error(Puppet::Error, /Could not match line/) + end + it 'should be able to set admin value' do + @provider.expects(:rabbitmqctl).with('set_user_tags', 'foo', 'administrator') + @provider.admin=:true + end + it 'should be able to unset admin value' do + @provider.expects(:rabbitmqctl).with('set_user_tags', 'foo') + @provider.admin=:false + end +end diff --git a/puppet/rabbitmq/spec/unit/puppet/provider/rabbitmq_user_permissions/rabbitmqctl_spec.rb b/puppet/rabbitmq/spec/unit/puppet/provider/rabbitmq_user_permissions/rabbitmqctl_spec.rb new file mode 100644 index 000000000..88689103a --- /dev/null +++ b/puppet/rabbitmq/spec/unit/puppet/provider/rabbitmq_user_permissions/rabbitmqctl_spec.rb @@ -0,0 +1,100 @@ +require 'puppet' +require 'mocha' +RSpec.configure do |config| + config.mock_with :mocha +end +describe 'Puppet::Type.type(:rabbitmq_user_permissions).provider(:rabbitmqctl)' do + before :each do + @provider_class = Puppet::Type.type(:rabbitmq_user_permissions).provider(:rabbitmqctl) + @resource = Puppet::Type::Rabbitmq_user_permissions.new( + {:name => 'foo@bar'} + ) + @provider = @provider_class.new(@resource) + end + after :each do + @provider_class.instance_variable_set(:@users, nil) + end + it 'should match user permissions from list' do + @provider.class.expects(:rabbitmqctl).with('list_user_permissions', 'foo').returns <<-EOT +Listing users ... +bar 1 2 3 +...done. +EOT + @provider.exists?.should == {:configure=>"1", :write=>"3", :read=>"2"} + end + it 'should not match user permissions with more than 3 columns' do + @provider.class.expects(:rabbitmqctl).with('list_user_permissions', 'foo').returns <<-EOT +Listing users ... +bar 1 2 3 4 +...done. +EOT + expect { @provider.exists? }.should raise_error(Puppet::Error, /cannot parse line from list_user_permissions/) + end + it 'should not match an empty list' do + @provider.class.expects(:rabbitmqctl).with('list_user_permissions', 'foo').returns <<-EOT +Listing users ... +...done. +EOT + @provider.exists?.should == nil + end + it 'should create default permissions' do + @provider.instance_variable_set(:@should_vhost, "bar") + @provider.instance_variable_set(:@should_user, "foo") + @provider.expects(:rabbitmqctl).with('set_permissions', '-p', 'bar', 'foo', "''", "''", "''") + @provider.create + end + it 'should destroy permissions' do + @provider.instance_variable_set(:@should_vhost, "bar") + @provider.instance_variable_set(:@should_user, "foo") + @provider.expects(:rabbitmqctl).with('clear_permissions', '-p', 'bar', 'foo') + @provider.destroy + end + {:configure_permission => '1', :write_permission => '3', :read_permission => '2'}.each do |k,v| + it "should be able to retrieve #{k}" do + @provider.class.expects(:rabbitmqctl).with('list_user_permissions', 'foo').returns <<-EOT +Listing users ... +bar 1 2 3 +...done. +EOT + @provider.send(k).should == v + end + end + {:configure_permission => '1', :write_permission => '3', :read_permission => '2'}.each do |k,v| + it "should be able to retrieve #{k} after exists has been called" do + @provider.class.expects(:rabbitmqctl).with('list_user_permissions', 'foo').returns <<-EOT +Listing users ... +bar 1 2 3 +...done. +EOT + @provider.exists? + @provider.send(k).should == v + end + end + {:configure_permission => ['foo', '2', '3'], + :read_permission => ['1', 'foo', '3'], + :write_permission => ['1', '2', 'foo'] + }.each do |perm, columns| + it "should be able to sync #{perm}" do + @provider.class.expects(:rabbitmqctl).with('list_user_permissions', 'foo').returns <<-EOT +Listing users ... +bar 1 2 3 +...done. +EOT + @provider.resource[perm] = 'foo' + @provider.expects(:rabbitmqctl).with('set_permissions', '-p', 'bar', 'foo', *columns) + @provider.send("#{perm}=".to_sym, 'foo') + end + end + it 'should only call set_permissions once' do + @provider.class.expects(:rabbitmqctl).with('list_user_permissions', 'foo').returns <<-EOT +Listing users ... +bar 1 2 3 +...done. +EOT + @provider.resource[:configure_permission] = 'foo' + @provider.resource[:read_permission] = 'foo' + @provider.expects(:rabbitmqctl).with('set_permissions', '-p', 'bar', 'foo', 'foo', 'foo', '3').once + @provider.configure_permission='foo' + @provider.read_permission='foo' + end +end diff --git a/puppet/rabbitmq/spec/unit/puppet/provider/rabbitmq_vhost/rabbitmqctl_spec.rb b/puppet/rabbitmq/spec/unit/puppet/provider/rabbitmq_vhost/rabbitmqctl_spec.rb new file mode 100644 index 000000000..a1f89ad2c --- /dev/null +++ b/puppet/rabbitmq/spec/unit/puppet/provider/rabbitmq_vhost/rabbitmqctl_spec.rb @@ -0,0 +1,45 @@ +require 'puppet' +require 'mocha' +RSpec.configure do |config| + config.mock_with :mocha +end +provider_class = Puppet::Type.type(:rabbitmq_vhost).provider(:rabbitmqctl) +describe provider_class do + before :each do + @resource = Puppet::Type::Rabbitmq_vhost.new( + {:name => 'foo'} + ) + @provider = provider_class.new(@resource) + end + it 'should match vhost names' do + @provider.expects(:rabbitmqctl).with('list_vhosts').returns <<-EOT +Listing vhosts ... +foo +...done. +EOT + @provider.exists?.should == 'foo' + end + it 'should not match if no vhosts on system' do + @provider.expects(:rabbitmqctl).with('list_vhosts').returns <<-EOT +Listing vhosts ... +...done. +EOT + @provider.exists?.should be_nil + end + it 'should not match if no matching vhosts on system' do + @provider.expects(:rabbitmqctl).with('list_vhosts').returns <<-EOT +Listing vhosts ... +fooey +...done. +EOT + @provider.exists?.should be_nil + end + it 'should call rabbitmqctl to create' do + @provider.expects(:rabbitmqctl).with('add_vhost', 'foo') + @provider.create + end + it 'should call rabbitmqctl to create' do + @provider.expects(:rabbitmqctl).with('delete_vhost', 'foo') + @provider.destroy + end +end diff --git a/puppet/rabbitmq/spec/unit/puppet/type/rabbitmq_user_permissions_spec.rb b/puppet/rabbitmq/spec/unit/puppet/type/rabbitmq_user_permissions_spec.rb new file mode 100644 index 000000000..87c6eaf37 --- /dev/null +++ b/puppet/rabbitmq/spec/unit/puppet/type/rabbitmq_user_permissions_spec.rb @@ -0,0 +1,45 @@ +require 'puppet' +require 'puppet/type/rabbitmq_user_permissions' +describe Puppet::Type.type(:rabbitmq_user_permissions) do + before :each do + @perms = Puppet::Type.type(:rabbitmq_user_permissions).new(:name => 'foo@bar') + end + it 'should accept a valid hostname name' do + @perms[:name] = 'dan@bar' + @perms[:name].should == 'dan@bar' + end + it 'should require a name' do + expect { Puppet::Type.type(:rabbitmq_user_permissions).new({}) }.should raise_error(Puppet::Error, 'Title or name must be provided') + end + it 'should fail when names dont have a @' do + expect { @perms[:name] = 'bar' }.should raise_error(Puppet::Error, /Valid values match/) + end + [:configure_permission, :read_permission, :write_permission].each do |param| + it 'should not default to anything' do + @perms[param].should == nil + end + it "should accept a valid regex for #{param}" do + @perms[param] = '.*?' + @perms[param].should == '.*?' + end + it "should not accept invalid regex for #{param}" do + expect { @perms[param] = '*' }.should raise_error(Puppet::Error, /Invalid regexp/) + end + end + {:rabbitmq_vhost => 'dan@test', :rabbitmq_user => 'test@dan'}.each do |k,v| + it "should autorequire #{k}" do + if k == :rabbitmq_vhost + vhost = Puppet::Type.type(k).new(:name => "test") + else + vhost = Puppet::Type.type(k).new(:name => "test", :password => 'pass') + end + perm = Puppet::Type.type(:rabbitmq_user_permissions).new(:name => v) + config = Puppet::Resource::Catalog.new :testing do |conf| + [vhost, perm].each { |resource| conf.add_resource resource } + end + rel = perm.autorequire[0] + rel.source.ref.should == vhost.ref + rel.target.ref.should == perm.ref + end + end +end diff --git a/puppet/rabbitmq/spec/unit/puppet/type/rabbitmq_user_spec.rb b/puppet/rabbitmq/spec/unit/puppet/type/rabbitmq_user_spec.rb new file mode 100644 index 000000000..2fef4f44b --- /dev/null +++ b/puppet/rabbitmq/spec/unit/puppet/type/rabbitmq_user_spec.rb @@ -0,0 +1,34 @@ +require 'puppet' +require 'puppet/type/rabbitmq_user' +describe Puppet::Type.type(:rabbitmq_user) do + before :each do + @user = Puppet::Type.type(:rabbitmq_user).new(:name => 'foo', :password => 'pass') + end + it 'should accept a user name' do + @user[:name] = 'dan' + @user[:name].should == 'dan' + @user[:admin].should == :false + end + it 'should accept a password' do + @user[:password] = 'foo' + @user[:password].should == 'foo' + end + it 'should require a password' do + expect {Puppet::Type.type(:rabbitmq_user).new(:name => 'foo') }.should raise_error(ArgumentError, /must set password/) + end + it 'should require a name' do + expect { Puppet::Type.type(:rabbitmq_user).new({}) }.should raise_error(Puppet::Error, 'Title or name must be provided') + end + it 'should not allow whitespace in the name' do + expect { @user[:name] = 'b r' }.should raise_error(Puppet::Error, /Valid values match/) + end + [true, false, 'true', 'false'].each do |val| + it "admin property should accept #{val}" do + @user[:admin] = val + @user[:admin].should == val.to_s.to_sym + end + end + it 'should not accept non-boolean values for admin' do + expect { @user[:admin] = 'yes' }.should raise_error(Puppet::Error, /Invalid value/) + end +end diff --git a/puppet/rabbitmq/spec/unit/puppet/type/rabbitmq_vhost_spec.rb b/puppet/rabbitmq/spec/unit/puppet/type/rabbitmq_vhost_spec.rb new file mode 100644 index 000000000..92fcb2e95 --- /dev/null +++ b/puppet/rabbitmq/spec/unit/puppet/type/rabbitmq_vhost_spec.rb @@ -0,0 +1,17 @@ +require 'puppet' +require 'puppet/type/rabbitmq_vhost' +describe Puppet::Type.type(:rabbitmq_vhost) do + before :each do + @vhost = Puppet::Type.type(:rabbitmq_vhost).new(:name => 'foo') + end + it 'should accept a vhost name' do + @vhost[:name] = 'dan' + @vhost[:name].should == 'dan' + end + it 'should require a name' do + expect { Puppet::Type.type(:rabbitmq_vhost).new({}) }.should raise_error(Puppet::Error, 'Title or name must be provided') + end + it 'should not allow whitespace in the name' do + expect { @vhost[:name] = 'b r' }.should raise_error(Puppet::Error, /Valid values match/) + end +end diff --git a/puppet/rabbitmq/templates/README.markdown b/puppet/rabbitmq/templates/README.markdown new file mode 100644 index 000000000..575bbeaef --- /dev/null +++ b/puppet/rabbitmq/templates/README.markdown @@ -0,0 +1,23 @@ +Templates +========= + +Puppet supports templates and templating via ERB, which is part of the Ruby +standard library and is used for many other projects including Ruby on Rails. +Templates allow you to manage the content of template files, for example +configuration files that cannot yet be managed as a Puppet type. Learn more at +http://projects.puppetlabs.com/projects/puppet/wiki/Puppet_Templating + +You can use templates like this: + + class myclass { + package { mypackage: ensure => latest } + service { myservice: ensure => running } + file { "/etc/myfile": + content => template("mymodule/myfile.erb") + } + } + +The templates are searched for in: + + $templatedir/mymodule/myfile.erb + $modulepath/mymodule/templates/myfile.erb diff --git a/puppet/rabbitmq/templates/rabbit.pub.key b/puppet/rabbitmq/templates/rabbit.pub.key new file mode 100644 index 000000000..7195cf707 --- /dev/null +++ b/puppet/rabbitmq/templates/rabbit.pub.key @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.6 (GNU/Linux) + +mQGiBEaOQ/IRBACs/n609zN+OzlK9qDkFWwYKfPG+BlgqBj5MSy0XE2K8cE3bWSV +2WftTe/TGEfW0hknXt1PyBla0cnO9Up1xCn142vo8bvUug8WjrxLQBBiAf11FAOR +dt9roGe4IWw/Lakgb88re09ZYKmOL9H7MEpvMqtjdWjFSq4zeeGa8rGEswCgnQLb +ZD/MNlUNQwQVCs+vVRdgpzcD+QELSc2EeYl4tef0NiUaZQt+mjFTs3DjQNDTjXao +ETVAqECx4kavcshx5tSE5JbbQPIMiUgh0h9J3z3uZsBVnx6P82aW/QTw+jLhsQry +/i3Z/+pS66mk6EWhAAYF/SPVqM/06BZh0ZvUmeG9WGGJXD9CUN1Wfi2mt42L2zhT +xg3uBACoIs5/GORi0H2i+blLiFSxTroXw+TdxiP+mfjdPho0oXJQTljXBgG70VfX +XW9sWsYtekqXBsmwMcbCZTjZGul/8jAUlUoYfthRw9KpP9N8Q7wB8Flx9jEv0M0H +tV1KTrLuXNZvEAB1sECMa7RRrV1yO4wyYDsOXiZNTL6rYugOU7QwUmFiYml0TVEg +UmVsZWFzZSBTaWduaW5nIEtleSA8aW5mb0ByYWJiaXRtcS5jb20+iGAEExECACAF +AkaOQ/ICGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRD3uM6mBW6OVkymAJ0R +6MwiZNRuTAttgYf1Xe7dK7HpzACfZioV/LqnDh7XvcTJEl+r4GB19by5Ag0ERo5D ++xAIAKu1ZxtAZjwlNLb0L5uwrEP7nTbRTNUYoEtE8+CNDSLLlmRIvBriKYNGicnz +Ebq2kDnAoyH38ACIMNayrkqc6I4l3BD2sv7zPZCd4qAbyFCu6gnewTANTWkVuH60 +R65QQ8pM8sM+VZAMSoMkDSP4u248xOzFyGgVYuuWuR/sIRcaA02FW9TGvZQ7fNoF +rf6UbKSYkjpY767IW8q0b68vKzSLw0GQvH+dsvhaj80hjKJ06+IZ9Gdi/b4+AIT2 +YWyWmrHo2QhnUmsarNdtusesQGQtiYgZw95PJJkzR0AttuPPfPNGLYZtVJenvOCC +jsK5uUL3/eEQ3UWGs+BKEyA/qLMAAwUH/2kIFCdgCw2DnL87TO+vruhGjsM7NjXf +57F4ojTdblFd6AerjRhMgICdzCF9WkFROdBSyQ/GajoNU81kbHZglxmKyKkVwWEb +G7pmSIc/sk5Z7OP/zrg4h8ZGzvMbRy0XLf86lQhbDE3AcHMeJCcShIWAHAbygnYW +j0KRhZiyqxqx4mrZQDZEWI7S1G9YNvgu1GS9EEKEpmxDEOME9nJZLi9o7mTeD1QV +TyOzWHkpQ42QcgrFuG7RMxDaQK6bdinNTl8aPmMoPamGzotSt4aMoVMiNxjatnlH +pqQ5UJlqbB5FGLnwJ0773WzgRdxIwSIxkFhL/Mq4agf4an8151kqcZCISQQYEQIA +CQUCRo5D+wIbDAAKCRD3uM6mBW6OVhLmAKCYY152B/10n7aUNKejs92NsNAnPACf +ZwbDOKBXGfkCPuRx5j/AGneASNU= +=Ry+c +-----END PGP PUBLIC KEY BLOCK----- diff --git a/puppet/rabbitmq/templates/rabbitmq-env.conf.erb b/puppet/rabbitmq/templates/rabbitmq-env.conf.erb new file mode 100644 index 000000000..17ca04a02 --- /dev/null +++ b/puppet/rabbitmq/templates/rabbitmq-env.conf.erb @@ -0,0 +1,8 @@ +RABBITMQ_NODE_PORT=<%= port %> +<% if node_ip_address != 'UNSET' -%> +RABBITMQ_NODE_IP_ADDRESS=<%= node_ip_address %> +<% end -%> +RABBITMQ_SERVER_ERL_ARGS="+K true +A30 +P 1048576 \ +-kernel inet_default_connect_options [{nodelay,true}] \ +-kernel inet_dist_listen_min <%= inet_dist_listen_min %> \ +-kernel inet_dist_listen_max <%= inet_dist_listen_max %>" diff --git a/puppet/rabbitmq/templates/rabbitmq.config b/puppet/rabbitmq/templates/rabbitmq.config new file mode 100644 index 000000000..922949f9d --- /dev/null +++ b/puppet/rabbitmq/templates/rabbitmq.config @@ -0,0 +1,12 @@ +% This file managed by Puppet <%= puppetversion %> +% Template Path: <%= module_name %>/templates/rabbitmq.config +[ +<% if config_cluster -%> + {rabbit, [{cluster_nodes, [<%= cluster_disk_nodes.map { |n| "\'rabbit@#{n}\'" }.join(', ') %>]}]} +<% end -%> +<% if config_stomp -%> +% Configure the Stomp Plugin listening port +{rabbitmq_stomp, [{tcp_listeners, [<%= stomp_port %>]} ]} +<% end -%> +]. +% EOF diff --git a/puppet/rabbitmq/tests/full.pp b/puppet/rabbitmq/tests/full.pp new file mode 100644 index 000000000..2f6a66622 --- /dev/null +++ b/puppet/rabbitmq/tests/full.pp @@ -0,0 +1,21 @@ +class { 'rabbitmq::repo::apt': + pin => '900', +}-> +class { 'rabbitmq::server': + delete_guest_user => true, +# version => '2.4.1', +}-> +rabbitmq_user { 'dan': + admin => true, + password => 'pass', + provider => 'rabbitmqctl', +}-> +rabbitmq_vhost { 'myhost': + provider => 'rabbitmqctl', +} +rabbitmq_user_permissions { 'dan@myhost': + configure_permission => '.*', + read_permission => '.*', + write_permission => '.*', + provider => 'rabbitmqctl', +} diff --git a/puppet/rabbitmq/tests/permissions/add.pp b/puppet/rabbitmq/tests/permissions/add.pp new file mode 100644 index 000000000..865347f67 --- /dev/null +++ b/puppet/rabbitmq/tests/permissions/add.pp @@ -0,0 +1,9 @@ +rabbitmq_user { 'blah7': + password => 'foo', +} +rabbitmq_vhost { 'test5': } +rabbitmq_user_permissions { 'blah7@test5': + configure_permission => 'config2', + read_permission => 'ready', + #write_permission => 'ready', +} diff --git a/puppet/rabbitmq/tests/plugin.pp b/puppet/rabbitmq/tests/plugin.pp new file mode 100644 index 000000000..77e07b8fe --- /dev/null +++ b/puppet/rabbitmq/tests/plugin.pp @@ -0,0 +1,11 @@ +class { 'rabbitmq::server': + config_stomp => true, +} + +$rabbitmq_plugins = [ 'amqp_client', 'rabbitmq_stomp' ] + +rabbitmq_plugin { $rabbitmq_plugins: + ensure => present, + require => Class['rabbitmq::server'], + provider => 'rabbitmqplugins', +} diff --git a/puppet/rabbitmq/tests/repo/apt.pp b/puppet/rabbitmq/tests/repo/apt.pp new file mode 100644 index 000000000..f1373737f --- /dev/null +++ b/puppet/rabbitmq/tests/repo/apt.pp @@ -0,0 +1,2 @@ +# requires pupetlabs-apt +include rabbitmq::repo::apt diff --git a/puppet/rabbitmq/tests/server.pp b/puppet/rabbitmq/tests/server.pp new file mode 100644 index 000000000..89ff4655e --- /dev/null +++ b/puppet/rabbitmq/tests/server.pp @@ -0,0 +1,5 @@ +class { 'rabbitmq::server': + port => '5672', + delete_guest_user => true, + version => 'latest', +} diff --git a/puppet/rabbitmq/tests/service.pp b/puppet/rabbitmq/tests/service.pp new file mode 100644 index 000000000..9a00d2b6d --- /dev/null +++ b/puppet/rabbitmq/tests/service.pp @@ -0,0 +1 @@ +class { 'rabbitmq::service': } diff --git a/puppet/rabbitmq/tests/site.pp b/puppet/rabbitmq/tests/site.pp new file mode 100644 index 000000000..75ebcfed3 --- /dev/null +++ b/puppet/rabbitmq/tests/site.pp @@ -0,0 +1,16 @@ +node default { + + $rabbitmq_plugins = [ 'amqp_client', 'rabbitmq_stomp' ] + + class { 'rabbitmq::server': + config => '[ {rabbit_stomp, [{tcp_listeners, [1234]} ]} ].', + } + + # Required for MCollective + rabbitmq_plugin { $rabbitmq_plugins: + ensure => present, + require => Class['rabbitmq::server'], + provider => 'rabbitmqplugins', + } +} + diff --git a/puppet/rabbitmq/tests/user/add.pp b/puppet/rabbitmq/tests/user/add.pp new file mode 100644 index 000000000..3479c84f1 --- /dev/null +++ b/puppet/rabbitmq/tests/user/add.pp @@ -0,0 +1,4 @@ +rabbitmq_user { ['blah2', 'blah3', 'blah4']: + password => 'phoey!', + # provider => 'rabbitmqctl', +} diff --git a/puppet/rabbitmq/tests/vhosts/add.pp b/puppet/rabbitmq/tests/vhosts/add.pp new file mode 100644 index 000000000..d818a1923 --- /dev/null +++ b/puppet/rabbitmq/tests/vhosts/add.pp @@ -0,0 +1 @@ +rabbitmq_vhost { ['fooey', 'blah']: } diff --git a/puppet/stdlib/.gemfile b/puppet/stdlib/.gemfile new file mode 100644 index 000000000..9aad840c0 --- /dev/null +++ b/puppet/stdlib/.gemfile @@ -0,0 +1,5 @@ +source :rubygems + +puppetversion = ENV.key?('PUPPET_VERSION') ? "= #{ENV['PUPPET_VERSION']}" : ['>= 2.7'] +gem 'puppet', puppetversion +gem 'puppetlabs_spec_helper', '>= 0.1.0' diff --git a/puppet/stdlib/.rspec b/puppet/stdlib/.rspec new file mode 100644 index 000000000..7ab5f55c5 --- /dev/null +++ b/puppet/stdlib/.rspec @@ -0,0 +1,4 @@ +--color +--format +progress +--backtrace diff --git a/puppet/stdlib/.travis.yml b/puppet/stdlib/.travis.yml new file mode 100644 index 000000000..0ec5a0873 --- /dev/null +++ b/puppet/stdlib/.travis.yml @@ -0,0 +1,16 @@ +language: ruby +rvm: + - 1.8.7 +before_script: +after_script: +script: "rake spec_full" +branches: + only: + - master +env: + - PUPPET_VERSION=2.7.13 + - PUPPET_VERSION=2.7.6 + - PUPPET_VERSION=2.6.9 +notifications: + email: false +gemfile: .gemfile diff --git a/puppet/stdlib/CHANGELOG b/puppet/stdlib/CHANGELOG new file mode 100644 index 000000000..f1bb6679f --- /dev/null +++ b/puppet/stdlib/CHANGELOG @@ -0,0 +1,117 @@ +2012-08-16 - Jeff McCune - 3.0.1 + * Fix accidental removal of facts_dot_d.rb in 3.0.0 release + +2012-08-16 - Jeff McCune - 3.0.0 + * stdlib 3.0 drops support with Puppet 2.6 + * stdlib 3.0 preserves support with Puppet 2.7 + +2012-08-07 - Dan Bode - 3.0.0 + * Add function ensure_resource and defined_with_params (ba789de) + +2012-07-10 - Hailee Kenney - 3.0.0 + * (#2157) Remove facter_dot_d for compatibility with external facts (f92574f) + +2012-04-10 - Chris Price - 3.0.0 + * (#13693) moving logic from local spec_helper to puppetlabs_spec_helper (85f96df) + +2012-06-07 - Chris Price - 2.4.0 + * Add support for a 'match' parameter to file_line (a06c0d8) + +2012-08-07 - Erik Dalén - 2.4.0 + * (#15872) Add to_bytes function (247b69c) + +2012-07-19 - Jeff McCune - 2.4.0 + * (Maint) use PuppetlabsSpec::PuppetInternals.scope (master) (deafe88) + +2012-07-10 - Hailee Kenney - 2.4.0 + * (#2157) Make facts_dot_d compatible with external facts (5fb0ddc) + +2012-03-16 - Steve Traylen - 2.4.0 + * (#13205) Rotate array/string randomley based on fqdn, fqdn_rotate() (fef247b) + +2012-05-22 - Peter Meier - 2.3.3 + * fix regression in #11017 properly (f0a62c7) + +2012-05-10 - Jeff McCune - 2.3.3 + * Fix spec tests using the new spec_helper (7d34333) + +2012-05-10 - Puppet Labs - 2.3.2 + * Make file_line default to ensure => present (1373e70) + * Memoize file_line spec instance variables (20aacc5) + * Fix spec tests using the new spec_helper (1ebfa5d) + * (#13595) initialize_everything_for_tests couples modules Puppet ver (3222f35) + * (#13439) Fix MRI 1.9 issue with spec_helper (15c5fd1) + * (#13439) Fix test failures with Puppet 2.6.x (665610b) + * (#13439) refactor spec helper for compatibility with both puppet 2.7 and master (82194ca) + * (#13494) Specify the behavior of zero padded strings (61891bb) + +2012-03-29 Puppet Labs - 2.1.3 +* (#11607) Add Rakefile to enable spec testing +* (#12377) Avoid infinite loop when retrying require json + +2012-03-13 Puppet Labs - 2.3.1 +* (#13091) Fix LoadError bug with puppet apply and puppet_vardir fact + +2012-03-12 Puppet Labs - 2.3.0 +* Add a large number of new Puppet functions +* Backwards compatibility preserved with 2.2.x + +2011-12-30 Puppet Labs - 2.2.1 +* Documentation only release for the Forge + +2011-12-30 Puppet Labs - 2.1.2 +* Documentation only release for PE 2.0.x + +2011-11-08 Puppet Labs - 2.2.0 +* #10285 - Refactor json to use pson instead. +* Maint - Add watchr autotest script +* Maint - Make rspec tests work with Puppet 2.6.4 +* #9859 - Add root_home fact and tests + +2011-08-18 Puppet Labs - 2.1.1 +* Change facts.d paths to match Facter 2.0 paths. +* /etc/facter/facts.d +* /etc/puppetlabs/facter/facts.d + +2011-08-17 Puppet Labs - 2.1.0 +* Add R.I. Pienaar's facts.d custom facter fact +* facts defined in /etc/facts.d and /etc/puppetlabs/facts.d are + automatically loaded now. + +2011-08-04 Puppet Labs - 2.0.0 +* Rename whole_line to file_line +* This is an API change and as such motivating a 2.0.0 release according to semver.org. + +2011-08-04 Puppet Labs - 1.1.0 +* Rename append_line to whole_line +* This is an API change and as such motivating a 1.1.0 release. + +2011-08-04 Puppet Labs - 1.0.0 +* Initial stable release +* Add validate_array and validate_string functions +* Make merge() function work with Ruby 1.8.5 +* Add hash merging function +* Add has_key function +* Add loadyaml() function +* Add append_line native + +2011-06-21 Jeff McCune - 0.1.7 +* Add validate_hash() and getvar() functions + +2011-06-15 Jeff McCune - 0.1.6 +* Add anchor resource type to provide containment for composite classes + +2011-06-03 Jeff McCune - 0.1.5 +* Add validate_bool() function to stdlib + +0.1.4 2011-05-26 Jeff McCune +* Move most stages after main + +0.1.3 2011-05-25 Jeff McCune +* Add validate_re() function + +0.1.2 2011-05-24 Jeff McCune +* Update to add annotated tag + +0.1.1 2011-05-24 Jeff McCune +* Add stdlib::stages class with a standard set of stages diff --git a/puppet/stdlib/LICENSE b/puppet/stdlib/LICENSE new file mode 100644 index 000000000..ec0587c0d --- /dev/null +++ b/puppet/stdlib/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2011 Puppet Labs Inc + +and some parts: + +Copyright (C) 2011 Krzysztof Wilczynski + +Puppet Labs can be contacted at: info@puppetlabs.com + +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. diff --git a/puppet/stdlib/Modulefile b/puppet/stdlib/Modulefile new file mode 100644 index 000000000..50c93dae7 --- /dev/null +++ b/puppet/stdlib/Modulefile @@ -0,0 +1,11 @@ +name 'puppetlabs-stdlib' +version '3.0.1' +source 'git://github.com/puppetlabs/puppetlabs-stdlib' +author 'puppetlabs' +license 'Apache 2.0' +summary 'Puppet Module Standard Library' +description 'Standard Library for Puppet Modules' +project_page 'https://github.com/puppetlabs/puppetlabs-stdlib' + +## Add dependencies, if any: +# dependency 'username/name', '>= 1.2.0' diff --git a/puppet/stdlib/README.markdown b/puppet/stdlib/README.markdown new file mode 100644 index 000000000..130753d77 --- /dev/null +++ b/puppet/stdlib/README.markdown @@ -0,0 +1,929 @@ +# Puppet Labs Standard Library # + +This module provides a "standard library" of resources for developing Puppet +Modules. This modules will include the following additions to Puppet + + * Stages + * Facts + * Functions + * Defined resource types + * Types + * Providers + +This module is officially curated and provided by Puppet Labs. The modules +Puppet Labs writes and distributes will make heavy use of this standard +library. + +To report or research a bug with any part of this module, please go to +[http://projects.puppetlabs.com/projects/stdlib](http://projects.puppetlabs.com/projects/stdlib) + +# Versions # + +This module follows semver.org (v1.0.0) versioning guidelines. The standard +library module is released as part of [Puppet +Enterprise](http://puppetlabs.com/puppet/puppet-enterprise/) and as a result +older versions of Puppet Enterprise that Puppet Labs still supports will have +bugfix maintenance branches periodically "merged up" into master. The current +list of integration branches are: + + * v2.1.x (v2.1.1 released in PE 1.2, 1.2.1, 1.2.3, 1.2.4) + * v2.2.x (Never released as part of PE, only to the Forge) + * v2.3.x (Released in PE 2.5.x) + * master (mainline development branch) + +The first Puppet Enterprise version including the stdlib module is Puppet +Enterprise 1.2. + +# Compatibility # + +The stdlib module does not work with Puppet versions released prior to Puppet +2.6.0. + +## stdlib 2.x ## + +All stdlib releases in the 2.0 major version support Puppet 2.6 and Puppet 2.7. + +## stdlib 3.x ## + +The 3.0 major release of stdlib drops support for Puppet 2.6. Stdlib 3.x +supports Puppet 2.7. + +# Functions # + +abs +--- +Returns the absolute value of a number, for example -34.56 becomes 34.56. Takes +a single integer and float value as an argument. + + +- *Type*: rvalue + +bool2num +-------- +Converts a boolean to a number. Converts the values: +false, f, 0, n, and no to 0 +true, t, 1, y, and yes to 1 + Requires a single boolean or string as an input. + + +- *Type*: rvalue + +capitalize +---------- +Capitalizes the first letter of a string or array of strings. +Requires either a single string or an array as an input. + + +- *Type*: rvalue + +chomp +----- +Removes the record separator from the end of a string or an array of strings, +for example `hello\n` becomes `hello`. Requires a single string or array as an +input. + + +- *Type*: rvalue + +chop +---- +Returns a new string with the last character removed. If the string ends +with `\r\n`, both characters are removed. Applying chop to an empty +string returns an empty string. If you wish to merely remove record +separators then you should use the `chomp` function. +Requires a string or array of strings as input. + + +- *Type*: rvalue + +defined_with_params +------------------- +Takes a resource reference and an optional hash of attributes. + +Returns true if a resource with the specified attributes has already been added +to the catalog, and false otherwise. + + user { 'dan': + ensure => present, + } + + if ! defined_with_params(User[dan], {'ensure' => 'present' }) { + user { 'dan': ensure => present, } + } + + +- *Type*: rvalue + +delete +------ +Deletes a selected element from an array. + +*Examples:* + + delete(['a','b','c'], 'b') + +Would return: ['a','c'] + + +- *Type*: rvalue + +delete_at +--------- +Deletes a determined indexed value from an array. + +*Examples:* + + delete_at(['a','b','c'], 1) + +Would return: ['a','c'] + + +- *Type*: rvalue + +downcase +-------- +Converts the case of a string or all strings in an array to lower case. + + +- *Type*: rvalue + +empty +----- +Returns true if the variable is empty. + + +- *Type*: rvalue + +ensure_resource +--------------- +Takes a resource type, title, and a list of attributes that describe a +resource. + + user { 'dan': + ensure => present, + } + +This example only creates the resource if it does not already exist: + + ensure_resource('user, 'dan', {'ensure' => 'present' }) + +If the resource already exists but does not match the specified parameters, +this function will attempt to recreate the resource leading to a duplicate +resource definition error. + + + +- *Type*: statement + +flatten +------- +This function flattens any deeply nested arrays and returns a single flat array +as a result. + +*Examples:* + + flatten(['a', ['b', ['c']]]) + +Would return: ['a','b','c'] + + +- *Type*: rvalue + +fqdn_rotate +----------- +Rotates an array a random number of times based on a nodes fqdn. + + +- *Type*: rvalue + +get_module_path +--------------- +Returns the absolute path of the specified module for the current +environment. + +Example: + $module_path = get_module_path('stdlib') + + +- *Type*: rvalue + +getvar +------ +Lookup a variable in a remote namespace. + +For example: + + $foo = getvar('site::data::foo') + # Equivalent to $foo = $site::data::foo + +This is useful if the namespace itself is stored in a string: + + $datalocation = 'site::data' + $bar = getvar("${datalocation}::bar") + # Equivalent to $bar = $site::data::bar + + +- *Type*: rvalue + +grep +---- +This function searches through an array and returns any elements that match +the provided regular expression. + +*Examples:* + + grep(['aaa','bbb','ccc','aaaddd'], 'aaa') + +Would return: + + ['aaa','aaaddd'] + + +- *Type*: rvalue + +has_key +------- +Determine if a hash has a certain key value. + +Example: + + $my_hash = {'key_one' => 'value_one'} + if has_key($my_hash, 'key_two') { + notice('we will not reach here') + } + if has_key($my_hash, 'key_one') { + notice('this will be printed') + } + + + +- *Type*: rvalue + +hash +---- +This function converts and array into a hash. + +*Examples:* + + hash(['a',1,'b',2,'c',3]) + +Would return: {'a'=>1,'b'=>2,'c'=>3} + + +- *Type*: rvalue + +is_array +-------- +Returns true if the variable passed to this function is an array. + + +- *Type*: rvalue + +is_domain_name +-------------- +Returns true if the string passed to this function is a syntactically correct domain name. + + +- *Type*: rvalue + +is_float +-------- +Returns true if the variable passed to this function is a float. + + +- *Type*: rvalue + +is_hash +------- +Returns true if the variable passed to this function is a hash. + + +- *Type*: rvalue + +is_integer +---------- +Returns true if the variable returned to this string is an integer. + + +- *Type*: rvalue + +is_ip_address +------------- +Returns true if the string passed to this function is a valid IP address. + + +- *Type*: rvalue + +is_mac_address +-------------- +Returns true if the string passed to this function is a valid mac address. + + +- *Type*: rvalue + +is_numeric +---------- +Returns true if the variable passed to this function is a number. + + +- *Type*: rvalue + +is_string +--------- +Returns true if the variable passed to this function is a string. + + +- *Type*: rvalue + +join +---- +This function joins an array into a string using a seperator. + +*Examples:* + + join(['a','b','c'], ",") + +Would result in: "a,b,c" + + +- *Type*: rvalue + +keys +---- +Returns the keys of a hash as an array. + + +- *Type*: rvalue + +loadyaml +-------- +Load a YAML file containing an array, string, or hash, and return the data +in the corresponding native data type. + +For example: + + $myhash = loadyaml('/etc/puppet/data/myhash.yaml') + + +- *Type*: rvalue + +lstrip +------ +Strips leading spaces to the left of a string. + + +- *Type*: rvalue + +member +------ +This function determines if a variable is a member of an array. + +*Examples:* + + member(['a','b'], 'b') + +Would return: true + + member(['a','b'], 'c') + +Would return: false + + +- *Type*: rvalue + +merge +----- +Merges two or more hashes together and returns the resulting hash. + +For example: + + $hash1 = {'one' => 1, 'two', => 2} + $hash2 = {'two' => 'dos', 'three', => 'tres'} + $merged_hash = merge($hash1, $hash2) + # The resulting hash is equivalent to: + # $merged_hash = {'one' => 1, 'two' => 'dos', 'three' => 'tres'} + +When there is a duplicate key, the key in the rightmost hash will "win." + + + +- *Type*: rvalue + +num2bool +-------- +This function converts a number into a true boolean. Zero becomes false. Numbers +higher then 0 become true. + + +- *Type*: rvalue + +parsejson +--------- +This function accepts JSON as a string and converts into the correct Puppet +structure. + + +- *Type*: rvalue + +parseyaml +--------- +This function accepts YAML as a string and converts it into the correct +Puppet structure. + + +- *Type*: rvalue + +prefix +------ +This function applies a prefix to all elements in an array. + +*Examles:* + + prefix(['a','b','c'], 'p') + +Will return: ['pa','pb','pc'] + + +- *Type*: rvalue + +range +----- +When given range in the form of (start, stop) it will extrapolate a range as +an array. + +*Examples:* + + range("0", "9") + +Will return: [0,1,2,3,4,5,6,7,8,9] + + range("00", "09") + +Will return: [0,1,2,3,4,5,6,7,8,9] (Zero padded strings are converted to +integers automatically) + + range("a", "c") + +Will return: ["a","b","c"] + + range("host01", "host10") + +Will return: ["host01", "host02", ..., "host09", "host10"] + + +- *Type*: rvalue + +reverse +------- +Reverses the order of a string or array. + + +- *Type*: rvalue + +rstrip +------ +Strips leading spaces to the right of the string. + + +- *Type*: rvalue + +shuffle +------- +Randomizes the order of a string or array elements. + + +- *Type*: rvalue + +size +---- +Returns the number of elements in a string or array. + + +- *Type*: rvalue + +sort +---- +Sorts strings and arrays lexically. + + +- *Type*: rvalue + +squeeze +------- +Returns a new string where runs of the same character that occur in this set +are replaced by a single character. + + +- *Type*: rvalue + +str2bool +-------- +This converts a string to a boolean. This attempt to convert strings that +contain things like: y, 1, t, true to 'true' and strings that contain things +like: 0, f, n, false, no to 'false'. + + +- *Type*: rvalue + +str2saltedsha512 +---------------- +This converts a string to a salted-SHA512 password hash (which is used for OS X +versions >= 10.7). Given any simple string, you will get a hex version of a +salted-SHA512 password hash that can be inserted into your Puppet manifests as +a valid password attribute. + + +- *Type*: rvalue + +strftime +-------- +This function returns formatted time. + +*Examples:* + +To return the time since epoch: + + strftime("%s") + +To return the date: + + strftime("%Y-%m-%d") + +*Format meaning:* + + %a - The abbreviated weekday name (``Sun'') + %A - The full weekday name (``Sunday'') + %b - The abbreviated month name (``Jan'') + %B - The full month name (``January'') + %c - The preferred local date and time representation + %C - Century (20 in 2009) + %d - Day of the month (01..31) + %D - Date (%m/%d/%y) + %e - Day of the month, blank-padded ( 1..31) + %F - Equivalent to %Y-%m-%d (the ISO 8601 date format) + %h - Equivalent to %b + %H - Hour of the day, 24-hour clock (00..23) + %I - Hour of the day, 12-hour clock (01..12) + %j - Day of the year (001..366) + %k - hour, 24-hour clock, blank-padded ( 0..23) + %l - hour, 12-hour clock, blank-padded ( 0..12) + %L - Millisecond of the second (000..999) + %m - Month of the year (01..12) + %M - Minute of the hour (00..59) + %n - Newline ( +) + %N - Fractional seconds digits, default is 9 digits (nanosecond) + %3N millisecond (3 digits) + %6N microsecond (6 digits) + %9N nanosecond (9 digits) + %p - Meridian indicator (``AM'' or ``PM'') + %P - Meridian indicator (``am'' or ``pm'') + %r - time, 12-hour (same as %I:%M:%S %p) + %R - time, 24-hour (%H:%M) + %s - Number of seconds since 1970-01-01 00:00:00 UTC. + %S - Second of the minute (00..60) + %t - Tab character ( ) + %T - time, 24-hour (%H:%M:%S) + %u - Day of the week as a decimal, Monday being 1. (1..7) + %U - Week number of the current year, + starting with the first Sunday as the first + day of the first week (00..53) + %v - VMS date (%e-%b-%Y) + %V - Week number of year according to ISO 8601 (01..53) + %W - Week number of the current year, + starting with the first Monday as the first + day of the first week (00..53) + %w - Day of the week (Sunday is 0, 0..6) + %x - Preferred representation for the date alone, no time + %X - Preferred representation for the time alone, no date + %y - Year without a century (00..99) + %Y - Year with century + %z - Time zone as hour offset from UTC (e.g. +0900) + %Z - Time zone name + %% - Literal ``%'' character + + +- *Type*: rvalue + +strip +----- +This function removes leading and trailing whitespace from a string or from +every string inside an array. + +*Examples:* + + strip(" aaa ") + +Would result in: "aaa" + + +- *Type*: rvalue + +swapcase +-------- +This function will swap the existing case of a string. + +*Examples:* + + swapcase("aBcD") + +Would result in: "AbCd" + + +- *Type*: rvalue + +time +---- +This function will return the current time since epoch as an integer. + +*Examples:* + + time() + +Will return something like: 1311972653 + + +- *Type*: rvalue + +to_bytes +-------- +Converts the argument into bytes, for example 4 kB becomes 4096. +Takes a single string value as an argument. + + +- *Type*: rvalue + +type +---- +Returns the type when passed a variable. Type can be one of: + +* string +* array +* hash +* float +* integer +* boolean + + +- *Type*: rvalue + +unique +------ +This function will remove duplicates from strings and arrays. + +*Examples:* + + unique("aabbcc") + +Will return: + + abc + +You can also use this with arrays: + + unique(["a","a","b","b","c","c"]) + +This returns: + + ["a","b","c"] + + +- *Type*: rvalue + +upcase +------ +Converts a string or an array of strings to uppercase. + +*Examples:* + + upcase("abcd") + +Will return: + + ABCD + + +- *Type*: rvalue + +validate_absolute_path +---------------------- +Validate the string represents an absolute path in the filesystem. This function works +for windows and unix style paths. + +The following values will pass: + + $my_path = "C:/Program Files (x86)/Puppet Labs/Puppet" + validate_absolute_path($my_path) + $my_path2 = "/var/lib/puppet" + validate_absolute_path($my_path2) + + +The following values will fail, causing compilation to abort: + + validate_absolute_path(true) + validate_absolute_path([ 'var/lib/puppet', '/var/foo' ]) + validate_absolute_path([ '/var/lib/puppet', 'var/foo' ]) + $undefined = undef + validate_absolute_path($undefined) + + + +- *Type*: statement + +validate_array +-------------- +Validate that all passed values are array data structures. Abort catalog +compilation if any value fails this check. + +The following values will pass: + + $my_array = [ 'one', 'two' ] + validate_array($my_array) + +The following values will fail, causing compilation to abort: + + validate_array(true) + validate_array('some_string') + $undefined = undef + validate_array($undefined) + + + +- *Type*: statement + +validate_bool +------------- +Validate that all passed values are either true or false. Abort catalog +compilation if any value fails this check. + +The following values will pass: + + $iamtrue = true + validate_bool(true) + validate_bool(true, true, false, $iamtrue) + +The following values will fail, causing compilation to abort: + + $some_array = [ true ] + validate_bool("false") + validate_bool("true") + validate_bool($some_array) + + + +- *Type*: statement + +validate_hash +------------- +Validate that all passed values are hash data structures. Abort catalog +compilation if any value fails this check. + +The following values will pass: + + $my_hash = { 'one' => 'two' } + validate_hash($my_hash) + +The following values will fail, causing compilation to abort: + + validate_hash(true) + validate_hash('some_string') + $undefined = undef + validate_hash($undefined) + + + +- *Type*: statement + +validate_re +----------- +Perform simple validation of a string against one or more regular +expressions. The first argument of this function should be a string to +test, and the second argument should be a stringified regular expression +(without the // delimiters) or an array of regular expressions. If none +of the regular expressions match the string passed in, compilation will +abort with a parse error. + +If a third argument is specified, this will be the error message raised and +seen by the user. + +The following strings will validate against the regular expressions: + + validate_re('one', '^one$') + validate_re('one', [ '^one', '^two' ]) + +The following strings will fail to validate, causing compilation to abort: + + validate_re('one', [ '^two', '^three' ]) + +A helpful error message can be returned like this: + + validate_re($::puppetversion, '^2.7', 'The $puppetversion fact value does not match 2.7') + + + +- *Type*: statement + +validate_slength +---------------- +Validate that the first argument is a string (or an array of strings), and +less/equal to than the length of the second argument. It fails if the first +argument is not a string or array of strings, and if arg 2 is not convertable +to a number. + +The following values will pass: + + validate_slength("discombobulate",17) + validate_slength(["discombobulate","moo"],17) + +The following valueis will not: + + validate_slength("discombobulate",1) + validate_slength(["discombobulate","thermometer"],5) + + + +- *Type*: statement + +validate_string +--------------- +Validate that all passed values are string data structures. Abort catalog +compilation if any value fails this check. + +The following values will pass: + + $my_string = "one two" + validate_string($my_string, 'three') + +The following values will fail, causing compilation to abort: + + validate_string(true) + validate_string([ 'some', 'array' ]) + $undefined = undef + validate_string($undefined) + + + +- *Type*: statement + +values +------ +When given a hash this function will return the values of that hash. + +*Examples:* + + $hash = { + 'a' => 1, + 'b' => 2, + 'c' => 3, + } + values($hash) + +This example would return: + + [1,2,3] + + +- *Type*: rvalue + +values_at +--------- +Finds value inside an array based on location. + +The first argument is the array you want to analyze, and the second element can +be a combination of: + +* A single numeric index +* A range in the form of 'start-stop' (eg. 4-9) +* An array combining the above + +*Examples*: + + values_at(['a','b','c'], 2) + +Would return ['c']. + + values_at(['a','b','c'], ["0-1"]) + +Would return ['a','b']. + + values_at(['a','b','c','d','e'], [0, "2-3"]) + +Would return ['a','c','d']. + + +- *Type*: rvalue + +zip +--- +Takes one element from first array and merges corresponding elements from second array. This generates a sequence of n-element arrays, where n is one more than the count of arguments. + +*Example:* + + zip(['1','2','3'],['4','5','6']) + +Would result in: + + ["1", "4"], ["2", "5"], ["3", "6"] + + +- *Type*: rvalue diff --git a/puppet/stdlib/README_DEVELOPER.markdown b/puppet/stdlib/README_DEVELOPER.markdown new file mode 100644 index 000000000..e8aa27aa0 --- /dev/null +++ b/puppet/stdlib/README_DEVELOPER.markdown @@ -0,0 +1,35 @@ +Puppet Specific Facts +===================== + +Facter is meant to stand alone and apart from Puppet. However, Facter often +runs inside Puppet and all custom facts included in the stdlib module will +almost always be evaluated in the context of Puppet and Facter working +together. + +Still, we don't want to write custom facts that blow up in the users face if +Puppet is not loaded in memory. This is often the case if the user run +`facter` without also supplying the `--puppet` flag. + +Ah! But Jeff, the custom fact won't be in the `$LOAD_PATH` unless the user +supplies `--facter`! You might say... + +Not (always) true I say! If the user happens to have a CWD of +`/stdlib/lib` then the facts will automatically be evaluated and +blow up. + +In any event, it's pretty easy to write a fact that has no value if Puppet is +not loaded. Simply do it like this: + + Facter.add(:node_vardir) do + setcode do + # This will be nil if Puppet is not available. + Facter::Util::PuppetSettings.with_puppet do + Puppet[:vardir] + end + end + end + +The `Facter::Util::PuppetSettings.with_puppet` method accepts a block and +yields to it only if the Puppet library is loaded. If the Puppet library is +not loaded, then the method silently returns `nil` which Facter interprets as +an undefined fact value. The net effect is that the fact won't be set. diff --git a/puppet/stdlib/RELEASE_PROCESS.markdown b/puppet/stdlib/RELEASE_PROCESS.markdown new file mode 100644 index 000000000..3982c844d --- /dev/null +++ b/puppet/stdlib/RELEASE_PROCESS.markdown @@ -0,0 +1,25 @@ +# Contributing to this module # + + * Work in a topic branch + * Submit a github pull request + * Address any comments / feeback + * Merge into master using --no-ff + +# Releasing this module # + + * This module adheres to http://semver.org/ + * Look for API breaking changes using git diff vX.Y.Z..master + * If no API breaking changes, the minor version may be bumped. + * If there are API breaking changes, the major version must be bumped. + * If there are only small minor changes, the patch version may be bumped. + * Update the CHANGELOG + * Update the Modulefile + * Commit these changes with a message along the lines of "Update CHANGELOG and + Modulefile for release" + * Create an annotated tag with git tag -a vX.Y.Z -m 'version X.Y.Z' (NOTE the + leading v as per semver.org) + * Push the tag with git push origin --tags + * Build a new package with puppet-module or the rake build task if it exists + * Publish the new package to the forge + * Bonus points for an announcement to puppet-users. + diff --git a/puppet/stdlib/Rakefile b/puppet/stdlib/Rakefile new file mode 100644 index 000000000..14f1c2462 --- /dev/null +++ b/puppet/stdlib/Rakefile @@ -0,0 +1,2 @@ +require 'rubygems' +require 'puppetlabs_spec_helper/rake_tasks' diff --git a/puppet/stdlib/lib/facter/puppet_vardir.rb b/puppet/stdlib/lib/facter/puppet_vardir.rb new file mode 100644 index 000000000..0e6af40e4 --- /dev/null +++ b/puppet/stdlib/lib/facter/puppet_vardir.rb @@ -0,0 +1,26 @@ +# This facter fact returns the value of the Puppet vardir setting for the node +# running puppet or puppet agent. The intent is to enable Puppet modules to +# automatically have insight into a place where they can place variable data, +# regardless of the node's platform. +# +# The value should be directly usable in a File resource path attribute. + + +begin + require 'facter/util/puppet_settings' +rescue LoadError => e + # puppet apply does not add module lib directories to the $LOAD_PATH (See + # #4248). It should (in the future) but for the time being we need to be + # defensive which is what this rescue block is doing. + rb_file = File.join(File.dirname(__FILE__), 'util', 'puppet_settings.rb') + load rb_file if File.exists?(rb_file) or raise e +end + +Facter.add(:puppet_vardir) do + setcode do + # This will be nil if Puppet is not available. + Facter::Util::PuppetSettings.with_puppet do + Puppet[:vardir] + end + end +end diff --git a/puppet/stdlib/lib/facter/root_home.rb b/puppet/stdlib/lib/facter/root_home.rb new file mode 100644 index 000000000..8249f7d02 --- /dev/null +++ b/puppet/stdlib/lib/facter/root_home.rb @@ -0,0 +1,19 @@ +# A facter fact to determine the root home directory. +# This varies on PE supported platforms and may be +# reconfigured by the end user. + +module Facter::Util::RootHome + class << self + def get_root_home + root_ent = Facter::Util::Resolution.exec("getent passwd root") + # The home directory is the sixth element in the passwd entry + # If the platform doesn't have getent, root_ent will be nil and we should + # return it straight away. + root_ent && root_ent.split(":")[5] + end + end +end + +Facter.add(:root_home) do + setcode { Facter::Util::RootHome.get_root_home } +end diff --git a/puppet/stdlib/lib/facter/util/puppet_settings.rb b/puppet/stdlib/lib/facter/util/puppet_settings.rb new file mode 100644 index 000000000..1ad945218 --- /dev/null +++ b/puppet/stdlib/lib/facter/util/puppet_settings.rb @@ -0,0 +1,21 @@ +module Facter + module Util + module PuppetSettings + # This method is intended to provide a convenient way to evaluate a + # Facter code block only if Puppet is loaded. This is to account for the + # situation where the fact happens to be in the load path, but Puppet is + # not loaded for whatever reason. Perhaps the user is simply running + # facter without the --puppet flag and they happen to be working in a lib + # directory of a module. + def self.with_puppet + begin + Module.const_get("Puppet") + rescue NameError + nil + else + yield + end + end + end + end +end diff --git a/puppet/stdlib/lib/puppet/parser/functions/abs.rb b/puppet/stdlib/lib/puppet/parser/functions/abs.rb new file mode 100644 index 000000000..ade546278 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/abs.rb @@ -0,0 +1,36 @@ +# +# abs.rb +# + +module Puppet::Parser::Functions + newfunction(:abs, :type => :rvalue, :doc => <<-EOS + Returns the absolute value of a number, for example -34.56 becomes + 34.56. Takes a single integer and float value as an argument. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "abs(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + value = arguments[0] + + # Numbers in Puppet are often string-encoded which is troublesome ... + if value.is_a?(String) + if value.match(/^-?(?:\d+)(?:\.\d+){1}$/) + value = value.to_f + elsif value.match(/^-?\d+$/) + value = value.to_i + else + raise(Puppet::ParseError, 'abs(): Requires float or ' + + 'integer to work with') + end + end + + # We have numeric value to handle ... + result = value.abs + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/bool2num.rb b/puppet/stdlib/lib/puppet/parser/functions/bool2num.rb new file mode 100644 index 000000000..9a07a8a11 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/bool2num.rb @@ -0,0 +1,49 @@ +# +# bool2num.rb +# + +module Puppet::Parser::Functions + newfunction(:bool2num, :type => :rvalue, :doc => <<-EOS + Converts a boolean to a number. Converts the values: + false, f, 0, n, and no to 0 + true, t, 1, y, and yes to 1 + Requires a single boolean or string as an input. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "bool2num(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + value = arguments[0] + klass = value.class + + # We can have either true or false, or string which resembles boolean ... + unless [FalseClass, TrueClass, String].include?(klass) + raise(Puppet::ParseError, 'bool2num(): Requires either ' + + 'boolean or string to work with') + end + + if value.is_a?(String) + # We consider all the yes, no, y, n and so on too ... + value = case value + # + # This is how undef looks like in Puppet ... + # We yield 0 (or false if you wish) in this case. + # + when /^$/, '' then false # Empty string will be false ... + when /^(1|t|y|true|yes)$/ then true + when /^(0|f|n|false|no)$/ then false + when /^(undef|undefined)$/ then false # This is not likely to happen ... + else + raise(Puppet::ParseError, 'bool2num(): Unknown type of boolean given') + end + end + + # We have real boolean values as well ... + result = value ? 1 : 0 + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/capitalize.rb b/puppet/stdlib/lib/puppet/parser/functions/capitalize.rb new file mode 100644 index 000000000..640d00b82 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/capitalize.rb @@ -0,0 +1,34 @@ +# +# capitalize.rb +# + +module Puppet::Parser::Functions + newfunction(:capitalize, :type => :rvalue, :doc => <<-EOS + Capitalizes the first letter of a string or array of strings. + Requires either a single string or an array as an input. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "capitalize(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + value = arguments[0] + klass = value.class + + unless [Array, String].include?(klass) + raise(Puppet::ParseError, 'capitalize(): Requires either ' + + 'array or string to work with') + end + + if value.is_a?(Array) + # Numbers in Puppet are often string-encoded which is troublesome ... + result = value.collect { |i| i.is_a?(String) ? i.capitalize : i } + else + result = value.capitalize + end + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/chomp.rb b/puppet/stdlib/lib/puppet/parser/functions/chomp.rb new file mode 100644 index 000000000..c99d139da --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/chomp.rb @@ -0,0 +1,35 @@ +# +# chomp.rb +# + +module Puppet::Parser::Functions + newfunction(:chomp, :type => :rvalue, :doc => <<-'EOS' + Removes the record separator from the end of a string or an array of + strings, for example `hello\n` becomes `hello`. + Requires a single string or array as an input. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "chomp(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + value = arguments[0] + klass = value.class + + unless [Array, String].include?(klass) + raise(Puppet::ParseError, 'chomp(): Requires either ' + + 'array or string to work with') + end + + if value.is_a?(Array) + # Numbers in Puppet are often string-encoded which is troublesome ... + result = value.collect { |i| i.is_a?(String) ? i.chomp : i } + else + result = value.chomp + end + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/chop.rb b/puppet/stdlib/lib/puppet/parser/functions/chop.rb new file mode 100644 index 000000000..636b990d2 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/chop.rb @@ -0,0 +1,37 @@ +# +# chop.rb +# + +module Puppet::Parser::Functions + newfunction(:chop, :type => :rvalue, :doc => <<-'EOS' + Returns a new string with the last character removed. If the string ends + with `\r\n`, both characters are removed. Applying chop to an empty + string returns an empty string. If you wish to merely remove record + separators then you should use the `chomp` function. + Requires a string or array of strings as input. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "chop(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + value = arguments[0] + klass = value.class + + unless [Array, String].include?(klass) + raise(Puppet::ParseError, 'chop(): Requires either an ' + + 'array or string to work with') + end + + if value.is_a?(Array) + # Numbers in Puppet are often string-encoded which is troublesome ... + result = value.collect { |i| i.is_a?(String) ? i.chop : i } + else + result = value.chop + end + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/defined_with_params.rb b/puppet/stdlib/lib/puppet/parser/functions/defined_with_params.rb new file mode 100644 index 000000000..d7df306c7 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/defined_with_params.rb @@ -0,0 +1,35 @@ +# Test whether a given class or definition is defined +require 'puppet/parser/functions' + +Puppet::Parser::Functions.newfunction(:defined_with_params, + :type => :rvalue, + :doc => <<-'ENDOFDOC' +Takes a resource reference and an optional hash of attributes. + +Returns true if a resource with the specified attributes has already been added +to the catalog, and false otherwise. + + user { 'dan': + ensure => present, + } + + if ! defined_with_params(User[dan], {'ensure' => 'present' }) { + user { 'dan': ensure => present, } + } +ENDOFDOC +) do |vals| + reference, params = vals + raise(ArgumentError, 'Must specify a reference') unless reference + if (! params) || params == '' + params = {} + end + ret = false + if resource = findresource(reference.to_s) + matches = params.collect do |key, value| + resource[key] == value + end + ret = params.empty? || !matches.include?(false) + end + Puppet.debug("Resource #{reference} was not determined to be defined") + ret +end diff --git a/puppet/stdlib/lib/puppet/parser/functions/delete.rb b/puppet/stdlib/lib/puppet/parser/functions/delete.rb new file mode 100644 index 000000000..ab8f75bdb --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/delete.rb @@ -0,0 +1,34 @@ +# +# delete.rb +# + +# TODO(Krzysztof Wilczynski): We need to add support for regular expression ... +# TODO(Krzysztof Wilczynski): Support for strings and hashes too ... + +module Puppet::Parser::Functions + newfunction(:delete, :type => :rvalue, :doc => <<-EOS +Deletes a selected element from an array. + +*Examples:* + + delete(['a','b','c'], 'b') + +Would return: ['a','c'] + EOS + ) do |arguments| + + if (arguments.size != 2) then + raise(Puppet::ParseError, "delete(): Wrong number of arguments "+ + "given #{arguments.size} for 2") + end + + a = arguments[0] + item = arguments[1] + + a.delete(item) + a + + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/delete_at.rb b/puppet/stdlib/lib/puppet/parser/functions/delete_at.rb new file mode 100644 index 000000000..3eb4b5375 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/delete_at.rb @@ -0,0 +1,49 @@ +# +# delete_at.rb +# + +module Puppet::Parser::Functions + newfunction(:delete_at, :type => :rvalue, :doc => <<-EOS +Deletes a determined indexed value from an array. + +*Examples:* + + delete_at(['a','b','c'], 1) + +Would return: ['a','c'] + EOS + ) do |arguments| + + raise(Puppet::ParseError, "delete_at(): Wrong number of arguments " + + "given (#{arguments.size} for 2)") if arguments.size < 2 + + array = arguments[0] + + unless array.is_a?(Array) + raise(Puppet::ParseError, 'delete_at(): Requires array to work with') + end + + index = arguments[1] + + if index.is_a?(String) and not index.match(/^\d+$/) + raise(Puppet::ParseError, 'delete_at(): You must provide ' + + 'non-negative numeric index') + end + + result = array.clone + + # Numbers in Puppet are often string-encoded which is troublesome ... + index = index.to_i + + if index > result.size - 1 # First element is at index 0 is it not? + raise(Puppet::ParseError, 'delete_at(): Given index ' + + 'exceeds size of array given') + end + + result.delete_at(index) # We ignore the element that got deleted ... + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/downcase.rb b/puppet/stdlib/lib/puppet/parser/functions/downcase.rb new file mode 100644 index 000000000..4066d210f --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/downcase.rb @@ -0,0 +1,33 @@ +# +# downcase.rb +# + +module Puppet::Parser::Functions + newfunction(:downcase, :type => :rvalue, :doc => <<-EOS +Converts the case of a string or all strings in an array to lower case. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "downcase(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + value = arguments[0] + klass = value.class + + unless [Array, String].include?(klass) + raise(Puppet::ParseError, 'downcase(): Requires either ' + + 'array or string to work with') + end + + if value.is_a?(Array) + # Numbers in Puppet are often string-encoded which is troublesome ... + result = value.collect { |i| i.is_a?(String) ? i.downcase : i } + else + result = value.downcase + end + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/empty.rb b/puppet/stdlib/lib/puppet/parser/functions/empty.rb new file mode 100644 index 000000000..80ebb86b8 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/empty.rb @@ -0,0 +1,28 @@ +# +# empty.rb +# + +module Puppet::Parser::Functions + newfunction(:empty, :type => :rvalue, :doc => <<-EOS +Returns true if the variable is empty. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "empty(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + value = arguments[0] + klass = value.class + + unless [Array, Hash, String].include?(klass) + raise(Puppet::ParseError, 'empty(): Requires either ' + + 'array, hash or string to work with') + end + + result = value.empty? + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/ensure_resource.rb b/puppet/stdlib/lib/puppet/parser/functions/ensure_resource.rb new file mode 100644 index 000000000..fba203561 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/ensure_resource.rb @@ -0,0 +1,35 @@ +# Test whether a given class or definition is defined +require 'puppet/parser/functions' + +Puppet::Parser::Functions.newfunction(:ensure_resource, + :type => :statement, + :doc => <<-'ENDOFDOC' +Takes a resource type, title, and a list of attributes that describe a +resource. + + user { 'dan': + ensure => present, + } + +This example only creates the resource if it does not already exist: + + ensure_resource('user, 'dan', {'ensure' => 'present' }) + +If the resource already exists but does not match the specified parameters, +this function will attempt to recreate the resource leading to a duplicate +resource definition error. + +ENDOFDOC +) do |vals| + type, title, params = vals + raise(ArgumentError, 'Must specify a type') unless type + raise(ArgumentError, 'Must specify a title') unless title + params ||= {} + Puppet::Parser::Functions.function(:defined_with_params) + if function_defined_with_params(["#{type}[#{title}]", params]) + Puppet.debug("Resource #{type}[#{title}] not created b/c it already exists") + else + Puppet::Parser::Functions.function(:create_resources) + function_create_resources([type.capitalize, { title => params }]) + end +end diff --git a/puppet/stdlib/lib/puppet/parser/functions/flatten.rb b/puppet/stdlib/lib/puppet/parser/functions/flatten.rb new file mode 100644 index 000000000..781da7862 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/flatten.rb @@ -0,0 +1,33 @@ +# +# flatten.rb +# + +module Puppet::Parser::Functions + newfunction(:flatten, :type => :rvalue, :doc => <<-EOS +This function flattens any deeply nested arrays and returns a single flat array +as a result. + +*Examples:* + + flatten(['a', ['b', ['c']]]) + +Would return: ['a','b','c'] + EOS + ) do |arguments| + + raise(Puppet::ParseError, "flatten(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + array = arguments[0] + + unless array.is_a?(Array) + raise(Puppet::ParseError, 'flatten(): Requires array to work with') + end + + result = array.flatten + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/fqdn_rotate.rb b/puppet/stdlib/lib/puppet/parser/functions/fqdn_rotate.rb new file mode 100644 index 000000000..655820605 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/fqdn_rotate.rb @@ -0,0 +1,46 @@ +# +# fqdn_rotate.rb +# + +module Puppet::Parser::Functions + newfunction(:fqdn_rotate, :type => :rvalue, :doc => <<-EOS +Rotates an array a random number of times based on a nodes fqdn. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "fqdn_rotate(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + value = arguments[0] + klass = value.class + require 'digest/md5' + + unless [Array, String].include?(klass) + raise(Puppet::ParseError, 'fqdn_rotate(): Requires either ' + + 'array or string to work with') + end + + result = value.clone + + string = value.is_a?(String) ? true : false + + # Check whether it makes sense to rotate ... + return result if result.size <= 1 + + # We turn any string value into an array to be able to rotate ... + result = string ? result.split('') : result + + elements = result.size + + srand(Digest::MD5.hexdigest([lookupvar('::fqdn'),arguments].join(':')).hex) + rand(elements).times { + result.push result.shift + } + + result = string ? result.join : result + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/get_module_path.rb b/puppet/stdlib/lib/puppet/parser/functions/get_module_path.rb new file mode 100644 index 000000000..1421b91f5 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/get_module_path.rb @@ -0,0 +1,17 @@ +module Puppet::Parser::Functions + newfunction(:get_module_path, :type =>:rvalue, :doc => <<-EOT + Returns the absolute path of the specified module for the current + environment. + + Example: + $module_path = get_module_path('stdlib') + EOT + ) do |args| + raise(Puppet::ParseError, "get_module_path(): Wrong number of arguments, expects one") unless args.size == 1 + if module_path = Puppet::Module.find(args[0], compiler.environment.to_s) + module_path.path + else + raise(Puppet::ParseError, "Could not find module #{args[0]} in environment #{compiler.environment}") + end + end +end diff --git a/puppet/stdlib/lib/puppet/parser/functions/getvar.rb b/puppet/stdlib/lib/puppet/parser/functions/getvar.rb new file mode 100644 index 000000000..162114995 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/getvar.rb @@ -0,0 +1,26 @@ +module Puppet::Parser::Functions + + newfunction(:getvar, :type => :rvalue, :doc => <<-'ENDHEREDOC') do |args| + Lookup a variable in a remote namespace. + + For example: + + $foo = getvar('site::data::foo') + # Equivalent to $foo = $site::data::foo + + This is useful if the namespace itself is stored in a string: + + $datalocation = 'site::data' + $bar = getvar("${datalocation}::bar") + # Equivalent to $bar = $site::data::bar + ENDHEREDOC + + unless args.length == 1 + raise Puppet::ParseError, ("getvar(): wrong number of arguments (#{args.length}; must be 1)") + end + + self.lookupvar("#{args[0]}") + + end + +end diff --git a/puppet/stdlib/lib/puppet/parser/functions/grep.rb b/puppet/stdlib/lib/puppet/parser/functions/grep.rb new file mode 100644 index 000000000..ceba9ecc8 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/grep.rb @@ -0,0 +1,33 @@ +# +# grep.rb +# + +module Puppet::Parser::Functions + newfunction(:grep, :type => :rvalue, :doc => <<-EOS +This function searches through an array and returns any elements that match +the provided regular expression. + +*Examples:* + + grep(['aaa','bbb','ccc','aaaddd'], 'aaa') + +Would return: + + ['aaa','aaaddd'] + EOS + ) do |arguments| + + if (arguments.size != 2) then + raise(Puppet::ParseError, "grep(): Wrong number of arguments "+ + "given #{arguments.size} for 2") + end + + a = arguments[0] + pattern = Regexp.new(arguments[1]) + + a.grep(pattern) + + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/has_key.rb b/puppet/stdlib/lib/puppet/parser/functions/has_key.rb new file mode 100644 index 000000000..4657cc29c --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/has_key.rb @@ -0,0 +1,28 @@ +module Puppet::Parser::Functions + + newfunction(:has_key, :type => :rvalue, :doc => <<-'ENDHEREDOC') do |args| + Determine if a hash has a certain key value. + + Example: + + $my_hash = {'key_one' => 'value_one'} + if has_key($my_hash, 'key_two') { + notice('we will not reach here') + } + if has_key($my_hash, 'key_one') { + notice('this will be printed') + } + + ENDHEREDOC + + unless args.length == 2 + raise Puppet::ParseError, ("has_key(): wrong number of arguments (#{args.length}; must be 2)") + end + unless args[0].is_a?(Hash) + raise Puppet::ParseError, "has_key(): expects the first argument to be a hash, got #{args[0].inspect} which is of type #{args[0].class}" + end + args[0].has_key?(args[1]) + + end + +end diff --git a/puppet/stdlib/lib/puppet/parser/functions/hash.rb b/puppet/stdlib/lib/puppet/parser/functions/hash.rb new file mode 100644 index 000000000..453ba1eab --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/hash.rb @@ -0,0 +1,41 @@ +# +# hash.rb +# + +module Puppet::Parser::Functions + newfunction(:hash, :type => :rvalue, :doc => <<-EOS +This function converts and array into a hash. + +*Examples:* + + hash(['a',1,'b',2,'c',3]) + +Would return: {'a'=>1,'b'=>2,'c'=>3} + EOS + ) do |arguments| + + raise(Puppet::ParseError, "hash(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + array = arguments[0] + + unless array.is_a?(Array) + raise(Puppet::ParseError, 'hash(): Requires array to work with') + end + + result = {} + + begin + # This is to make it compatible with older version of Ruby ... + array = array.flatten + result = Hash[*array] + rescue Exception + raise(Puppet::ParseError, 'hash(): Unable to compute ' + + 'hash from array given') + end + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/is_array.rb b/puppet/stdlib/lib/puppet/parser/functions/is_array.rb new file mode 100644 index 000000000..b39e184ae --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/is_array.rb @@ -0,0 +1,22 @@ +# +# is_array.rb +# + +module Puppet::Parser::Functions + newfunction(:is_array, :type => :rvalue, :doc => <<-EOS +Returns true if the variable passed to this function is an array. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "is_array(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + type = arguments[0] + + result = type.is_a?(Array) + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/is_domain_name.rb b/puppet/stdlib/lib/puppet/parser/functions/is_domain_name.rb new file mode 100644 index 000000000..5826dc0d9 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/is_domain_name.rb @@ -0,0 +1,47 @@ +# +# is_domain_name.rb +# + +module Puppet::Parser::Functions + newfunction(:is_domain_name, :type => :rvalue, :doc => <<-EOS +Returns true if the string passed to this function is a syntactically correct domain name. + EOS + ) do |arguments| + + if (arguments.size != 1) then + raise(Puppet::ParseError, "is_domain_name(): Wrong number of arguments "+ + "given #{arguments.size} for 1") + end + + domain = arguments[0] + + # Limits (rfc1035, 3.1) + domain_max_length=255 + label_min_length=1 + label_max_length=63 + + # Allow ".", it is the top level domain + return true if domain == '.' + + # Remove the final dot, if present. + domain.chomp!('.') + + # Check the whole domain + return false if domain.empty? + return false if domain.length > domain_max_length + + # Check each label in the domain + labels = domain.split('.') + vlabels = labels.each do |label| + break if label.length < label_min_length + break if label.length > label_max_length + break if label[-1..-1] == '-' + break if label[0..0] == '-' + break unless /^[a-z\d-]+$/i.match(label) + end + return vlabels == labels + + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/is_float.rb b/puppet/stdlib/lib/puppet/parser/functions/is_float.rb new file mode 100644 index 000000000..2fc05ba9c --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/is_float.rb @@ -0,0 +1,27 @@ +# +# is_float.rb +# + +module Puppet::Parser::Functions + newfunction(:is_float, :type => :rvalue, :doc => <<-EOS +Returns true if the variable passed to this function is a float. + EOS + ) do |arguments| + + if (arguments.size != 1) then + raise(Puppet::ParseError, "is_float(): Wrong number of arguments "+ + "given #{arguments.size} for 1") + end + + value = arguments[0] + + if value != value.to_f.to_s then + return false + else + return true + end + + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/is_hash.rb b/puppet/stdlib/lib/puppet/parser/functions/is_hash.rb new file mode 100644 index 000000000..ad907f086 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/is_hash.rb @@ -0,0 +1,22 @@ +# +# is_hash.rb +# + +module Puppet::Parser::Functions + newfunction(:is_hash, :type => :rvalue, :doc => <<-EOS +Returns true if the variable passed to this function is a hash. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "is_hash(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size != 1 + + type = arguments[0] + + result = type.is_a?(Hash) + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/is_integer.rb b/puppet/stdlib/lib/puppet/parser/functions/is_integer.rb new file mode 100644 index 000000000..8ee34f699 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/is_integer.rb @@ -0,0 +1,27 @@ +# +# is_integer.rb +# + +module Puppet::Parser::Functions + newfunction(:is_integer, :type => :rvalue, :doc => <<-EOS +Returns true if the variable returned to this string is an integer. + EOS + ) do |arguments| + + if (arguments.size != 1) then + raise(Puppet::ParseError, "is_integer(): Wrong number of arguments "+ + "given #{arguments.size} for 1") + end + + value = arguments[0] + + if value != value.to_i.to_s then + return false + else + return true + end + + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/is_ip_address.rb b/puppet/stdlib/lib/puppet/parser/functions/is_ip_address.rb new file mode 100644 index 000000000..b4a9a15dd --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/is_ip_address.rb @@ -0,0 +1,32 @@ +# +# is_ip_address.rb +# + +module Puppet::Parser::Functions + newfunction(:is_ip_address, :type => :rvalue, :doc => <<-EOS +Returns true if the string passed to this function is a valid IP address. + EOS + ) do |arguments| + + require 'ipaddr' + + if (arguments.size != 1) then + raise(Puppet::ParseError, "is_ip_address(): Wrong number of arguments "+ + "given #{arguments.size} for 1") + end + + begin + ip = IPAddr.new(arguments[0]) + rescue ArgumentError + return false + end + + if ip.ipv4? or ip.ipv6? then + return true + else + return false + end + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/is_mac_address.rb b/puppet/stdlib/lib/puppet/parser/functions/is_mac_address.rb new file mode 100644 index 000000000..1b3088a26 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/is_mac_address.rb @@ -0,0 +1,27 @@ +# +# is_mac_address.rb +# + +module Puppet::Parser::Functions + newfunction(:is_mac_address, :type => :rvalue, :doc => <<-EOS +Returns true if the string passed to this function is a valid mac address. + EOS + ) do |arguments| + + if (arguments.size != 1) then + raise(Puppet::ParseError, "is_mac_address(): Wrong number of arguments "+ + "given #{arguments.size} for 1") + end + + mac = arguments[0] + + if /^[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}$/.match(mac) then + return true + else + return false + end + + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/is_numeric.rb b/puppet/stdlib/lib/puppet/parser/functions/is_numeric.rb new file mode 100644 index 000000000..ce13eceaa --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/is_numeric.rb @@ -0,0 +1,27 @@ +# +# is_numeric.rb +# + +module Puppet::Parser::Functions + newfunction(:is_numeric, :type => :rvalue, :doc => <<-EOS +Returns true if the variable passed to this function is a number. + EOS + ) do |arguments| + + if (arguments.size != 1) then + raise(Puppet::ParseError, "is_numeric(): Wrong number of arguments "+ + "given #{arguments.size} for 1") + end + + value = arguments[0] + + if value == value.to_f.to_s or value == value.to_i.to_s then + return true + else + return false + end + + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/is_string.rb b/puppet/stdlib/lib/puppet/parser/functions/is_string.rb new file mode 100644 index 000000000..f5bef0457 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/is_string.rb @@ -0,0 +1,26 @@ +# +# is_string.rb +# + +module Puppet::Parser::Functions + newfunction(:is_string, :type => :rvalue, :doc => <<-EOS +Returns true if the variable passed to this function is a string. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "is_string(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + type = arguments[0] + + result = type.is_a?(String) + + if result and (type == type.to_f.to_s or type == type.to_i.to_s) then + return false + end + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/join.rb b/puppet/stdlib/lib/puppet/parser/functions/join.rb new file mode 100644 index 000000000..005a46ea5 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/join.rb @@ -0,0 +1,41 @@ +# +# join.rb +# + +module Puppet::Parser::Functions + newfunction(:join, :type => :rvalue, :doc => <<-EOS +This function joins an array into a string using a seperator. + +*Examples:* + + join(['a','b','c'], ",") + +Would result in: "a,b,c" + EOS + ) do |arguments| + + # Technically we support two arguments but only first is mandatory ... + raise(Puppet::ParseError, "join(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + array = arguments[0] + + unless array.is_a?(Array) + raise(Puppet::ParseError, 'join(): Requires array to work with') + end + + suffix = arguments[1] if arguments[1] + + if suffix + unless suffix.is_a?(String) + raise(Puppet::ParseError, 'join(): Requires string to work with') + end + end + + result = suffix ? array.join(suffix) : array.join + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/keys.rb b/puppet/stdlib/lib/puppet/parser/functions/keys.rb new file mode 100644 index 000000000..f0d13b647 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/keys.rb @@ -0,0 +1,26 @@ +# +# keys.rb +# + +module Puppet::Parser::Functions + newfunction(:keys, :type => :rvalue, :doc => <<-EOS +Returns the keys of a hash as an array. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "keys(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + hash = arguments[0] + + unless hash.is_a?(Hash) + raise(Puppet::ParseError, 'keys(): Requires hash to work with') + end + + result = hash.keys + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/loadyaml.rb b/puppet/stdlib/lib/puppet/parser/functions/loadyaml.rb new file mode 100644 index 000000000..10c400501 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/loadyaml.rb @@ -0,0 +1,20 @@ +module Puppet::Parser::Functions + + newfunction(:loadyaml, :type => :rvalue, :doc => <<-'ENDHEREDOC') do |args| + Load a YAML file containing an array, string, or hash, and return the data + in the corresponding native data type. + + For example: + + $myhash = loadyaml('/etc/puppet/data/myhash.yaml') + ENDHEREDOC + + unless args.length == 1 + raise Puppet::ParseError, ("loadyaml(): wrong number of arguments (#{args.length}; must be 1)") + end + + YAML.load_file(args[0]) + + end + +end diff --git a/puppet/stdlib/lib/puppet/parser/functions/lstrip.rb b/puppet/stdlib/lib/puppet/parser/functions/lstrip.rb new file mode 100644 index 000000000..3a64de337 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/lstrip.rb @@ -0,0 +1,33 @@ +# +# lstrip.rb +# + +module Puppet::Parser::Functions + newfunction(:lstrip, :type => :rvalue, :doc => <<-EOS +Strips leading spaces to the left of a string. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "lstrip(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + value = arguments[0] + klass = value.class + + unless [Array, String].include?(klass) + raise(Puppet::ParseError, 'lstrip(): Requires either ' + + 'array or string to work with') + end + + if value.is_a?(Array) + # Numbers in Puppet are often string-encoded which is troublesome ... + result = value.collect { |i| i.is_a?(String) ? i.lstrip : i } + else + result = value.lstrip + end + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/member.rb b/puppet/stdlib/lib/puppet/parser/functions/member.rb new file mode 100644 index 000000000..43d76affd --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/member.rb @@ -0,0 +1,44 @@ +# +# member.rb +# + +# TODO(Krzysztof Wilczynski): We need to add support for regular expression ... +# TODO(Krzysztof Wilczynski): Support for strings and hashes too ... + +module Puppet::Parser::Functions + newfunction(:member, :type => :rvalue, :doc => <<-EOS +This function determines if a variable is a member of an array. + +*Examples:* + + member(['a','b'], 'b') + +Would return: true + + member(['a','b'], 'c') + +Would return: false + EOS + ) do |arguments| + + raise(Puppet::ParseError, "member(): Wrong number of arguments " + + "given (#{arguments.size} for 2)") if arguments.size < 2 + + array = arguments[0] + + unless array.is_a?(Array) + raise(Puppet::ParseError, 'member(): Requires array to work with') + end + + item = arguments[1] + + raise(Puppet::ParseError, 'member(): You must provide item ' + + 'to search for within array given') if item.empty? + + result = array.include?(item) + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/merge.rb b/puppet/stdlib/lib/puppet/parser/functions/merge.rb new file mode 100644 index 000000000..6ec085eb1 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/merge.rb @@ -0,0 +1,33 @@ +module Puppet::Parser::Functions + newfunction(:merge, :type => :rvalue, :doc => <<-'ENDHEREDOC') do |args| + Merges two or more hashes together and returns the resulting hash. + + For example: + + $hash1 = {'one' => 1, 'two', => 2} + $hash2 = {'two' => 'dos', 'three', => 'tres'} + $merged_hash = merge($hash1, $hash2) + # The resulting hash is equivalent to: + # $merged_hash = {'one' => 1, 'two' => 'dos', 'three' => 'tres'} + + When there is a duplicate key, the key in the rightmost hash will "win." + + ENDHEREDOC + + if args.length < 2 + raise Puppet::ParseError, ("merge(): wrong number of arguments (#{args.length}; must be at least 2)") + end + + # The hash we accumulate into + accumulator = Hash.new + # Merge into the accumulator hash + args.each do |arg| + unless arg.is_a?(Hash) + raise Puppet::ParseError, "merge: unexpected argument type #{arg.class}, only expects hash arguments" + end + accumulator.merge!(arg) + end + # Return the fully merged hash + accumulator + end +end diff --git a/puppet/stdlib/lib/puppet/parser/functions/num2bool.rb b/puppet/stdlib/lib/puppet/parser/functions/num2bool.rb new file mode 100644 index 000000000..874db226e --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/num2bool.rb @@ -0,0 +1,40 @@ +# +# num2bool.rb +# + +# TODO(Krzysztof Wilczynski): We probably need to approach numeric values differently ... + +module Puppet::Parser::Functions + newfunction(:num2bool, :type => :rvalue, :doc => <<-EOS +This function converts a number into a true boolean. Zero becomes false. Numbers +higher then 0 become true. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "num2bool(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + number = arguments[0] + + # Only numbers allowed ... + unless number.match(/^\-?\d+$/) + raise(Puppet::ParseError, 'num2bool(): Requires integer to work with') + end + + result = case number + when /^0$/ + false + when /^\-?\d+$/ + # Numbers in Puppet are often string-encoded which is troublesome ... + number = number.to_i + # We yield true for any positive number and false otherwise ... + number > 0 ? true : false + else + raise(Puppet::ParseError, 'num2bool(): Unknown numeric format given') + end + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/parsejson.rb b/puppet/stdlib/lib/puppet/parser/functions/parsejson.rb new file mode 100644 index 000000000..a9a16a452 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/parsejson.rb @@ -0,0 +1,24 @@ +# +# parsejson.rb +# + +module Puppet::Parser::Functions + newfunction(:parsejson, :type => :rvalue, :doc => <<-EOS +This function accepts JSON as a string and converts into the correct Puppet +structure. + EOS + ) do |arguments| + + if (arguments.size != 1) then + raise(Puppet::ParseError, "parsejson(): Wrong number of arguments "+ + "given #{arguments.size} for 1") + end + + json = arguments[0] + + # PSON is natively available in puppet + PSON.load(json) + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/parseyaml.rb b/puppet/stdlib/lib/puppet/parser/functions/parseyaml.rb new file mode 100644 index 000000000..e8ac8a4cb --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/parseyaml.rb @@ -0,0 +1,24 @@ +# +# parseyaml.rb +# + +module Puppet::Parser::Functions + newfunction(:parseyaml, :type => :rvalue, :doc => <<-EOS +This function accepts YAML as a string and converts it into the correct +Puppet structure. + EOS + ) do |arguments| + + if (arguments.size != 1) then + raise(Puppet::ParseError, "parseyaml(): Wrong number of arguments "+ + "given #{arguments.size} for 1") + end + + require 'yaml' + + YAML::load(arguments[0]) + + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/prefix.rb b/puppet/stdlib/lib/puppet/parser/functions/prefix.rb new file mode 100644 index 000000000..459397667 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/prefix.rb @@ -0,0 +1,45 @@ +# +# prefix.rb +# + +module Puppet::Parser::Functions + newfunction(:prefix, :type => :rvalue, :doc => <<-EOS +This function applies a prefix to all elements in an array. + +*Examles:* + + prefix(['a','b','c'], 'p') + +Will return: ['pa','pb','pc'] + EOS + ) do |arguments| + + # Technically we support two arguments but only first is mandatory ... + raise(Puppet::ParseError, "prefix(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + array = arguments[0] + + unless array.is_a?(Array) + raise(Puppet::ParseError, 'prefix(): Requires array to work with') + end + + prefix = arguments[1] if arguments[1] + + if prefix + unless prefix.is_a?(String) + raise(Puppet::ParseError, 'prefix(): Requires string to work with') + end + end + + # Turn everything into string same as join would do ... + result = array.collect do |i| + i = i.to_s + prefix ? prefix + i : i + end + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/range.rb b/puppet/stdlib/lib/puppet/parser/functions/range.rb new file mode 100644 index 000000000..825617b38 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/range.rb @@ -0,0 +1,80 @@ +# +# range.rb +# + +# TODO(Krzysztof Wilczynski): We probably need to approach numeric values differently ... + +module Puppet::Parser::Functions + newfunction(:range, :type => :rvalue, :doc => <<-EOS +When given range in the form of (start, stop) it will extrapolate a range as +an array. + +*Examples:* + + range("0", "9") + +Will return: [0,1,2,3,4,5,6,7,8,9] + + range("00", "09") + +Will return: [0,1,2,3,4,5,6,7,8,9] (Zero padded strings are converted to +integers automatically) + + range("a", "c") + +Will return: ["a","b","c"] + + range("host01", "host10") + +Will return: ["host01", "host02", ..., "host09", "host10"] + EOS + ) do |arguments| + + # We support more than one argument but at least one is mandatory ... + raise(Puppet::ParseError, "range(): Wrong number of " + + "arguments given (#{arguments.size} for 1)") if arguments.size < 1 + + if arguments.size > 1 + start = arguments[0] + stop = arguments[1] + + type = '..' # We select simplest type for Range available in Ruby ... + + elsif arguments.size > 0 + value = arguments[0] + + if m = value.match(/^(\w+)(\.\.\.?|\-)(\w+)$/) + start = m[1] + stop = m[3] + + type = m[2] + + elsif value.match(/^.+$/) + raise(Puppet::ParseError, 'range(): Unable to compute range ' + + 'from the value given') + else + raise(Puppet::ParseError, 'range(): Unknown format of range given') + end + end + + # Check whether we have integer value if so then make it so ... + if start.match(/^\d+$/) + start = start.to_i + stop = stop.to_i + else + start = start.to_s + stop = stop.to_s + end + + range = case type + when /^(\.\.|\-)$/ then (start .. stop) + when /^(\.\.\.)$/ then (start ... stop) # Exclusive of last element ... + end + + result = range.collect { |i| i } # Get them all ... Pokemon ... + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/reverse.rb b/puppet/stdlib/lib/puppet/parser/functions/reverse.rb new file mode 100644 index 000000000..fe048690c --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/reverse.rb @@ -0,0 +1,28 @@ +# +# reverse.rb +# + +module Puppet::Parser::Functions + newfunction(:reverse, :type => :rvalue, :doc => <<-EOS +Reverses the order of a string or array. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "reverse(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + value = arguments[0] + klass = value.class + + unless [Array, String].include?(klass) + raise(Puppet::ParseError, 'reverse(): Requires either ' + + 'array or string to work with') + end + + result = value.reverse + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/rstrip.rb b/puppet/stdlib/lib/puppet/parser/functions/rstrip.rb new file mode 100644 index 000000000..29b099820 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/rstrip.rb @@ -0,0 +1,32 @@ +# +# rstrip.rb +# + +module Puppet::Parser::Functions + newfunction(:rstrip, :type => :rvalue, :doc => <<-EOS +Strips leading spaces to the right of the string. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "rstrip(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + value = arguments[0] + klass = value.class + + unless [Array, String].include?(klass) + raise(Puppet::ParseError, 'rstrip(): Requires either ' + + 'array or string to work with') + end + + if value.is_a?(Array) + result = value.collect { |i| i.is_a?(String) ? i.rstrip : i } + else + result = value.rstrip + end + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/shuffle.rb b/puppet/stdlib/lib/puppet/parser/functions/shuffle.rb new file mode 100644 index 000000000..18134ab63 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/shuffle.rb @@ -0,0 +1,46 @@ +# +# shuffle.rb +# + +module Puppet::Parser::Functions + newfunction(:shuffle, :type => :rvalue, :doc => <<-EOS +Randomizes the order of a string or array elements. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "shuffle(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + value = arguments[0] + klass = value.class + + unless [Array, String].include?(klass) + raise(Puppet::ParseError, 'shuffle(): Requires either ' + + 'array or string to work with') + end + + result = value.clone + + string = value.is_a?(String) ? true : false + + # Check whether it makes sense to shuffle ... + return result if result.size <= 1 + + # We turn any string value into an array to be able to shuffle ... + result = string ? result.split('') : result + + elements = result.size + + # Simple implementation of Fisher–Yates in-place shuffle ... + elements.times do |i| + j = rand(elements - i) + i + result[j], result[i] = result[i], result[j] + end + + result = string ? result.join : result + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/size.rb b/puppet/stdlib/lib/puppet/parser/functions/size.rb new file mode 100644 index 000000000..cc207e3fa --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/size.rb @@ -0,0 +1,48 @@ +# +# size.rb +# + +# TODO(Krzysztof Wilczynski): Support for hashes would be nice too ... + +module Puppet::Parser::Functions + newfunction(:size, :type => :rvalue, :doc => <<-EOS +Returns the number of elements in a string or array. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "size(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + item = arguments[0] + + if item.is_a?(String) + + begin + # + # Check whether your item is a numeric value or not ... + # This will take care about positive and/or negative numbers + # for both integer and floating-point values ... + # + # Please note that Puppet has no notion of hexadecimal + # nor octal numbers for its DSL at this point in time ... + # + Float(item) + + raise(Puppet::ParseError, 'size(): Requires either ' + + 'string or array to work with') + + rescue ArgumentError + result = item.size + end + + elsif item.is_a?(Array) + result = item.size + else + raise(Puppet::ParseError, 'size(): Unknown type given') + end + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/sort.rb b/puppet/stdlib/lib/puppet/parser/functions/sort.rb new file mode 100644 index 000000000..cefbe5463 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/sort.rb @@ -0,0 +1,27 @@ +# +# sort.rb +# + +module Puppet::Parser::Functions + newfunction(:sort, :type => :rvalue, :doc => <<-EOS +Sorts strings and arrays lexically. + EOS + ) do |arguments| + + if (arguments.size != 1) then + raise(Puppet::ParseError, "sort(): Wrong number of arguments "+ + "given #{arguments.size} for 1") + end + + value = arguments[0] + + if value.is_a?(Array) then + value.sort + elsif value.is_a?(String) then + value.split("").sort.join("") + end + + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/squeeze.rb b/puppet/stdlib/lib/puppet/parser/functions/squeeze.rb new file mode 100644 index 000000000..65c174aae --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/squeeze.rb @@ -0,0 +1,36 @@ +# +# squeeze.rb +# + +module Puppet::Parser::Functions + newfunction(:squeeze, :type => :rvalue, :doc => <<-EOS +Returns a new string where runs of the same character that occur in this set are replaced by a single character. + EOS + ) do |arguments| + + if ((arguments.size != 2) and (arguments.size != 1)) then + raise(Puppet::ParseError, "squeeze(): Wrong number of arguments "+ + "given #{arguments.size} for 2 or 1") + end + + item = arguments[0] + squeezeval = arguments[1] + + if item.is_a?(Array) then + if squeezeval then + item.collect { |i| i.squeeze(squeezeval) } + else + item.collect { |i| i.squeeze } + end + else + if squeezeval then + item.squeeze(squeezeval) + else + item.squeeze + end + end + + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/str2bool.rb b/puppet/stdlib/lib/puppet/parser/functions/str2bool.rb new file mode 100644 index 000000000..c320da663 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/str2bool.rb @@ -0,0 +1,41 @@ +# +# str2bool.rb +# + +module Puppet::Parser::Functions + newfunction(:str2bool, :type => :rvalue, :doc => <<-EOS +This converts a string to a boolean. This attempt to convert strings that +contain things like: y, 1, t, true to 'true' and strings that contain things +like: 0, f, n, false, no to 'false'. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "str2bool(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + string = arguments[0] + + unless string.is_a?(String) + raise(Puppet::ParseError, 'str2bool(): Requires either ' + + 'string to work with') + end + + # We consider all the yes, no, y, n and so on too ... + result = case string + # + # This is how undef looks like in Puppet ... + # We yield false in this case. + # + when /^$/, '' then false # Empty string will be false ... + when /^(1|t|y|true|yes)$/ then true + when /^(0|f|n|false|no)$/ then false + when /^(undef|undefined)$/ then false # This is not likely to happen ... + else + raise(Puppet::ParseError, 'str2bool(): Unknown type of boolean given') + end + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/str2saltedsha512.rb b/puppet/stdlib/lib/puppet/parser/functions/str2saltedsha512.rb new file mode 100644 index 000000000..7fe7b0128 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/str2saltedsha512.rb @@ -0,0 +1,32 @@ +# +# str2saltedsha512.rb +# + +module Puppet::Parser::Functions + newfunction(:str2saltedsha512, :type => :rvalue, :doc => <<-EOS +This converts a string to a salted-SHA512 password hash (which is used for +OS X versions >= 10.7). Given any simple string, you will get a hex version +of a salted-SHA512 password hash that can be inserted into your Puppet +manifests as a valid password attribute. + EOS + ) do |arguments| + require 'digest/sha2' + + raise(Puppet::ParseError, "str2saltedsha512(): Wrong number of arguments " + + "passed (#{arguments.size} but we require 1)") if arguments.size != 1 + + password = arguments[0] + + unless password.is_a?(String) + raise(Puppet::ParseError, 'str2saltedsha512(): Requires a ' + + "String argument, you passed: #{password.class}") + end + + seedint = rand(2**31 - 1) + seedstring = Array(seedint).pack("L") + saltedpass = Digest::SHA512.digest(seedstring + password) + (seedstring + saltedpass).unpack('H*')[0] + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/strftime.rb b/puppet/stdlib/lib/puppet/parser/functions/strftime.rb new file mode 100644 index 000000000..0b52adecd --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/strftime.rb @@ -0,0 +1,107 @@ +# +# strftime.rb +# + +module Puppet::Parser::Functions + newfunction(:strftime, :type => :rvalue, :doc => <<-EOS +This function returns formatted time. + +*Examples:* + +To return the time since epoch: + + strftime("%s") + +To return the date: + + strftime("%Y-%m-%d") + +*Format meaning:* + + %a - The abbreviated weekday name (``Sun'') + %A - The full weekday name (``Sunday'') + %b - The abbreviated month name (``Jan'') + %B - The full month name (``January'') + %c - The preferred local date and time representation + %C - Century (20 in 2009) + %d - Day of the month (01..31) + %D - Date (%m/%d/%y) + %e - Day of the month, blank-padded ( 1..31) + %F - Equivalent to %Y-%m-%d (the ISO 8601 date format) + %h - Equivalent to %b + %H - Hour of the day, 24-hour clock (00..23) + %I - Hour of the day, 12-hour clock (01..12) + %j - Day of the year (001..366) + %k - hour, 24-hour clock, blank-padded ( 0..23) + %l - hour, 12-hour clock, blank-padded ( 0..12) + %L - Millisecond of the second (000..999) + %m - Month of the year (01..12) + %M - Minute of the hour (00..59) + %n - Newline (\n) + %N - Fractional seconds digits, default is 9 digits (nanosecond) + %3N millisecond (3 digits) + %6N microsecond (6 digits) + %9N nanosecond (9 digits) + %p - Meridian indicator (``AM'' or ``PM'') + %P - Meridian indicator (``am'' or ``pm'') + %r - time, 12-hour (same as %I:%M:%S %p) + %R - time, 24-hour (%H:%M) + %s - Number of seconds since 1970-01-01 00:00:00 UTC. + %S - Second of the minute (00..60) + %t - Tab character (\t) + %T - time, 24-hour (%H:%M:%S) + %u - Day of the week as a decimal, Monday being 1. (1..7) + %U - Week number of the current year, + starting with the first Sunday as the first + day of the first week (00..53) + %v - VMS date (%e-%b-%Y) + %V - Week number of year according to ISO 8601 (01..53) + %W - Week number of the current year, + starting with the first Monday as the first + day of the first week (00..53) + %w - Day of the week (Sunday is 0, 0..6) + %x - Preferred representation for the date alone, no time + %X - Preferred representation for the time alone, no date + %y - Year without a century (00..99) + %Y - Year with century + %z - Time zone as hour offset from UTC (e.g. +0900) + %Z - Time zone name + %% - Literal ``%'' character + EOS + ) do |arguments| + + # Technically we support two arguments but only first is mandatory ... + raise(Puppet::ParseError, "strftime(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + format = arguments[0] + + raise(Puppet::ParseError, 'strftime(): You must provide ' + + 'format for evaluation') if format.empty? + + # The Time Zone argument is optional ... + time_zone = arguments[1] if arguments[1] + + time = Time.new + + # There is probably a better way to handle Time Zone ... + if time_zone and not time_zone.empty? + original_zone = ENV['TZ'] + + local_time = time.clone + local_time = local_time.utc + + ENV['TZ'] = time_zone + + time = local_time.localtime + + ENV['TZ'] = original_zone + end + + result = time.strftime(format) + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/strip.rb b/puppet/stdlib/lib/puppet/parser/functions/strip.rb new file mode 100644 index 000000000..5f4630d7d --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/strip.rb @@ -0,0 +1,39 @@ +# +# strip.rb +# + +module Puppet::Parser::Functions + newfunction(:strip, :type => :rvalue, :doc => <<-EOS +This function removes leading and trailing whitespace from a string or from +every string inside an array. + +*Examples:* + + strip(" aaa ") + +Would result in: "aaa" + EOS + ) do |arguments| + + raise(Puppet::ParseError, "strip(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + value = arguments[0] + klass = value.class + + unless [Array, String].include?(klass) + raise(Puppet::ParseError, 'strip(): Requires either ' + + 'array or string to work with') + end + + if value.is_a?(Array) + result = value.collect { |i| i.is_a?(String) ? i.strip : i } + else + result = value.strip + end + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/swapcase.rb b/puppet/stdlib/lib/puppet/parser/functions/swapcase.rb new file mode 100644 index 000000000..b9e663253 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/swapcase.rb @@ -0,0 +1,39 @@ +# +# swapcase.rb +# + +module Puppet::Parser::Functions + newfunction(:swapcase, :type => :rvalue, :doc => <<-EOS +This function will swap the existing case of a string. + +*Examples:* + + swapcase("aBcD") + +Would result in: "AbCd" + EOS + ) do |arguments| + + raise(Puppet::ParseError, "swapcase(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + value = arguments[0] + klass = value.class + + unless [Array, String].include?(klass) + raise(Puppet::ParseError, 'swapcase(): Requires either ' + + 'array or string to work with') + end + + if value.is_a?(Array) + # Numbers in Puppet are often string-encoded which is troublesome ... + result = value.collect { |i| i.is_a?(String) ? i.swapcase : i } + else + result = value.swapcase + end + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/time.rb b/puppet/stdlib/lib/puppet/parser/functions/time.rb new file mode 100644 index 000000000..0cddaf86b --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/time.rb @@ -0,0 +1,49 @@ +# +# time.rb +# + +module Puppet::Parser::Functions + newfunction(:time, :type => :rvalue, :doc => <<-EOS +This function will return the current time since epoch as an integer. + +*Examples:* + + time() + +Will return something like: 1311972653 + EOS + ) do |arguments| + + # The Time Zone argument is optional ... + time_zone = arguments[0] if arguments[0] + + if (arguments.size != 0) and (arguments.size != 1) then + raise(Puppet::ParseError, "time(): Wrong number of arguments "+ + "given #{arguments.size} for 0 or 1") + end + + time = Time.new + + # There is probably a better way to handle Time Zone ... + if time_zone and not time_zone.empty? + original_zone = ENV['TZ'] + + local_time = time.clone + local_time = local_time.utc + + ENV['TZ'] = time_zone + + time = local_time.localtime + + ENV['TZ'] = original_zone + end + + # Calling Time#to_i on a receiver changes it. Trust me I am the Doctor. + result = time.strftime('%s') + result = result.to_i + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/to_bytes.rb b/puppet/stdlib/lib/puppet/parser/functions/to_bytes.rb new file mode 100644 index 000000000..8ff73d10b --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/to_bytes.rb @@ -0,0 +1,28 @@ +module Puppet::Parser::Functions + newfunction(:to_bytes, :type => :rvalue, :doc => <<-EOS + Converts the argument into bytes, for example 4 kB becomes 4096. + Takes a single string value as an argument. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "to_bytes(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size != 1 + + arg = arguments[0] + + return arg if arg.is_a? Numeric + + value,prefix = */([0-9.e+-]*)\s*([^bB]?)/.match(arg)[1,2] + + value = value.to_f + case prefix + when '' then return value.to_i + when 'k' then return (value*(1<<10)).to_i + when 'M' then return (value*(1<<20)).to_i + when 'G' then return (value*(1<<30)).to_i + when 'T' then return (value*(1<<40)).to_i + when 'E' then return (value*(1<<50)).to_i + else raise Puppet::ParseError, "to_bytes(): Unknown prefix #{prefix}" + end + end +end diff --git a/puppet/stdlib/lib/puppet/parser/functions/type.rb b/puppet/stdlib/lib/puppet/parser/functions/type.rb new file mode 100644 index 000000000..8d85f1158 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/type.rb @@ -0,0 +1,50 @@ +# +# type.rb +# + +module Puppet::Parser::Functions + newfunction(:type, :type => :rvalue, :doc => <<-EOS +Returns the type when passed a variable. Type can be one of: + +* string +* array +* hash +* float +* integer +* boolean + EOS + ) do |arguments| + + raise(Puppet::ParseError, "type(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + value = arguments[0] + + klass = value.class + + if not [TrueClass, FalseClass, Array, Bignum, Fixnum, Float, Hash, String].include?(klass) + raise(Puppet::ParseError, 'type(): Unknown type') + end + + klass = klass.to_s # Ugly ... + + # We note that Integer is the parent to Bignum and Fixnum ... + result = case klass + when /^(?:Big|Fix)num$/ then 'integer' + when /^(?:True|False)Class$/ then 'boolean' + else klass + end + + if result == "String" then + if value == value.to_i.to_s then + result = "Integer" + elsif value == value.to_f.to_s then + result = "Float" + end + end + + return result.downcase + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/unique.rb b/puppet/stdlib/lib/puppet/parser/functions/unique.rb new file mode 100644 index 000000000..8844a7418 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/unique.rb @@ -0,0 +1,51 @@ +# +# unique.rb +# + +module Puppet::Parser::Functions + newfunction(:unique, :type => :rvalue, :doc => <<-EOS +This function will remove duplicates from strings and arrays. + +*Examples:* + + unique("aabbcc") + +Will return: + + abc + +You can also use this with arrays: + + unique(["a","a","b","b","c","c"]) + +This returns: + + ["a","b","c"] + EOS + ) do |arguments| + + raise(Puppet::ParseError, "unique(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + value = arguments[0] + klass = value.class + + unless [Array, String].include?(klass) + raise(Puppet::ParseError, 'unique(): Requires either ' + + 'array or string to work with') + end + + result = value.clone + + string = value.is_a?(String) ? true : false + + # We turn any string value into an array to be able to shuffle ... + result = string ? result.split('') : result + result = result.uniq # Remove duplicates ... + result = string ? result.join : result + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/upcase.rb b/puppet/stdlib/lib/puppet/parser/functions/upcase.rb new file mode 100644 index 000000000..fe6cadc3c --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/upcase.rb @@ -0,0 +1,41 @@ +# +# upcase.rb +# + +module Puppet::Parser::Functions + newfunction(:upcase, :type => :rvalue, :doc => <<-EOS +Converts a string or an array of strings to uppercase. + +*Examples:* + + upcase("abcd") + +Will return: + + ASDF + EOS + ) do |arguments| + + raise(Puppet::ParseError, "upcase(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + value = arguments[0] + klass = value.class + + unless [Array, String].include?(klass) + raise(Puppet::ParseError, 'upcase(): Requires either ' + + 'array or string to work with') + end + + if value.is_a?(Array) + # Numbers in Puppet are often string-encoded which is troublesome ... + result = value.collect { |i| i.is_a?(String) ? i.upcase : i } + else + result = value.upcase + end + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/validate_absolute_path.rb b/puppet/stdlib/lib/puppet/parser/functions/validate_absolute_path.rb new file mode 100644 index 000000000..fe279744e --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/validate_absolute_path.rb @@ -0,0 +1,56 @@ +module Puppet::Parser::Functions + newfunction(:validate_absolute_path, :doc => <<-'ENDHEREDOC') do |args| + Validate the string represents an absolute path in the filesystem. This function works + for windows and unix style paths. + + The following values will pass: + + $my_path = "C:/Program Files (x86)/Puppet Labs/Puppet" + validate_absolute_path($my_path) + $my_path2 = "/var/lib/puppet" + validate_absolute_path($my_path2) + + + The following values will fail, causing compilation to abort: + + validate_absolute_path(true) + validate_absolute_path([ 'var/lib/puppet', '/var/foo' ]) + validate_absolute_path([ '/var/lib/puppet', 'var/foo' ]) + $undefined = undef + validate_absolute_path($undefined) + + ENDHEREDOC + + require 'puppet/util' + + unless args.length > 0 then + raise Puppet::ParseError, ("validate_absolute_path(): wrong number of arguments (#{args.length}; must be > 0)") + end + + args.each do |arg| + # This logic was borrowed from + # [lib/puppet/file_serving/base.rb](https://github.com/puppetlabs/puppet/blob/master/lib/puppet/file_serving/base.rb) + + # Puppet 2.7 and beyond will have Puppet::Util.absolute_path? Fall back to a back-ported implementation otherwise. + if Puppet::Util.respond_to?(:absolute_path?) then + unless Puppet::Util.absolute_path?(arg, :posix) or Puppet::Util.absolute_path?(arg, :windows) + raise Puppet::ParseError, ("#{arg.inspect} is not an absolute path.") + end + else + # This code back-ported from 2.7.x's lib/puppet/util.rb Puppet::Util.absolute_path? + # Determine in a platform-specific way whether a path is absolute. This + # defaults to the local platform if none is specified. + # Escape once for the string literal, and once for the regex. + slash = '[\\\\/]' + name = '[^\\\\/]+' + regexes = { + :windows => %r!^(([A-Z]:#{slash})|(#{slash}#{slash}#{name}#{slash}#{name})|(#{slash}#{slash}\?#{slash}#{name}))!i, + :posix => %r!^/!, + } + + rval = (!!(arg =~ regexes[:posix])) || (!!(arg =~ regexes[:windows])) + rval or raise Puppet::ParseError, ("#{arg.inspect} is not an absolute path.") + end + end + end +end diff --git a/puppet/stdlib/lib/puppet/parser/functions/validate_array.rb b/puppet/stdlib/lib/puppet/parser/functions/validate_array.rb new file mode 100644 index 000000000..34b511825 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/validate_array.rb @@ -0,0 +1,33 @@ +module Puppet::Parser::Functions + + newfunction(:validate_array, :doc => <<-'ENDHEREDOC') do |args| + Validate that all passed values are array data structures. Abort catalog + compilation if any value fails this check. + + The following values will pass: + + $my_array = [ 'one', 'two' ] + validate_array($my_array) + + The following values will fail, causing compilation to abort: + + validate_array(true) + validate_array('some_string') + $undefined = undef + validate_array($undefined) + + ENDHEREDOC + + unless args.length > 0 then + raise Puppet::ParseError, ("validate_array(): wrong number of arguments (#{args.length}; must be > 0)") + end + + args.each do |arg| + unless arg.is_a?(Array) + raise Puppet::ParseError, ("#{arg.inspect} is not an Array. It looks to be a #{arg.class}") + end + end + + end + +end diff --git a/puppet/stdlib/lib/puppet/parser/functions/validate_bool.rb b/puppet/stdlib/lib/puppet/parser/functions/validate_bool.rb new file mode 100644 index 000000000..62c1d8882 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/validate_bool.rb @@ -0,0 +1,34 @@ +module Puppet::Parser::Functions + + newfunction(:validate_bool, :doc => <<-'ENDHEREDOC') do |args| + Validate that all passed values are either true or false. Abort catalog + compilation if any value fails this check. + + The following values will pass: + + $iamtrue = true + validate_bool(true) + validate_bool(true, true, false, $iamtrue) + + The following values will fail, causing compilation to abort: + + $some_array = [ true ] + validate_bool("false") + validate_bool("true") + validate_bool($some_array) + + ENDHEREDOC + + unless args.length > 0 then + raise Puppet::ParseError, ("validate_bool(): wrong number of arguments (#{args.length}; must be > 0)") + end + + args.each do |arg| + unless (arg.is_a?(TrueClass) || arg.is_a?(FalseClass)) + raise Puppet::ParseError, ("#{arg.inspect} is not a boolean. It looks to be a #{arg.class}") + end + end + + end + +end diff --git a/puppet/stdlib/lib/puppet/parser/functions/validate_hash.rb b/puppet/stdlib/lib/puppet/parser/functions/validate_hash.rb new file mode 100644 index 000000000..9bdd54328 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/validate_hash.rb @@ -0,0 +1,33 @@ +module Puppet::Parser::Functions + + newfunction(:validate_hash, :doc => <<-'ENDHEREDOC') do |args| + Validate that all passed values are hash data structures. Abort catalog + compilation if any value fails this check. + + The following values will pass: + + $my_hash = { 'one' => 'two' } + validate_hash($my_hash) + + The following values will fail, causing compilation to abort: + + validate_hash(true) + validate_hash('some_string') + $undefined = undef + validate_hash($undefined) + + ENDHEREDOC + + unless args.length > 0 then + raise Puppet::ParseError, ("validate_hash(): wrong number of arguments (#{args.length}; must be > 0)") + end + + args.each do |arg| + unless arg.is_a?(Hash) + raise Puppet::ParseError, ("#{arg.inspect} is not a Hash. It looks to be a #{arg.class}") + end + end + + end + +end diff --git a/puppet/stdlib/lib/puppet/parser/functions/validate_re.rb b/puppet/stdlib/lib/puppet/parser/functions/validate_re.rb new file mode 100644 index 000000000..ca25a702c --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/validate_re.rb @@ -0,0 +1,40 @@ +module Puppet::Parser::Functions + newfunction(:validate_re, :doc => <<-'ENDHEREDOC') do |args| + Perform simple validation of a string against one or more regular + expressions. The first argument of this function should be a string to + test, and the second argument should be a stringified regular expression + (without the // delimiters) or an array of regular expressions. If none + of the regular expressions match the string passed in, compilation will + abort with a parse error. + + If a third argument is specified, this will be the error message raised and + seen by the user. + + The following strings will validate against the regular expressions: + + validate_re('one', '^one$') + validate_re('one', [ '^one', '^two' ]) + + The following strings will fail to validate, causing compilation to abort: + + validate_re('one', [ '^two', '^three' ]) + + A helpful error message can be returned like this: + + validate_re($::puppetversion, '^2.7', 'The $puppetversion fact value does not match 2.7') + + ENDHEREDOC + if (args.length < 2) or (args.length > 3) then + raise Puppet::ParseError, ("validate_re(): wrong number of arguments (#{args.length}; must be 2 or 3)") + end + + msg = args[2] || "validate_re(): #{args[0].inspect} does not match #{args[1].inspect}" + + # We're using a flattened array here because we can't call String#any? in + # Ruby 1.9 like we can in Ruby 1.8 + raise Puppet::ParseError, (msg) unless [args[1]].flatten.any? do |re_str| + args[0] =~ Regexp.compile(re_str) + end + + end +end diff --git a/puppet/stdlib/lib/puppet/parser/functions/validate_slength.rb b/puppet/stdlib/lib/puppet/parser/functions/validate_slength.rb new file mode 100644 index 000000000..fdcc0a2d1 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/validate_slength.rb @@ -0,0 +1,52 @@ +module Puppet::Parser::Functions + + newfunction(:validate_slength, :doc => <<-'ENDHEREDOC') do |args| + Validate that the first argument is a string (or an array of strings), and + less/equal to than the length of the second argument. It fails if the first + argument is not a string or array of strings, and if arg 2 is not convertable + to a number. + + The following values will pass: + + validate_slength("discombobulate",17) + validate_slength(["discombobulate","moo"],17) + + The following valueis will not: + + validate_slength("discombobulate",1) + validate_slength(["discombobulate","thermometer"],5) + + ENDHEREDOC + + raise Puppet::ParseError, ("validate_slength(): Wrong number of arguments (#{args.length}; must be = 2)") unless args.length == 2 + + unless (args[0].is_a?(String) or args[0].is_a?(Array)) + raise Puppet::ParseError, ("validate_slength(): please pass a string, or an array of strings - what you passed didn't work for me at all - #{args[0].class}") + end + + begin + max_length = args[1].to_i + rescue NoMethodError => e + raise Puppet::ParseError, ("validate_slength(): Couldn't convert whatever you passed as the length parameter to an integer - sorry: " + e.message ) + end + + raise Puppet::ParseError, ("validate_slength(): please pass a positive number as max_length") unless max_length > 0 + + case args[0] + when String + raise Puppet::ParseError, ("validate_slength(): #{args[0].inspect} is #{args[0].length} characters. It should have been less than or equal to #{max_length} characters") unless args[0].length <= max_length + when Array + args[0].each do |arg| + if arg.is_a?(String) + unless ( arg.is_a?(String) and arg.length <= max_length ) + raise Puppet::ParseError, ("validate_slength(): #{arg.inspect} is #{arg.length} characters. It should have been less than or equal to #{max_length} characters") + end + else + raise Puppet::ParseError, ("validate_slength(): #{arg.inspect} is not a string, it's a #{arg.class}") + end + end + else + raise Puppet::ParseError, ("validate_slength(): please pass a string, or an array of strings - what you passed didn't work for me at all - #{args[0].class}") + end + end +end diff --git a/puppet/stdlib/lib/puppet/parser/functions/validate_string.rb b/puppet/stdlib/lib/puppet/parser/functions/validate_string.rb new file mode 100644 index 000000000..e667794a6 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/validate_string.rb @@ -0,0 +1,33 @@ +module Puppet::Parser::Functions + + newfunction(:validate_string, :doc => <<-'ENDHEREDOC') do |args| + Validate that all passed values are string data structures. Abort catalog + compilation if any value fails this check. + + The following values will pass: + + $my_string = "one two" + validate_string($my_string, 'three') + + The following values will fail, causing compilation to abort: + + validate_string(true) + validate_string([ 'some', 'array' ]) + $undefined = undef + validate_string($undefined) + + ENDHEREDOC + + unless args.length > 0 then + raise Puppet::ParseError, ("validate_string(): wrong number of arguments (#{args.length}; must be > 0)") + end + + args.each do |arg| + unless arg.is_a?(String) + raise Puppet::ParseError, ("#{arg.inspect} is not a string. It looks to be a #{arg.class}") + end + end + + end + +end diff --git a/puppet/stdlib/lib/puppet/parser/functions/values.rb b/puppet/stdlib/lib/puppet/parser/functions/values.rb new file mode 100644 index 000000000..16067561b --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/values.rb @@ -0,0 +1,39 @@ +# +# values.rb +# + +module Puppet::Parser::Functions + newfunction(:values, :type => :rvalue, :doc => <<-EOS +When given a hash this function will return the values of that hash. + +*Examples:* + + $hash = { + 'a' => 1, + 'b' => 2, + 'c' => 3, + } + values($hash) + +This example would return: + + [1,2,3] + EOS + ) do |arguments| + + raise(Puppet::ParseError, "values(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size < 1 + + hash = arguments[0] + + unless hash.is_a?(Hash) + raise(Puppet::ParseError, 'values(): Requires hash to work with') + end + + result = hash.values + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/values_at.rb b/puppet/stdlib/lib/puppet/parser/functions/values_at.rb new file mode 100644 index 000000000..d3e69d97f --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/values_at.rb @@ -0,0 +1,98 @@ +# +# values_at.rb +# + +module Puppet::Parser::Functions + newfunction(:values_at, :type => :rvalue, :doc => <<-EOS +Finds value inside an array based on location. + +The first argument is the array you want to analyze, and the second element can +be a combination of: + +* A single numeric index +* A range in the form of 'start-stop' (eg. 4-9) +* An array combining the above + +*Examples*: + + values_at(['a','b','c'], 2) + +Would return ['c']. + + values_at(['a','b','c'], ["0-1"]) + +Would return ['a','b']. + + values_at(['a','b','c','d','e'], [0, "2-3"]) + +Would return ['a','c','d']. + EOS + ) do |arguments| + + raise(Puppet::ParseError, "values_at(): Wrong number of " + + "arguments given (#{arguments.size} for 2)") if arguments.size < 2 + + array = arguments.shift + + unless array.is_a?(Array) + raise(Puppet::ParseError, 'values_at(): Requires array to work with') + end + + indices = [arguments.shift].flatten() # Get them all ... Pokemon ... + + if not indices or indices.empty? + raise(Puppet::ParseError, 'values_at(): You must provide ' + + 'at least one positive index to collect') + end + + result = [] + indices_list = [] + + indices.each do |i| + if m = i.match(/^(\d+)(\.\.\.?|\-)(\d+)$/) + start = m[1].to_i + stop = m[3].to_i + + type = m[2] + + if start > stop + raise(Puppet::ParseError, 'values_at(): Stop index in ' + + 'given indices range is smaller than the start index') + elsif stop > array.size - 1 # First element is at index 0 is it not? + raise(Puppet::ParseError, 'values_at(): Stop index in ' + + 'given indices range exceeds array size') + end + + range = case type + when /^(\.\.|\-)$/ then (start .. stop) + when /^(\.\.\.)$/ then (start ... stop) # Exclusive of last element ... + end + + range.each { |i| indices_list << i.to_i } + else + # Only positive numbers allowed in this case ... + if not i.match(/^\d+$/) + raise(Puppet::ParseError, 'values_at(): Unknown format ' + + 'of given index') + end + + # In Puppet numbers are often string-encoded ... + i = i.to_i + + if i > array.size - 1 # Same story. First element is at index 0 ... + raise(Puppet::ParseError, 'values_at(): Given index ' + + 'exceeds array size') + end + + indices_list << i + end + end + + # We remove nil values as they make no sense in Puppet DSL ... + result = indices_list.collect { |i| array[i] }.compact + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/parser/functions/zip.rb b/puppet/stdlib/lib/puppet/parser/functions/zip.rb new file mode 100644 index 000000000..2b56e9ca0 --- /dev/null +++ b/puppet/stdlib/lib/puppet/parser/functions/zip.rb @@ -0,0 +1,65 @@ +# +# zip.rb +# + +module Puppet::Parser::Functions + newfunction(:zip, :type => :rvalue, :doc => <<-EOS +Takes one element from first array and merges corresponding elements from second array. This generates a sequence of n-element arrays, where n is one more than the count of arguments. + +*Example:* + + zip(['1','2','3'],['4','5','6']) + +Would result in: + + ["1", "4"], ["2", "5"], ["3", "6"] + EOS + ) do |arguments| + + # Technically we support three arguments but only first is mandatory ... + raise(Puppet::ParseError, "zip(): Wrong number of arguments " + + "given (#{arguments.size} for 2)") if arguments.size < 2 + + a = arguments[0] + b = arguments[1] + + unless a.is_a?(Array) and b.is_a?(Array) + raise(Puppet::ParseError, 'zip(): Requires array to work with') + end + + flatten = arguments[2] if arguments[2] + + if flatten + klass = flatten.class + + # We can have either true or false, or string which resembles boolean ... + unless [FalseClass, TrueClass, String].include?(klass) + raise(Puppet::ParseError, 'zip(): Requires either ' + + 'boolean or string to work with') + end + + if flatten.is_a?(String) + # We consider all the yes, no, y, n and so on too ... + flatten = case flatten + # + # This is how undef looks like in Puppet ... + # We yield false in this case. + # + when /^$/, '' then false # Empty string will be false ... + when /^(1|t|y|true|yes)$/ then true + when /^(0|f|n|false|no)$/ then false + when /^(undef|undefined)$/ then false # This is not likely to happen ... + else + raise(Puppet::ParseError, 'zip(): Unknown type of boolean given') + end + end + end + + result = a.zip(b) + result = flatten ? result.flatten : result + + return result + end +end + +# vim: set ts=2 sw=2 et : diff --git a/puppet/stdlib/lib/puppet/provider/file_line/ruby.rb b/puppet/stdlib/lib/puppet/provider/file_line/ruby.rb new file mode 100644 index 000000000..e21eaa82a --- /dev/null +++ b/puppet/stdlib/lib/puppet/provider/file_line/ruby.rb @@ -0,0 +1,59 @@ + +Puppet::Type.type(:file_line).provide(:ruby) do + + def exists? + lines.find do |line| + line.chomp == resource[:line].chomp + end + end + + def create + if resource[:match] + handle_create_with_match() + else + handle_create_without_match() + end + end + + def destroy + local_lines = lines + File.open(resource[:path],'w') do |fh| + fh.write(local_lines.reject{|l| l.chomp == resource[:line] }.join('')) + end + end + + private + def lines + # If this type is ever used with very large files, we should + # write this in a different way, using a temp + # file; for now assuming that this type is only used on + # small-ish config files that can fit into memory without + # too much trouble. + @lines ||= File.readlines(resource[:path]) + end + + def handle_create_with_match() + regex = resource[:match] ? Regexp.new(resource[:match]) : nil + match_count = lines.select { |l| regex.match(l) }.count + if match_count > 1 + raise Puppet::Error, "More than one line in file '#{resource[:path]}' matches pattern '#{resource[:match]}'" + end + File.open(resource[:path], 'w') do |fh| + lines.each do |l| + fh.puts(regex.match(l) ? resource[:line] : l) + end + + if (match_count == 0) + fh.puts(resource[:line]) + end + end + end + + def handle_create_without_match + File.open(resource[:path], 'a') do |fh| + fh.puts resource[:line] + end + end + + +end diff --git a/puppet/stdlib/lib/puppet/type/anchor.rb b/puppet/stdlib/lib/puppet/type/anchor.rb new file mode 100644 index 000000000..6b8173215 --- /dev/null +++ b/puppet/stdlib/lib/puppet/type/anchor.rb @@ -0,0 +1,41 @@ +Puppet::Type.newtype(:anchor) do + desc <<-'ENDOFDESC' + A simple resource type intended to be used as an anchor in a composite class. + + In Puppet 2.6, when a class declares another class, the resources in the + interior class are not contained by the exterior class. This interacts badly + with the pattern of composing complex modules from smaller classes, as it + makes it impossible for end users to specify order relationships between the + exterior class and other modules. + + The anchor type lets you work around this. By sandwiching any interior + classes between two no-op resources that _are_ contained by the exterior + class, you can ensure that all resources in the module are contained. + + class ntp { + # These classes will have the correct order relationship with each + # other. However, without anchors, they won't have any order + # relationship to Class['ntp']. + class { 'ntp::package': } + -> class { 'ntp::config': } + -> class { 'ntp::service': } + + # These two resources "anchor" the composed classes within the ntp + # class. + anchor { 'ntp::begin': } -> Class['ntp::package'] + Class['ntp::service'] -> anchor { 'ntp::end': } + } + + This allows the end user of the ntp module to establish require and before + relationships with Class['ntp']: + + class { 'ntp': } -> class { 'mcollective': } + class { 'mcollective': } -> class { 'ntp': } + + ENDOFDESC + + newparam :name do + desc "The name of the anchor resource." + end + +end diff --git a/puppet/stdlib/lib/puppet/type/file_line.rb b/puppet/stdlib/lib/puppet/type/file_line.rb new file mode 100644 index 000000000..6b3590237 --- /dev/null +++ b/puppet/stdlib/lib/puppet/type/file_line.rb @@ -0,0 +1,65 @@ +Puppet::Type.newtype(:file_line) do + + desc <<-EOT + Ensures that a given line is contained within a file. The implementation + matches the full line, including whitespace at the beginning and end. If + the line is not contained in the given file, Puppet will add the line to + ensure the desired state. Multiple resources may be declared to manage + multiple lines in the same file. + + Example: + + file_line { 'sudo_rule': + path => '/etc/sudoers', + line => '%sudo ALL=(ALL) ALL', + } + file_line { 'sudo_rule_nopw': + path => '/etc/sudoers', + line => '%sudonopw ALL=(ALL) NOPASSWD: ALL', + } + + In this example, Puppet will ensure both of the specified lines are + contained in the file /etc/sudoers. + + EOT + + ensurable do + defaultvalues + defaultto :present + end + + newparam(:name, :namevar => true) do + desc 'An arbitrary name used as the identity of the resource.' + end + + newparam(:match) do + desc 'An optional regular expression to run against existing lines in the file;\n' + + 'if a match is found, we replace that line rather than adding a new line.' + end + + newparam(:line) do + desc 'The line to be appended to the file located by the path parameter.' + end + + newparam(:path) do + desc 'The file Puppet will ensure contains the line specified by the line parameter.' + validate do |value| + unless (Puppet.features.posix? and value =~ /^\//) or (Puppet.features.microsoft_windows? and (value =~ /^.:\// or value =~ /^\/\/[^\/]+\/[^\/]+/)) + raise(Puppet::Error, "File paths must be fully qualified, not '#{value}'") + end + end + end + + validate do + unless self[:line] and self[:path] + raise(Puppet::Error, "Both line and path are required attributes") + end + + if (self[:match]) + unless Regexp.new(self[:match]).match(self[:line]) + raise(Puppet::Error, "When providing a 'match' parameter, the value must be a regex that matches against the value of your 'line' parameter") + end + end + + end +end diff --git a/puppet/stdlib/manifests/init.pp b/puppet/stdlib/manifests/init.pp new file mode 100644 index 000000000..500ad770d --- /dev/null +++ b/puppet/stdlib/manifests/init.pp @@ -0,0 +1,20 @@ +# Class: stdlib +# +# This module manages stdlib. Most of stdlib's features are automatically +# loaded by Puppet, but this class should be declared in order to use the +# standardized run stages. +# +# Parameters: none +# +# Actions: +# +# Declares all other classes in the stdlib module. Currently, this consists +# of stdlib::stages. +# +# Requires: nothing +# +class stdlib { + + class { 'stdlib::stages': } + +} diff --git a/puppet/stdlib/manifests/stages.pp b/puppet/stdlib/manifests/stages.pp new file mode 100644 index 000000000..eb15fd650 --- /dev/null +++ b/puppet/stdlib/manifests/stages.pp @@ -0,0 +1,43 @@ +# Class: stdlib::stages +# +# This class manages a standard set of run stages for Puppet. It is managed by +# the stdlib class, and should not be declared independently. +# +# The high level stages are (in order): +# +# * setup +# * main +# * runtime +# * setup_infra +# * deploy_infra +# * setup_app +# * deploy_app +# * deploy +# +# Parameters: none +# +# Actions: +# +# Declares various run-stages for deploying infrastructure, +# language runtimes, and application layers. +# +# Requires: nothing +# +# Sample Usage: +# +# node default { +# include stdlib +# class { java: stage => 'runtime' } +# } +# +class stdlib::stages { + + stage { 'setup': before => Stage['main'] } + stage { 'runtime': require => Stage['main'] } + -> stage { 'setup_infra': } + -> stage { 'deploy_infra': } + -> stage { 'setup_app': } + -> stage { 'deploy_app': } + -> stage { 'deploy': } + +} diff --git a/puppet/stdlib/spec/functions/defined_with_params_spec.rb b/puppet/stdlib/spec/functions/defined_with_params_spec.rb new file mode 100644 index 000000000..28dbab311 --- /dev/null +++ b/puppet/stdlib/spec/functions/defined_with_params_spec.rb @@ -0,0 +1,37 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +require 'rspec-puppet' +describe 'defined_with_params' do + describe 'when a resource is not specified' do + it { should run.with_params().and_raise_error(ArgumentError) } + end + describe 'when compared against a resource with no attributes' do + let :pre_condition do + 'user { "dan": }' + end + it do + should run.with_params('User[dan]', {}).and_return(true) + should run.with_params('User[bob]', {}).and_return(false) + should run.with_params('User[dan]', {'foo' => 'bar'}).and_return(false) + end + end + + describe 'when compared against a resource with attributes' do + let :pre_condition do + 'user { "dan": ensure => present, shell => "/bin/csh", managehome => false}' + end + it do + should run.with_params('User[dan]', {}).and_return(true) + should run.with_params('User[dan]', '').and_return(true) + should run.with_params('User[dan]', {'ensure' => 'present'} + ).and_return(true) + should run.with_params('User[dan]', + {'ensure' => 'present', 'managehome' => false} + ).and_return(true) + should run.with_params('User[dan]', + {'ensure' => 'absent', 'managehome' => false} + ).and_return(false) + end + end +end diff --git a/puppet/stdlib/spec/functions/ensure_resource_spec.rb b/puppet/stdlib/spec/functions/ensure_resource_spec.rb new file mode 100644 index 000000000..611666ee8 --- /dev/null +++ b/puppet/stdlib/spec/functions/ensure_resource_spec.rb @@ -0,0 +1,40 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +require 'rspec-puppet' +describe 'ensure_resource' do + describe 'when a type or title is not specified' do + it do + should run.with_params().and_raise_error(ArgumentError) + should run.with_params(['type']).and_raise_error(ArgumentError) + end + end + describe 'when compared against a resource with no attributes' do + let :pre_condition do + 'user { "dan": }' + end + it do + should run.with_params('user', 'dan', {}) + compiler.catalog.resource('User[dan]').to_s.should == 'User[dan]' + end + end + + describe 'when compared against a resource with attributes' do + let :pre_condition do + 'user { "dan": ensure => present, shell => "/bin/csh", managehome => false}' + end + it do + # these first three should not fail + should run.with_params('User', 'dan', {}) + should run.with_params('User', 'dan', '') + should run.with_params('User', 'dan', {'ensure' => 'present'}) + should run.with_params('User', 'dan', + {'ensure' => 'present', 'managehome' => false} + ) + # test that this fails + should run.with_params('User', 'dan', + {'ensure' => 'absent', 'managehome' => false} + ).and_raise_error(Puppet::Error) + end + end +end diff --git a/puppet/stdlib/spec/monkey_patches/alias_should_to_must.rb b/puppet/stdlib/spec/monkey_patches/alias_should_to_must.rb new file mode 100755 index 000000000..1a1111799 --- /dev/null +++ b/puppet/stdlib/spec/monkey_patches/alias_should_to_must.rb @@ -0,0 +1,8 @@ +require 'rspec' + +class Object + # This is necessary because the RAL has a 'should' + # method. + alias :must :should + alias :must_not :should_not +end diff --git a/puppet/stdlib/spec/monkey_patches/publicize_methods.rb b/puppet/stdlib/spec/monkey_patches/publicize_methods.rb new file mode 100755 index 000000000..b39e9c002 --- /dev/null +++ b/puppet/stdlib/spec/monkey_patches/publicize_methods.rb @@ -0,0 +1,11 @@ +# Some monkey-patching to allow us to test private methods. +class Class + def publicize_methods(*methods) + saved_private_instance_methods = methods.empty? ? self.private_instance_methods : methods + + self.class_eval { public(*saved_private_instance_methods) } + yield + self.class_eval { private(*saved_private_instance_methods) } + end +end + diff --git a/puppet/stdlib/spec/spec.opts b/puppet/stdlib/spec/spec.opts new file mode 100644 index 000000000..91cd6427e --- /dev/null +++ b/puppet/stdlib/spec/spec.opts @@ -0,0 +1,6 @@ +--format +s +--colour +--loadby +mtime +--backtrace diff --git a/puppet/stdlib/spec/spec_helper.rb b/puppet/stdlib/spec/spec_helper.rb new file mode 100644 index 000000000..e269b90df --- /dev/null +++ b/puppet/stdlib/spec/spec_helper.rb @@ -0,0 +1,13 @@ +dir = File.expand_path(File.dirname(__FILE__)) +$LOAD_PATH.unshift File.join(dir, 'lib') + +# Don't want puppet getting the command line arguments for rake or autotest +ARGV.clear + +require 'puppet' +require 'facter' +require 'mocha' +gem 'rspec', '>=2.0.0' +require 'rspec/expectations' + +require 'puppetlabs_spec_helper/module_spec_helper' diff --git a/puppet/stdlib/spec/unit/facter/root_home_spec.rb b/puppet/stdlib/spec/unit/facter/root_home_spec.rb new file mode 100644 index 000000000..ce80684ca --- /dev/null +++ b/puppet/stdlib/spec/unit/facter/root_home_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' +require 'facter/root_home' + +describe Facter::Util::RootHome do + context "solaris" do + let(:root_ent) { "root:x:0:0:Super-User:/:/sbin/sh" } + let(:expected_root_home) { "/" } + + it "should return /" do + Facter::Util::Resolution.expects(:exec).with("getent passwd root").returns(root_ent) + Facter::Util::RootHome.get_root_home.should == expected_root_home + end + end + context "linux" do + let(:root_ent) { "root:x:0:0:root:/root:/bin/bash" } + let(:expected_root_home) { "/root" } + + it "should return /root" do + Facter::Util::Resolution.expects(:exec).with("getent passwd root").returns(root_ent) + Facter::Util::RootHome.get_root_home.should == expected_root_home + end + end + context "macosx" do + let(:root_ent) { "root:*:0:0:System Administrator:/var/root:/bin/sh" } + let(:expected_root_home) { "/var/root" } + + it "should return /var/root" do + Facter::Util::Resolution.expects(:exec).with("getent passwd root").returns(root_ent) + Facter::Util::RootHome.get_root_home.should == expected_root_home + end + end + context "windows" do + before :each do + Facter::Util::Resolution.expects(:exec).with("getent passwd root").returns(nil) + end + it "should be nil on windows" do + Facter::Util::RootHome.get_root_home.should be_nil + end + end +end diff --git a/puppet/stdlib/spec/unit/facter/util/puppet_settings_spec.rb b/puppet/stdlib/spec/unit/facter/util/puppet_settings_spec.rb new file mode 100644 index 000000000..c3ce6ea07 --- /dev/null +++ b/puppet/stdlib/spec/unit/facter/util/puppet_settings_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' +require 'facter/util/puppet_settings' + +describe Facter::Util::PuppetSettings do + + describe "#with_puppet" do + context "Without Puppet loaded" do + before(:each) do + Module.expects(:const_get).with("Puppet").raises(NameError) + end + + it 'should be nil' do + subject.with_puppet { Puppet[:vardir] }.should be_nil + end + it 'should not yield to the block' do + Puppet.expects(:[]).never + subject.with_puppet { Puppet[:vardir] }.should be_nil + end + end + context "With Puppet loaded" do + module Puppet; end + let(:vardir) { "/var/lib/puppet" } + + before :each do + Puppet.expects(:[]).with(:vardir).returns vardir + end + it 'should yield to the block' do + subject.with_puppet { Puppet[:vardir] } + end + it 'should return the nodes vardir' do + subject.with_puppet { Puppet[:vardir] }.should eq vardir + end + end + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/abs_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/abs_spec.rb new file mode 100755 index 000000000..c0b42970c --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/abs_spec.rb @@ -0,0 +1,25 @@ +#! /usr/bin/env ruby -S rspec + +require 'spec_helper' + +describe "the abs function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("abs").should == "function_abs" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_abs([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should convert a negative number into a positive" do + result = scope.function_abs(["-34"]) + result.should(eq(34)) + end + + it "should do nothing with a positive number" do + result = scope.function_abs(["5678"]) + result.should(eq(5678)) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/bool2num_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/bool2num_spec.rb new file mode 100755 index 000000000..518ac85ec --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/bool2num_spec.rb @@ -0,0 +1,24 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the bool2num function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("bool2num").should == "function_bool2num" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_bool2num([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should convert true to 1" do + result = scope.function_bool2num([true]) + result.should(eq(1)) + end + + it "should convert false to 0" do + result = scope.function_bool2num([false]) + result.should(eq(0)) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/capitalize_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/capitalize_spec.rb new file mode 100755 index 000000000..69c9758f2 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/capitalize_spec.rb @@ -0,0 +1,19 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the capitalize function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("capitalize").should == "function_capitalize" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_capitalize([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should capitalize the beginning of a string" do + result = scope.function_capitalize(["abc"]) + result.should(eq("Abc")) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/chomp_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/chomp_spec.rb new file mode 100755 index 000000000..e425365fc --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/chomp_spec.rb @@ -0,0 +1,19 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the chomp function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("chomp").should == "function_chomp" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_chomp([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should chomp the end of a string" do + result = scope.function_chomp(["abc\n"]) + result.should(eq("abc")) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/chop_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/chop_spec.rb new file mode 100755 index 000000000..9e466de4b --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/chop_spec.rb @@ -0,0 +1,19 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the chop function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("chop").should == "function_chop" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_chop([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should chop the end of a string" do + result = scope.function_chop(["asdf\n"]) + result.should(eq("asdf")) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/delete_at_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/delete_at_spec.rb new file mode 100755 index 000000000..d8d961848 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/delete_at_spec.rb @@ -0,0 +1,19 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the delete_at function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("delete_at").should == "function_delete_at" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_delete_at([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should delete an item at specified location from an array" do + result = scope.function_delete_at([['a','b','c'],1]) + result.should(eq(['a','c'])) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/delete_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/delete_spec.rb new file mode 100755 index 000000000..0549232a5 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/delete_spec.rb @@ -0,0 +1,19 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the delete function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("delete").should == "function_delete" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_delete([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should delete an item from an array" do + result = scope.function_delete([['a','b','c'],'b']) + result.should(eq(['a','c'])) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/downcase_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/downcase_spec.rb new file mode 100755 index 000000000..acef1f05d --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/downcase_spec.rb @@ -0,0 +1,24 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the downcase function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("downcase").should == "function_downcase" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_downcase([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should downcase a string" do + result = scope.function_downcase(["ASFD"]) + result.should(eq("asfd")) + end + + it "should do nothing to a string that is already downcase" do + result = scope.function_downcase(["asdf asdf"]) + result.should(eq("asdf asdf")) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/empty_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/empty_spec.rb new file mode 100755 index 000000000..774587522 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/empty_spec.rb @@ -0,0 +1,23 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the empty function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + it "should exist" do + Puppet::Parser::Functions.function("empty").should == "function_empty" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_empty([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return a true for an empty string" do + result = scope.function_empty(['']) + result.should(eq(true)) + end + + it "should return a false for a non-empty string" do + result = scope.function_empty(['asdf']) + result.should(eq(false)) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/flatten_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/flatten_spec.rb new file mode 100755 index 000000000..d4dfd2018 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/flatten_spec.rb @@ -0,0 +1,23 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the flatten function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + it "should exist" do + Puppet::Parser::Functions.function("flatten").should == "function_flatten" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_flatten([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should flatten a complex data structure" do + result = scope.function_flatten([["a","b",["c",["d","e"],"f","g"]]]) + result.should(eq(["a","b","c","d","e","f","g"])) + end + + it "should do nothing to a structure that is already flat" do + result = scope.function_flatten([["a","b","c","d"]]) + result.should(eq(["a","b","c","d"])) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/fqdn_rotate_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/fqdn_rotate_spec.rb new file mode 100644 index 000000000..4eb799dc5 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/fqdn_rotate_spec.rb @@ -0,0 +1,33 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the fqdn_rotate function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("fqdn_rotate").should == "function_fqdn_rotate" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_fqdn_rotate([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should rotate a string and the result should be the same size" do + scope.expects(:lookupvar).with("::fqdn").returns("127.0.0.1") + result = scope.function_fqdn_rotate(["asdf"]) + result.size.should(eq(4)) + end + + it "should rotate a string to give the same results for one host" do + scope.expects(:lookupvar).with("::fqdn").returns("127.0.0.1").twice + scope.function_fqdn_rotate(["abcdefg"]).should eql(scope.function_fqdn_rotate(["abcdefg"])) + end + + it "should rotate a string to give different values on different hosts" do + scope.expects(:lookupvar).with("::fqdn").returns("127.0.0.1") + val1 = scope.function_fqdn_rotate(["abcdefghijklmnopqrstuvwxyz01234567890987654321"]) + scope.expects(:lookupvar).with("::fqdn").returns("127.0.0.2") + val2 = scope.function_fqdn_rotate(["abcdefghijklmnopqrstuvwxyz01234567890987654321"]) + val1.should_not eql(val2) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/get_module_path_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/get_module_path_spec.rb new file mode 100644 index 000000000..e761706c0 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/get_module_path_spec.rb @@ -0,0 +1,46 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe Puppet::Parser::Functions.function(:get_module_path) do + Internals = PuppetlabsSpec::PuppetInternals + class StubModule + attr_reader :path + def initialize(path) + @path = path + end + end + + def scope(environment = "production") + Internals.scope(:compiler => Internals.compiler(:node => Internals.node(:environment => environment))) + end + + it 'should only allow one argument' do + expect { scope.function_get_module_path([]) }.should raise_error(Puppet::ParseError, /Wrong number of arguments, expects one/) + expect { scope.function_get_module_path(['1','2','3']) }.should raise_error(Puppet::ParseError, /Wrong number of arguments, expects one/) + end + it 'should raise an exception when the module cannot be found' do + expect { scope.function_get_module_path(['foo']) }.should raise_error(Puppet::ParseError, /Could not find module/) + end + describe 'when locating a module' do + let(:modulepath) { "/tmp/does_not_exist" } + let(:path_of_module_foo) { StubModule.new("/tmp/does_not_exist/foo") } + + before(:each) { Puppet[:modulepath] = modulepath } + + it 'should be able to find module paths from the modulepath setting' do + Puppet::Module.expects(:find).with('foo', 'production').returns(path_of_module_foo) + scope.function_get_module_path(['foo']).should == path_of_module_foo.path + end + it 'should be able to find module paths when the modulepath is a list' do + Puppet[:modulepath] = modulepath + ":/tmp" + Puppet::Module.expects(:find).with('foo', 'production').returns(path_of_module_foo) + scope.function_get_module_path(['foo']).should == path_of_module_foo.path + end + it 'should respect the environment' do + pending("Disabled on Puppet 2.6.x") if Puppet.version =~ /^2\.6\b/ + Puppet.settings[:environment] = 'danstestenv' + Puppet::Module.expects(:find).with('foo', 'danstestenv').returns(path_of_module_foo) + scope('danstestenv').function_get_module_path(['foo']).should == path_of_module_foo.path + end + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/getvar_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/getvar_spec.rb new file mode 100644 index 000000000..5ff834ee7 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/getvar_spec.rb @@ -0,0 +1,37 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe Puppet::Parser::Functions.function(:getvar) do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + describe 'when calling getvar from puppet' do + + it "should not compile when no arguments are passed" do + pending("Fails on 2.6.x, see bug #15912") if Puppet.version =~ /^2\.6\./ + Puppet[:code] = '$foo = getvar()' + expect { + scope.compiler.compile + }.to raise_error(Puppet::ParseError, /wrong number of arguments/) + end + + it "should not compile when too many arguments are passed" do + pending("Fails on 2.6.x, see bug #15912") if Puppet.version =~ /^2\.6\./ + Puppet[:code] = '$foo = getvar("foo::bar", "baz")' + expect { + scope.compiler.compile + }.to raise_error(Puppet::ParseError, /wrong number of arguments/) + end + + it "should lookup variables in other namespaces" do + pending("Fails on 2.6.x, see bug #15912") if Puppet.version =~ /^2\.6\./ + Puppet[:code] = <<-'ENDofPUPPETcode' + class site::data { $foo = 'baz' } + include site::data + $foo = getvar("site::data::foo") + if $foo != 'baz' { + fail('getvar did not return what we expect') + } + ENDofPUPPETcode + scope.compiler.compile + end + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/grep_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/grep_spec.rb new file mode 100755 index 000000000..a93b84253 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/grep_spec.rb @@ -0,0 +1,19 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the grep function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("grep").should == "function_grep" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_grep([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should grep contents from an array" do + result = scope.function_grep([["aaabbb","bbbccc","dddeee"], "bbb"]) + result.should(eq(["aaabbb","bbbccc"])) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/has_key_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/has_key_spec.rb new file mode 100644 index 000000000..490daeae7 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/has_key_spec.rb @@ -0,0 +1,42 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe Puppet::Parser::Functions.function(:has_key) do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + describe 'when calling has_key from puppet' do + it "should not compile when no arguments are passed" do + pending("Fails on 2.6.x, see bug #15912") if Puppet.version =~ /^2\.6\./ + Puppet[:code] = '$x = has_key()' + expect { + scope.compiler.compile + }.to raise_error(Puppet::ParseError, /wrong number of arguments/) + end + + it "should not compile when 1 argument is passed" do + pending("Fails on 2.6.x, see bug #15912") if Puppet.version =~ /^2\.6\./ + Puppet[:code] = "$x = has_key('foo')" + expect { + scope.compiler.compile + }.to raise_error(Puppet::ParseError, /wrong number of arguments/) + end + + it "should require the first value to be a Hash" do + pending("Fails on 2.6.x, see bug #15912") if Puppet.version =~ /^2\.6\./ + Puppet[:code] = "$x = has_key('foo', 'bar')" + expect { + scope.compiler.compile + }.to raise_error(Puppet::ParseError, /expects the first argument to be a hash/) + end + end + + describe 'when calling the function has_key from a scope instance' do + it 'should detect existing keys' do + scope.function_has_key([{'one' => 1}, 'one']).should be_true + end + + it 'should detect existing keys' do + scope.function_has_key([{'one' => 1}, 'two']).should be_false + end + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/hash_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/hash_spec.rb new file mode 100644 index 000000000..7c91be907 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/hash_spec.rb @@ -0,0 +1,19 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the hash function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("hash").should == "function_hash" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_hash([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should convert an array to a hash" do + result = scope.function_hash([['a',1,'b',2,'c',3]]) + result.should(eq({'a'=>1,'b'=>2,'c'=>3})) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/is_array_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/is_array_spec.rb new file mode 100644 index 000000000..e7f4bcd6d --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/is_array_spec.rb @@ -0,0 +1,29 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the is_array function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("is_array").should == "function_is_array" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_is_array([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return true if passed an array" do + result = scope.function_is_array([[1,2,3]]) + result.should(eq(true)) + end + + it "should return false if passed a hash" do + result = scope.function_is_array([{'a'=>1}]) + result.should(eq(false)) + end + + it "should return false if passed a string" do + result = scope.function_is_array(["asdf"]) + result.should(eq(false)) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/is_domain_name_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/is_domain_name_spec.rb new file mode 100644 index 000000000..f2ea76dac --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/is_domain_name_spec.rb @@ -0,0 +1,64 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the is_domain_name function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("is_domain_name").should == "function_is_domain_name" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_is_domain_name([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return true if a valid short domain name" do + result = scope.function_is_domain_name(["x.com"]) + result.should(be_true) + end + + it "should return true if the domain is ." do + result = scope.function_is_domain_name(["."]) + result.should(be_true) + end + + it "should return true if the domain is x.com." do + result = scope.function_is_domain_name(["x.com."]) + result.should(be_true) + end + + it "should return true if a valid domain name" do + result = scope.function_is_domain_name(["foo.bar.com"]) + result.should(be_true) + end + + it "should allow domain parts to start with numbers" do + result = scope.function_is_domain_name(["3foo.2bar.com"]) + result.should(be_true) + end + + it "should allow domain to end with a dot" do + result = scope.function_is_domain_name(["3foo.2bar.com."]) + result.should(be_true) + end + + it "should allow a single part domain" do + result = scope.function_is_domain_name(["orange"]) + result.should(be_true) + end + + it "should return false if domain parts start with hyphens" do + result = scope.function_is_domain_name(["-3foo.2bar.com"]) + result.should(be_false) + end + + it "should return true if domain contains hyphens" do + result = scope.function_is_domain_name(["3foo-bar.2bar-fuzz.com"]) + result.should(be_true) + end + + it "should return false if domain name contains spaces" do + result = scope.function_is_domain_name(["not valid"]) + result.should(be_false) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/is_float_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/is_float_spec.rb new file mode 100644 index 000000000..2f527d932 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/is_float_spec.rb @@ -0,0 +1,29 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the is_float function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("is_float").should == "function_is_float" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_is_float([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return true if a float" do + result = scope.function_is_float(["0.12"]) + result.should(eq(true)) + end + + it "should return false if a string" do + result = scope.function_is_float(["asdf"]) + result.should(eq(false)) + end + + it "should return false if an integer" do + result = scope.function_is_float(["3"]) + result.should(eq(false)) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/is_hash_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/is_hash_spec.rb new file mode 100644 index 000000000..bbebf39f9 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/is_hash_spec.rb @@ -0,0 +1,29 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the is_hash function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("is_hash").should == "function_is_hash" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_is_hash([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return true if passed a hash" do + result = scope.function_is_hash([{"a"=>1,"b"=>2}]) + result.should(eq(true)) + end + + it "should return false if passed an array" do + result = scope.function_is_hash([["a","b"]]) + result.should(eq(false)) + end + + it "should return false if passed a string" do + result = scope.function_is_hash(["asdf"]) + result.should(eq(false)) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/is_integer_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/is_integer_spec.rb new file mode 100644 index 000000000..5afbba4a0 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/is_integer_spec.rb @@ -0,0 +1,29 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the is_integer function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("is_integer").should == "function_is_integer" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_is_integer([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return true if an integer" do + result = scope.function_is_integer(["3"]) + result.should(eq(true)) + end + + it "should return false if a float" do + result = scope.function_is_integer(["3.2"]) + result.should(eq(false)) + end + + it "should return false if a string" do + result = scope.function_is_integer(["asdf"]) + result.should(eq(false)) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/is_ip_address_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/is_ip_address_spec.rb new file mode 100644 index 000000000..c0debb3d4 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/is_ip_address_spec.rb @@ -0,0 +1,39 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the is_ip_address function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("is_ip_address").should == "function_is_ip_address" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_is_ip_address([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return true if an IPv4 address" do + result = scope.function_is_ip_address(["1.2.3.4"]) + result.should(eq(true)) + end + + it "should return true if a full IPv6 address" do + result = scope.function_is_ip_address(["fe80:0000:cd12:d123:e2f8:47ff:fe09:dd74"]) + result.should(eq(true)) + end + + it "should return true if a compressed IPv6 address" do + result = scope.function_is_ip_address(["fe00::1"]) + result.should(eq(true)) + end + + it "should return false if not valid" do + result = scope.function_is_ip_address(["asdf"]) + result.should(eq(false)) + end + + it "should return false if IP octets out of range" do + result = scope.function_is_ip_address(["1.1.1.300"]) + result.should(eq(false)) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/is_mac_address_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/is_mac_address_spec.rb new file mode 100644 index 000000000..ca9c59047 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/is_mac_address_spec.rb @@ -0,0 +1,29 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the is_mac_address function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("is_mac_address").should == "function_is_mac_address" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_is_mac_address([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return true if a valid mac address" do + result = scope.function_is_mac_address(["00:a0:1f:12:7f:a0"]) + result.should(eq(true)) + end + + it "should return false if octets are out of range" do + result = scope.function_is_mac_address(["00:a0:1f:12:7f:g0"]) + result.should(eq(false)) + end + + it "should return false if not valid" do + result = scope.function_is_mac_address(["not valid"]) + result.should(eq(false)) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/is_numeric_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/is_numeric_spec.rb new file mode 100644 index 000000000..4078b37f2 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/is_numeric_spec.rb @@ -0,0 +1,29 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the is_numeric function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("is_numeric").should == "function_is_numeric" + end + + it "should raise a ParseError if there is less than 1 argument" do + lambda { scope.function_is_numeric([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return true if an integer" do + result = scope.function_is_numeric(["3"]) + result.should(eq(true)) + end + + it "should return true if a float" do + result = scope.function_is_numeric(["3.2"]) + result.should(eq(true)) + end + + it "should return false if a string" do + result = scope.function_is_numeric(["asdf"]) + result.should(eq(false)) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/is_string_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/is_string_spec.rb new file mode 100644 index 000000000..3756bea8b --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/is_string_spec.rb @@ -0,0 +1,34 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the is_string function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("is_string").should == "function_is_string" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_is_string([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return true if a string" do + result = scope.function_is_string(["asdf"]) + result.should(eq(true)) + end + + it "should return false if an integer" do + result = scope.function_is_string(["3"]) + result.should(eq(false)) + end + + it "should return false if a float" do + result = scope.function_is_string(["3.23"]) + result.should(eq(false)) + end + + it "should return false if an array" do + result = scope.function_is_string([["a","b","c"]]) + result.should(eq(false)) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/join_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/join_spec.rb new file mode 100644 index 000000000..aafa1a7f7 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/join_spec.rb @@ -0,0 +1,19 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the join function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("join").should == "function_join" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_join([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should join an array into a string" do + result = scope.function_join([["a","b","c"], ":"]) + result.should(eq("a:b:c")) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/keys_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/keys_spec.rb new file mode 100644 index 000000000..fdd7a7073 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/keys_spec.rb @@ -0,0 +1,21 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the keys function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("keys").should == "function_keys" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_keys([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return an array of keys when given a hash" do + result = scope.function_keys([{'a'=>1, 'b'=>2}]) + # =~ performs 'array with same elements' (set) matching + # For more info see RSpec::Matchers::MatchArray + result.should =~ ['a','b'] + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/lstrip_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/lstrip_spec.rb new file mode 100644 index 000000000..b280ae7ac --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/lstrip_spec.rb @@ -0,0 +1,19 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the lstrip function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("lstrip").should == "function_lstrip" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_lstrip([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should lstrip a string" do + result = scope.function_lstrip([" asdf"]) + result.should(eq('asdf')) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/member_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/member_spec.rb new file mode 100644 index 000000000..6e9a023fa --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/member_spec.rb @@ -0,0 +1,24 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the member function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("member").should == "function_member" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_member([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return true if a member is in an array" do + result = scope.function_member([["a","b","c"], "a"]) + result.should(eq(true)) + end + + it "should return false if a member is not in an array" do + result = scope.function_member([["a","b","c"], "d"]) + result.should(eq(false)) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/merge_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/merge_spec.rb new file mode 100644 index 000000000..db7d837be --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/merge_spec.rb @@ -0,0 +1,47 @@ +#! /usr/bin/env ruby -S rspec + +require 'spec_helper' + +describe Puppet::Parser::Functions.function(:merge) do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + describe 'when calling merge from puppet' do + it "should not compile when no arguments are passed" do + pending("Fails on 2.6.x, see bug #15912") if Puppet.version =~ /^2\.6\./ + Puppet[:code] = '$x = merge()' + expect { + scope.compiler.compile + }.to raise_error(Puppet::ParseError, /wrong number of arguments/) + end + + it "should not compile when 1 argument is passed" do + pending("Fails on 2.6.x, see bug #15912") if Puppet.version =~ /^2\.6\./ + Puppet[:code] = "$my_hash={'one' => 1}\n$x = merge($my_hash)" + expect { + scope.compiler.compile + }.to raise_error(Puppet::ParseError, /wrong number of arguments/) + end + end + + describe 'when calling merge on the scope instance' do + it 'should require all parameters are hashes' do + expect { new_hash = scope.function_merge([{}, '2'])}.should raise_error(Puppet::ParseError, /unexpected argument type String/) + end + + it 'should be able to merge two hashes' do + new_hash = scope.function_merge([{'one' => '1', 'two' => '1'}, {'two' => '2', 'three' => '2'}]) + new_hash['one'].should == '1' + new_hash['two'].should == '2' + new_hash['three'].should == '2' + end + + it 'should merge multiple hashes' do + hash = scope.function_merge([{'one' => 1}, {'one' => '2'}, {'one' => '3'}]) + hash['one'].should == '3' + end + + it 'should accept empty hashes' do + scope.function_merge([{},{},{}]).should == {} + end + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/num2bool_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/num2bool_spec.rb new file mode 100644 index 000000000..640c68985 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/num2bool_spec.rb @@ -0,0 +1,24 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the num2bool function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("num2bool").should == "function_num2bool" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_num2bool([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return true if 1" do + result = scope.function_num2bool(["1"]) + result.should(be_true) + end + + it "should return false if 0" do + result = scope.function_num2bool(["0"]) + result.should(be_false) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/parsejson_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/parsejson_spec.rb new file mode 100644 index 000000000..f179ac111 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/parsejson_spec.rb @@ -0,0 +1,22 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the parsejson function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("parsejson").should == "function_parsejson" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_parsejson([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should convert JSON to a data structure" do + json = <<-EOS +["aaa","bbb","ccc"] +EOS + result = scope.function_parsejson([json]) + result.should(eq(['aaa','bbb','ccc'])) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/parseyaml_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/parseyaml_spec.rb new file mode 100644 index 000000000..0c7aea8a5 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/parseyaml_spec.rb @@ -0,0 +1,24 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the parseyaml function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("parseyaml").should == "function_parseyaml" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_parseyaml([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should convert YAML to a data structure" do + yaml = <<-EOS +- aaa +- bbb +- ccc +EOS + result = scope.function_parseyaml([yaml]) + result.should(eq(['aaa','bbb','ccc'])) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/prefix_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/prefix_spec.rb new file mode 100644 index 000000000..5cf592bfb --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/prefix_spec.rb @@ -0,0 +1,19 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the prefix function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("prefix").should == "function_prefix" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_prefix([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return a prefixed array" do + result = scope.function_prefix([['a','b','c'], 'p']) + result.should(eq(['pa','pb','pc'])) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/range_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/range_spec.rb new file mode 100644 index 000000000..42751f460 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/range_spec.rb @@ -0,0 +1,34 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the range function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("range").should == "function_range" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_range([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return a letter range" do + result = scope.function_range(["a","d"]) + result.should(eq(['a','b','c','d'])) + end + + it "should return a number range" do + result = scope.function_range(["1","4"]) + result.should(eq([1,2,3,4])) + end + + it "should work with padded hostname like strings" do + expected = ("host01".."host10").to_a + scope.function_range(["host01","host10"]).should eq expected + end + + it "should coerce zero padded digits to integers" do + expected = (0..10).to_a + scope.function_range(["00", "10"]).should eq expected + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/reverse_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/reverse_spec.rb new file mode 100644 index 000000000..1b5920654 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/reverse_spec.rb @@ -0,0 +1,19 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the reverse function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("reverse").should == "function_reverse" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_reverse([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should reverse a string" do + result = scope.function_reverse(["asdfghijkl"]) + result.should(eq('lkjihgfdsa')) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/rstrip_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/rstrip_spec.rb new file mode 100644 index 000000000..d90de1d06 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/rstrip_spec.rb @@ -0,0 +1,24 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the rstrip function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("rstrip").should == "function_rstrip" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_rstrip([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should rstrip a string" do + result = scope.function_rstrip(["asdf "]) + result.should(eq('asdf')) + end + + it "should rstrip each element in an array" do + result = scope.function_rstrip([["a ","b ", "c "]]) + result.should(eq(['a','b','c'])) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/shuffle_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/shuffle_spec.rb new file mode 100644 index 000000000..93346d537 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/shuffle_spec.rb @@ -0,0 +1,24 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the shuffle function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("shuffle").should == "function_shuffle" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_shuffle([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should shuffle a string and the result should be the same size" do + result = scope.function_shuffle(["asdf"]) + result.size.should(eq(4)) + end + + it "should shuffle a string but the sorted contents should still be the same" do + result = scope.function_shuffle(["adfs"]) + result.split("").sort.join("").should(eq("adfs")) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/size_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/size_spec.rb new file mode 100644 index 000000000..b1c435a30 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/size_spec.rb @@ -0,0 +1,24 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the size function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("size").should == "function_size" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_size([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return the size of a string" do + result = scope.function_size(["asdf"]) + result.should(eq(4)) + end + + it "should return the size of an array" do + result = scope.function_size([["a","b","c"]]) + result.should(eq(3)) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/sort_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/sort_spec.rb new file mode 100644 index 000000000..3187a5aec --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/sort_spec.rb @@ -0,0 +1,24 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the sort function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("sort").should == "function_sort" + end + + it "should raise a ParseError if there is not 1 arguments" do + lambda { scope.function_sort(['','']) }.should( raise_error(Puppet::ParseError)) + end + + it "should sort an array" do + result = scope.function_sort([["a","c","b"]]) + result.should(eq(['a','b','c'])) + end + + it "should sort a string" do + result = scope.function_sort(["acb"]) + result.should(eq('abc')) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/squeeze_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/squeeze_spec.rb new file mode 100644 index 000000000..60e5a3028 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/squeeze_spec.rb @@ -0,0 +1,24 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the squeeze function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("squeeze").should == "function_squeeze" + end + + it "should raise a ParseError if there is less than 2 arguments" do + lambda { scope.function_squeeze([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should squeeze a string" do + result = scope.function_squeeze(["aaabbbbcccc"]) + result.should(eq('abc')) + end + + it "should squeeze all elements in an array" do + result = scope.function_squeeze([["aaabbbbcccc","dddfff"]]) + result.should(eq(['abc','df'])) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/str2bool_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/str2bool_spec.rb new file mode 100644 index 000000000..2782bbea8 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/str2bool_spec.rb @@ -0,0 +1,24 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the str2bool function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("str2bool").should == "function_str2bool" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_str2bool([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should convert string 'true' to true" do + result = scope.function_str2bool(["true"]) + result.should(eq(true)) + end + + it "should convert string 'undef' to false" do + result = scope.function_str2bool(["undef"]) + result.should(eq(false)) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/str2saltedsha512_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/str2saltedsha512_spec.rb new file mode 100644 index 000000000..a692c3133 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/str2saltedsha512_spec.rb @@ -0,0 +1,45 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the str2saltedsha512 function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("str2saltedsha512").should == "function_str2saltedsha512" + end + + it "should raise a ParseError if there is less than 1 argument" do + expect { scope.function_str2saltedsha512([]) }.should( raise_error(Puppet::ParseError) ) + end + + it "should raise a ParseError if there is more than 1 argument" do + expect { scope.function_str2saltedsha512(['foo', 'bar', 'baz']) }.should( raise_error(Puppet::ParseError) ) + end + + it "should return a salted-sha512 password hash 136 characters in length" do + result = scope.function_str2saltedsha512(["password"]) + result.length.should(eq(136)) + end + + it "should raise an error if you pass a non-string password" do + expect { scope.function_str2saltedsha512([1234]) }.should( raise_error(Puppet::ParseError) ) + end + + it "should generate a valid password" do + # Allow the function to generate a password based on the string 'password' + password_hash = scope.function_str2saltedsha512(["password"]) + + # Separate the Salt and Password from the Password Hash + salt = password_hash[0..7] + password = password_hash[8..-1] + + # Convert the Salt and Password from Hex to Binary Data + str_salt = Array(salt.lines).pack('H*') + str_password = Array(password.lines).pack('H*') + + # Combine the Binary Salt with 'password' and compare the end result + saltedpass = Digest::SHA512.digest(str_salt + 'password') + result = (str_salt + saltedpass).unpack('H*')[0] + result.should == password_hash + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/strftime_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/strftime_spec.rb new file mode 100644 index 000000000..df42b6f26 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/strftime_spec.rb @@ -0,0 +1,29 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the strftime function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("strftime").should == "function_strftime" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_strftime([]) }.should( raise_error(Puppet::ParseError)) + end + + it "using %s should be higher then when I wrote this test" do + result = scope.function_strftime(["%s"]) + result.to_i.should(be > 1311953157) + end + + it "using %s should be lower then 1.5 trillion" do + result = scope.function_strftime(["%s"]) + result.to_i.should(be < 1500000000) + end + + it "should return a date when given %Y-%m-%d" do + result = scope.function_strftime(["%Y-%m-%d"]) + result.should =~ /^\d{4}-\d{2}-\d{2}$/ + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/strip_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/strip_spec.rb new file mode 100644 index 000000000..fccdd2606 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/strip_spec.rb @@ -0,0 +1,18 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the strip function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + it "should exist" do + Puppet::Parser::Functions.function("strip").should == "function_strip" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_strip([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should strip a string" do + result = scope.function_strip([" ab cd "]) + result.should(eq('ab cd')) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/swapcase_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/swapcase_spec.rb new file mode 100644 index 000000000..808b41587 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/swapcase_spec.rb @@ -0,0 +1,19 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the swapcase function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("swapcase").should == "function_swapcase" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_swapcase([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should swapcase a string" do + result = scope.function_swapcase(["aaBBccDD"]) + result.should(eq('AAbbCCdd')) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/time_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/time_spec.rb new file mode 100644 index 000000000..e9fb76e6a --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/time_spec.rb @@ -0,0 +1,29 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the time function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("time").should == "function_time" + end + + it "should raise a ParseError if there is more than 2 arguments" do + lambda { scope.function_time(['','']) }.should( raise_error(Puppet::ParseError)) + end + + it "should return a number" do + result = scope.function_time([]) + result.should be_an(Integer) + end + + it "should be higher then when I wrote this test" do + result = scope.function_time([]) + result.should(be > 1311953157) + end + + it "should be lower then 1.5 trillion" do + result = scope.function_time([]) + result.should(be < 1500000000) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/to_bytes_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/to_bytes_spec.rb new file mode 100755 index 000000000..d1ea4c80c --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/to_bytes_spec.rb @@ -0,0 +1,58 @@ +#! /usr/bin/env ruby -S rspec + +require 'spec_helper' + +describe "the to_bytes function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("to_bytes").should == "function_to_bytes" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_to_bytes([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should convert kB to B" do + result = scope.function_to_bytes(["4 kB"]) + result.should(eq(4096)) + end + + it "should work without B in unit" do + result = scope.function_to_bytes(["4 k"]) + result.should(eq(4096)) + end + + it "should work without a space before unit" do + result = scope.function_to_bytes(["4k"]) + result.should(eq(4096)) + end + + it "should work without a unit" do + result = scope.function_to_bytes(["5678"]) + result.should(eq(5678)) + end + + it "should convert fractions" do + result = scope.function_to_bytes(["1.5 kB"]) + result.should(eq(1536)) + end + + it "should convert scientific notation" do + result = scope.function_to_bytes(["1.5e2 B"]) + result.should(eq(150)) + end + + it "should do nothing with a positive number" do + result = scope.function_to_bytes([5678]) + result.should(eq(5678)) + end + + it "should should raise a ParseError if input isn't a number" do + lambda { scope.function_to_bytes(["foo"]) }.should( raise_error(Puppet::ParseError)) + end + + it "should should raise a ParseError if prefix is unknown" do + lambda { scope.function_to_bytes(["5 uB"]) }.should( raise_error(Puppet::ParseError)) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/type_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/type_spec.rb new file mode 100644 index 000000000..8fec88f26 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/type_spec.rb @@ -0,0 +1,43 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the type function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + it "should exist" do + Puppet::Parser::Functions.function("type").should == "function_type" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_type([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return string when given a string" do + result = scope.function_type(["aaabbbbcccc"]) + result.should(eq('string')) + end + + it "should return array when given an array" do + result = scope.function_type([["aaabbbbcccc","asdf"]]) + result.should(eq('array')) + end + + it "should return hash when given a hash" do + result = scope.function_type([{"a"=>1,"b"=>2}]) + result.should(eq('hash')) + end + + it "should return integer when given an integer" do + result = scope.function_type(["1"]) + result.should(eq('integer')) + end + + it "should return float when given a float" do + result = scope.function_type(["1.34"]) + result.should(eq('float')) + end + + it "should return boolean when given a boolean" do + result = scope.function_type([true]) + result.should(eq('boolean')) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/unique_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/unique_spec.rb new file mode 100644 index 000000000..5d48d49b7 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/unique_spec.rb @@ -0,0 +1,24 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the unique function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("unique").should == "function_unique" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_unique([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should remove duplicate elements in a string" do + result = scope.function_unique(["aabbc"]) + result.should(eq('abc')) + end + + it "should remove duplicate elements in an array" do + result = scope.function_unique([["a","a","b","b","c"]]) + result.should(eq(['a','b','c'])) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/upcase_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/upcase_spec.rb new file mode 100644 index 000000000..5db55138a --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/upcase_spec.rb @@ -0,0 +1,24 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the upcase function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("upcase").should == "function_upcase" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_upcase([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should upcase a string" do + result = scope.function_upcase(["abc"]) + result.should(eq('ABC')) + end + + it "should do nothing if a string is already upcase" do + result = scope.function_upcase(["ABC"]) + result.should(eq('ABC')) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/validate_absolute_path_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/validate_absolute_path_spec.rb new file mode 100644 index 000000000..08aaf7899 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/validate_absolute_path_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe Puppet::Parser::Functions.function(:validate_absolute_path) do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + # The subject of these examples is the method itself. + subject do + # This makes sure the function is loaded within each test + function_name = Puppet::Parser::Functions.function(:validate_absolute_path) + scope.method(function_name) + end + + describe "Valid Paths" do + def self.valid_paths + %w{ + C:/ + C:\\ + C:\\WINDOWS\\System32 + C:/windows/system32 + X:/foo/bar + X:\\foo\\bar + /var/tmp + /var/lib/puppet + /var/opt/../lib/puppet + } + end + + context "Without Puppet::Util.absolute_path? (e.g. Puppet <= 2.6)" do + before :each do + # The intent here is to mock Puppet to behave like Puppet 2.6 does. + # Puppet 2.6 does not have the absolute_path? method. This is only a + # convenience test, stdlib should be run with the Puppet 2.6.x in the + # $LOAD_PATH in addition to 2.7.x and master. + Puppet::Util.expects(:respond_to?).with(:absolute_path?).returns(false) + end + valid_paths.each do |path| + it "validate_absolute_path(#{path.inspect}) should not fail" do + expect { subject.call [path] }.not_to raise_error Puppet::ParseError + end + end + end + + context "Puppet without mocking" do + valid_paths.each do |path| + it "validate_absolute_path(#{path.inspect}) should not fail" do + expect { subject.call [path] }.not_to raise_error Puppet::ParseError + end + end + end + end + + describe 'Invalid paths' do + context 'Garbage inputs' do + [ + nil, + [ nil ], + { 'foo' => 'bar' }, + { }, + '', + ].each do |path| + it "validate_absolute_path(#{path.inspect}) should fail" do + expect { subject.call [path] }.to raise_error Puppet::ParseError + end + end + end + + context 'Relative paths' do + %w{ + relative1 + . + .. + ./foo + ../foo + etc/puppetlabs/puppet + opt/puppet/bin + }.each do |path| + it "validate_absolute_path(#{path.inspect}) should fail" do + expect { subject.call [path] }.to raise_error Puppet::ParseError + end + end + end + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/validate_array_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/validate_array_spec.rb new file mode 100644 index 000000000..8eee72abb --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/validate_array_spec.rb @@ -0,0 +1,38 @@ +#! /usr/bin/env ruby -S rspec + +require 'spec_helper' + +describe Puppet::Parser::Functions.function(:validate_array) do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + describe 'when calling validate_array from puppet' do + + %w{ true false }.each do |the_string| + it "should not compile when #{the_string} is a string" do + Puppet[:code] = "validate_array('#{the_string}')" + expect { scope.compiler.compile }.should raise_error(Puppet::ParseError, /is not an Array/) + end + + it "should not compile when #{the_string} is a bare word" do + Puppet[:code] = "validate_array(#{the_string})" + expect { scope.compiler.compile }.should raise_error(Puppet::ParseError, /is not an Array/) + end + end + + it "should compile when multiple array arguments are passed" do + Puppet[:code] = <<-'ENDofPUPPETcode' + $foo = [ ] + $bar = [ 'one', 'two' ] + validate_array($foo, $bar) + ENDofPUPPETcode + scope.compiler.compile + end + + it "should not compile when an undef variable is passed" do + Puppet[:code] = <<-'ENDofPUPPETcode' + $foo = undef + validate_array($foo) + ENDofPUPPETcode + expect { scope.compiler.compile }.should raise_error(Puppet::ParseError, /is not an Array/) + end + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/validate_bool_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/validate_bool_spec.rb new file mode 100644 index 000000000..31ab8fb72 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/validate_bool_spec.rb @@ -0,0 +1,51 @@ +#! /usr/bin/env/ruby -S rspec + +require 'spec_helper' + +describe Puppet::Parser::Functions.function(:validate_bool) do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + describe 'when calling validate_bool from puppet' do + + %w{ true false }.each do |the_string| + + it "should not compile when #{the_string} is a string" do + Puppet[:code] = "validate_bool('#{the_string}')" + expect { scope.compiler.compile }.should raise_error(Puppet::ParseError, /is not a boolean/) + end + + it "should compile when #{the_string} is a bare word" do + Puppet[:code] = "validate_bool(#{the_string})" + scope.compiler.compile + end + + end + + it "should not compile when an arbitrary string is passed" do + Puppet[:code] = 'validate_bool("jeff and dan are awesome")' + expect { scope.compiler.compile }.should raise_error(Puppet::ParseError, /is not a boolean/) + end + + it "should not compile when no arguments are passed" do + Puppet[:code] = 'validate_bool()' + expect { scope.compiler.compile }.should raise_error(Puppet::ParseError, /wrong number of arguments/) + end + + it "should compile when multiple boolean arguments are passed" do + Puppet[:code] = <<-'ENDofPUPPETcode' + $foo = true + $bar = false + validate_bool($foo, $bar, true, false) + ENDofPUPPETcode + scope.compiler.compile + end + + it "should compile when multiple boolean arguments are passed" do + Puppet[:code] = <<-'ENDofPUPPETcode' + $foo = true + $bar = false + validate_bool($foo, $bar, true, false, 'jeff') + ENDofPUPPETcode + expect { scope.compiler.compile }.should raise_error(Puppet::ParseError, /is not a boolean/) + end + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/validate_hash_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/validate_hash_spec.rb new file mode 100644 index 000000000..f63db1dc2 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/validate_hash_spec.rb @@ -0,0 +1,44 @@ +#! /usr/bin/env ruby -S rspec + +require 'spec_helper' + +describe Puppet::Parser::Functions.function(:validate_hash) do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + describe 'when calling validate_hash from puppet' do + + %w{ true false }.each do |the_string| + + it "should not compile when #{the_string} is a string" do + Puppet[:code] = "validate_hash('#{the_string}')" + expect { scope.compiler.compile }.should raise_error(Puppet::ParseError, /is not a Hash/) + end + + it "should not compile when #{the_string} is a bare word" do + Puppet[:code] = "validate_hash(#{the_string})" + expect { scope.compiler.compile }.should raise_error(Puppet::ParseError, /is not a Hash/) + end + + end + + it "should compile when multiple hash arguments are passed" do + Puppet[:code] = <<-'ENDofPUPPETcode' + $foo = {} + $bar = { 'one' => 'two' } + validate_hash($foo, $bar) + ENDofPUPPETcode + scope.compiler.compile + end + + it "should not compile when an undef variable is passed" do + Puppet[:code] = <<-'ENDofPUPPETcode' + $foo = undef + validate_hash($foo) + ENDofPUPPETcode + expect { scope.compiler.compile }.should raise_error(Puppet::ParseError, /is not a Hash/) + end + + end + +end + diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/validate_re_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/validate_re_spec.rb new file mode 100644 index 000000000..d189efb66 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/validate_re_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +describe Puppet::Parser::Functions.function(:validate_re) do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + # The subject of these examplres is the method itself. + subject do + # This makes sure the function is loaded within each test + function_name = Puppet::Parser::Functions.function(:validate_re) + scope.method(function_name) + end + + context 'Using Puppet::Parser::Scope.new' do + + describe 'Garbage inputs' do + inputs = [ + [ nil ], + [ [ nil ] ], + [ { 'foo' => 'bar' } ], + [ { } ], + [ '' ], + [ "one", "one", "MSG to User", "4th arg" ], + ] + + inputs.each do |input| + it "validate_re(#{input.inspect}) should fail" do + expect { subject.call [input] }.to raise_error Puppet::ParseError + end + end + end + + describe 'Valid inputs' do + inputs = [ + [ '/full/path/to/something', '^/full' ], + [ '/full/path/to/something', 'full' ], + [ '/full/path/to/something', ['full', 'absent'] ], + [ '/full/path/to/something', ['full', 'absent'], 'Message to the user' ], + ] + + inputs.each do |input| + it "validate_re(#{input.inspect}) should not fail" do + expect { subject.call input }.not_to raise_error + end + end + end + describe "Valid inputs which should raise an exception without a message" do + # The intent here is to make sure valid inputs raise exceptions when they + # don't specify an error message to display. This is the behvior in + # 2.2.x and prior. + inputs = [ + [ "hello", [ "bye", "later", "adios" ] ], + [ "greetings", "salutations" ], + ] + + inputs.each do |input| + it "validate_re(#{input.inspect}) should fail" do + expect { subject.call input }.to raise_error /validate_re.*?does not match/ + end + end + end + describe "Nicer Error Messages" do + # The intent here is to make sure the function returns the 3rd argument + # in the exception thrown + inputs = [ + [ "hello", [ "bye", "later", "adios" ], "MSG to User" ], + [ "greetings", "salutations", "Error, greetings does not match salutations" ], + ] + + inputs.each do |input| + it "validate_re(#{input.inspect}) should fail" do + expect { subject.call input }.to raise_error /#{input[2]}/ + end + end + end + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/validate_slength_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/validate_slength_spec.rb new file mode 100755 index 000000000..eccf908de --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/validate_slength_spec.rb @@ -0,0 +1,48 @@ +#! /usr/bin/env ruby -S rspec + +require 'spec_helper' + +describe "the validate_slength function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("validate_slength").should == "function_validate_slength" + end + + it "should raise a ParseError if there is less than 2 arguments" do + expect { scope.function_validate_slength([]) }.to(raise_error(Puppet::ParseError)) + expect { scope.function_validate_slength(["asdf"]) }.to(raise_error(Puppet::ParseError)) + end + + it "should raise a ParseError if argument 2 doesn't convert to a fixnum" do + expect { scope.function_validate_slength(["moo",["2"]]) }.to(raise_error(Puppet::ParseError, /Couldn't convert whatever you passed/)) + end + + it "should raise a ParseError if argument 2 converted, but to 0, e.g. a string" do + expect { scope.function_validate_slength(["moo","monkey"]) }.to(raise_error(Puppet::ParseError, /please pass a positive number as max_length/)) + end + + it "should raise a ParseError if argument 2 converted, but to 0" do + expect { scope.function_validate_slength(["moo","0"]) }.to(raise_error(Puppet::ParseError, /please pass a positive number as max_length/)) + end + + it "should fail if string greater then size" do + expect { scope.function_validate_slength(["test", 2]) }.to(raise_error(Puppet::ParseError, /It should have been less than or equal to/)) + end + + it "should fail if you pass an array of something other than strings" do + expect { scope.function_validate_slength([["moo",["moo"],Hash.new["moo" => 7]], 7]) }.to(raise_error(Puppet::ParseError, /is not a string, it's a/)) + end + + it "should fail if you pass something other than a string or array" do + expect { scope.function_validate_slength([Hash.new["moo" => "7"],6]) }.to(raise_error(Puppet::ParseError), /please pass a string, or an array of strings/) + end + + it "should not fail if string is smaller or equal to size" do + expect { scope.function_validate_slength(["test", 5]) }.to_not(raise_error(Puppet::ParseError)) + end + + it "should not fail if array of string is are all smaller or equal to size" do + expect { scope.function_validate_slength([["moo","foo","bar"], 5]) }.to_not(raise_error(Puppet::ParseError)) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/validate_string_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/validate_string_spec.rb new file mode 100644 index 000000000..f40bf2a20 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/validate_string_spec.rb @@ -0,0 +1,61 @@ +#! /usr/bin/env ruby -S rspec + +require 'spec_helper' + +describe Puppet::Parser::Functions.function(:validate_string) do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + describe 'when calling validate_string from puppet' do + + %w{ foo bar baz }.each do |the_string| + + it "should compile when #{the_string} is a string" do + Puppet[:code] = "validate_string('#{the_string}')" + scope.compiler.compile + end + + it "should compile when #{the_string} is a bare word" do + Puppet[:code] = "validate_string(#{the_string})" + scope.compiler.compile + end + + end + + %w{ true false }.each do |the_string| + it "should compile when #{the_string} is a string" do + Puppet[:code] = "validate_string('#{the_string}')" + scope.compiler.compile + end + + it "should not compile when #{the_string} is a bare word" do + Puppet[:code] = "validate_string(#{the_string})" + expect { scope.compiler.compile }.should raise_error(Puppet::ParseError, /is not a string/) + end + end + + it "should compile when multiple string arguments are passed" do + Puppet[:code] = <<-'ENDofPUPPETcode' + $foo = '' + $bar = 'two' + validate_string($foo, $bar) + ENDofPUPPETcode + scope.compiler.compile + end + + it "should compile when an explicitly undef variable is passed (NOTE THIS MAY NOT BE DESIRABLE)" do + Puppet[:code] = <<-'ENDofPUPPETcode' + $foo = undef + validate_string($foo) + ENDofPUPPETcode + scope.compiler.compile + end + + it "should compile when an undefined variable is passed (NOTE THIS MAY NOT BE DESIRABLE)" do + Puppet[:code] = <<-'ENDofPUPPETcode' + validate_string($foobarbazishouldnotexist) + ENDofPUPPETcode + scope.compiler.compile + end + end +end + diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/values_at_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/values_at_spec.rb new file mode 100644 index 000000000..08e95a567 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/values_at_spec.rb @@ -0,0 +1,38 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the values_at function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("values_at").should == "function_values_at" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_values_at([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should raise a ParseError if you try to use a range where stop is greater then start" do + lambda { scope.function_values_at([['a','b'],["3-1"]]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return a value at from an array" do + result = scope.function_values_at([['a','b','c'],"1"]) + result.should(eq(['b'])) + end + + it "should return a value at from an array when passed a range" do + result = scope.function_values_at([['a','b','c'],"0-1"]) + result.should(eq(['a','b'])) + end + + it "should return chosen values from an array when passed number of indexes" do + result = scope.function_values_at([['a','b','c'],["0","2"]]) + result.should(eq(['a','c'])) + end + + it "should return chosen values from an array when passed ranges and multiple indexes" do + result = scope.function_values_at([['a','b','c','d','e','f','g'],["0","2","4-5"]]) + result.should(eq(['a','c','e','f'])) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/values_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/values_spec.rb new file mode 100644 index 000000000..14ae41763 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/values_spec.rb @@ -0,0 +1,31 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the values function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + Puppet::Parser::Functions.function("values").should == "function_values" + end + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_values([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should return values from a hash" do + result = scope.function_values([{'a'=>'1','b'=>'2','c'=>'3'}]) + # =~ is the RSpec::Matchers::MatchArray matcher. + # A.K.A. "array with same elements" (multiset) matching + result.should =~ %w{ 1 2 3 } + end + + it "should return a multiset" do + result = scope.function_values([{'a'=>'1','b'=>'3','c'=>'3'}]) + result.should =~ %w{ 1 3 3 } + result.should_not =~ %w{ 1 3 } + end + + it "should raise a ParseError unless a Hash is provided" do + lambda { scope.function_values([['a','b','c']]) }.should( raise_error(Puppet::ParseError)) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/parser/functions/zip_spec.rb b/puppet/stdlib/spec/unit/puppet/parser/functions/zip_spec.rb new file mode 100644 index 000000000..f45ab1730 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/parser/functions/zip_spec.rb @@ -0,0 +1,15 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the zip function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should raise a ParseError if there is less than 1 arguments" do + lambda { scope.function_zip([]) }.should( raise_error(Puppet::ParseError)) + end + + it "should be able to zip an array" do + result = scope.function_zip([['1','2','3'],['4','5','6']]) + result.should(eq([["1", "4"], ["2", "5"], ["3", "6"]])) + end +end diff --git a/puppet/stdlib/spec/unit/puppet/provider/file_line/ruby_spec.rb b/puppet/stdlib/spec/unit/puppet/provider/file_line/ruby_spec.rb new file mode 100644 index 000000000..7857d399f --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/provider/file_line/ruby_spec.rb @@ -0,0 +1,127 @@ +require 'puppet' +require 'tempfile' +provider_class = Puppet::Type.type(:file_line).provider(:ruby) +describe provider_class do + context "when adding" do + before :each do + # TODO: these should be ported over to use the PuppetLabs spec_helper + # file fixtures once the following pull request has been merged: + # https://github.com/puppetlabs/puppetlabs-stdlib/pull/73/files + tmp = Tempfile.new('tmp') + @tmpfile = tmp.path + tmp.close! + @resource = Puppet::Type::File_line.new( + {:name => 'foo', :path => @tmpfile, :line => 'foo'} + ) + @provider = provider_class.new(@resource) + end + it 'should detect if the line exists in the file' do + File.open(@tmpfile, 'w') do |fh| + fh.write('foo') + end + @provider.exists?.should be_true + end + it 'should detect if the line does not exist in the file' do + File.open(@tmpfile, 'w') do |fh| + fh.write('foo1') + end + @provider.exists?.should be_nil + end + it 'should append to an existing file when creating' do + @provider.create + File.read(@tmpfile).chomp.should == 'foo' + end + end + + context "when matching" do + before :each do + # TODO: these should be ported over to use the PuppetLabs spec_helper + # file fixtures once the following pull request has been merged: + # https://github.com/puppetlabs/puppetlabs-stdlib/pull/73/files + tmp = Tempfile.new('tmp') + @tmpfile = tmp.path + tmp.close! + @resource = Puppet::Type::File_line.new( + { + :name => 'foo', + :path => @tmpfile, + :line => 'foo = bar', + :match => '^foo\s*=.*$', + } + ) + @provider = provider_class.new(@resource) + end + + it 'should raise an error if more than one line matches, and should not have modified the file' do + File.open(@tmpfile, 'w') do |fh| + fh.write("foo1\nfoo=blah\nfoo2\nfoo=baz") + end + @provider.exists?.should be_nil + expect { @provider.create }.to raise_error(Puppet::Error, /More than one line.*matches/) + File.read(@tmpfile).should eql("foo1\nfoo=blah\nfoo2\nfoo=baz") + end + + it 'should replace a line that matches' do + File.open(@tmpfile, 'w') do |fh| + fh.write("foo1\nfoo=blah\nfoo2") + end + @provider.exists?.should be_nil + @provider.create + File.read(@tmpfile).chomp.should eql("foo1\nfoo = bar\nfoo2") + end + it 'should add a new line if no lines match' do + File.open(@tmpfile, 'w') do |fh| + fh.write("foo1\nfoo2") + end + @provider.exists?.should be_nil + @provider.create + File.read(@tmpfile).should eql("foo1\nfoo2\nfoo = bar\n") + end + it 'should do nothing if the exact line already exists' do + File.open(@tmpfile, 'w') do |fh| + fh.write("foo1\nfoo = bar\nfoo2") + end + @provider.exists?.should be_true + @provider.create + File.read(@tmpfile).chomp.should eql("foo1\nfoo = bar\nfoo2") + end + end + + context "when removing" do + before :each do + # TODO: these should be ported over to use the PuppetLabs spec_helper + # file fixtures once the following pull request has been merged: + # https://github.com/puppetlabs/puppetlabs-stdlib/pull/73/files + tmp = Tempfile.new('tmp') + @tmpfile = tmp.path + tmp.close! + @resource = Puppet::Type::File_line.new( + {:name => 'foo', :path => @tmpfile, :line => 'foo', :ensure => 'absent' } + ) + @provider = provider_class.new(@resource) + end + it 'should remove the line if it exists' do + File.open(@tmpfile, 'w') do |fh| + fh.write("foo1\nfoo\nfoo2") + end + @provider.destroy + File.read(@tmpfile).should eql("foo1\nfoo2") + end + + it 'should remove the line without touching the last new line' do + File.open(@tmpfile, 'w') do |fh| + fh.write("foo1\nfoo\nfoo2\n") + end + @provider.destroy + File.read(@tmpfile).should eql("foo1\nfoo2\n") + end + + it 'should remove any occurence of the line' do + File.open(@tmpfile, 'w') do |fh| + fh.write("foo1\nfoo\nfoo2\nfoo\nfoo") + end + @provider.destroy + File.read(@tmpfile).should eql("foo1\nfoo2\n") + end + end +end diff --git a/puppet/stdlib/spec/unit/puppet/type/anchor_spec.rb b/puppet/stdlib/spec/unit/puppet/type/anchor_spec.rb new file mode 100644 index 000000000..2030b83f2 --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/type/anchor_spec.rb @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby + +require 'puppet' + +anchor = Puppet::Type.type(:anchor).new(:name => "ntp::begin") + +describe anchor do + it "should stringify normally" do + anchor.to_s.should == "Anchor[ntp::begin]" + end +end diff --git a/puppet/stdlib/spec/unit/puppet/type/file_line_spec.rb b/puppet/stdlib/spec/unit/puppet/type/file_line_spec.rb new file mode 100644 index 000000000..e1c07ac6e --- /dev/null +++ b/puppet/stdlib/spec/unit/puppet/type/file_line_spec.rb @@ -0,0 +1,51 @@ +require 'puppet' +require 'tempfile' +describe Puppet::Type.type(:file_line) do + let :file_line do + Puppet::Type.type(:file_line).new(:name => 'foo', :line => 'line', :path => '/tmp/path') + end + it 'should accept a line and path' do + file_line[:line] = 'my_line' + file_line[:line].should == 'my_line' + file_line[:path] = '/my/path' + file_line[:path].should == '/my/path' + end + it 'should accept a match regex' do + file_line[:match] = '^foo.*$' + file_line[:match].should == '^foo.*$' + end + it 'should not accept a match regex that does not match the specified line' do + expect { + Puppet::Type.type(:file_line).new( + :name => 'foo', + :path => '/my/path', + :line => 'foo=bar', + :match => '^bar=blah$' + )}.to raise_error(Puppet::Error, /the value must be a regex that matches/) + end + it 'should accept a match regex that does match the specified line' do + expect { + Puppet::Type.type(:file_line).new( + :name => 'foo', + :path => '/my/path', + :line => 'foo=bar', + :match => '^\s*foo=.*$' + )}.not_to raise_error + end + it 'should accept posix filenames' do + file_line[:path] = '/tmp/path' + file_line[:path].should == '/tmp/path' + end + it 'should not accept unqualified path' do + expect { file_line[:path] = 'file' }.should raise_error(Puppet::Error, /File paths must be fully qualified/) + end + it 'should require that a line is specified' do + expect { Puppet::Type.type(:file_line).new(:name => 'foo', :path => '/tmp/file') }.should raise_error(Puppet::Error, /Both line and path are required attributes/) + end + it 'should require that a file is specified' do + expect { Puppet::Type.type(:file_line).new(:name => 'foo', :line => 'path') }.should raise_error(Puppet::Error, /Both line and path are required attributes/) + end + it 'should default to ensure => present' do + file_line[:ensure].should eq :present + end +end diff --git a/puppet/stdlib/spec/watchr.rb b/puppet/stdlib/spec/watchr.rb new file mode 100644 index 000000000..885ef1d5f --- /dev/null +++ b/puppet/stdlib/spec/watchr.rb @@ -0,0 +1,86 @@ +ENV['FOG_MOCK'] ||= 'true' +ENV['AUTOTEST'] = 'true' +ENV['WATCHR'] = '1' + +system 'clear' + +def growl(message) + growlnotify = `which growlnotify`.chomp + title = "Watchr Test Results" + image = case message + when /(\d+)\s+?(failure|error)/i + ($1.to_i == 0) ? "~/.watchr_images/passed.png" : "~/.watchr_images/failed.png" + else + '~/.watchr_images/unknown.png' + end + options = "-w -n Watchr --image '#{File.expand_path(image)}' -m '#{message}' '#{title}'" + system %(#{growlnotify} #{options} &) +end + +def run(cmd) + puts(cmd) + `#{cmd}` +end + +def run_spec_test(file) + if File.exist? file + result = run "rspec --format p --color #{file}" + growl result.split("\n").last + puts result + else + puts "FIXME: No test #{file} [#{Time.now}]" + end +end + +def filter_rspec(data) + data.split("\n").find_all do |l| + l =~ /^(\d+)\s+exampl\w+.*?(\d+).*?failur\w+.*?(\d+).*?pending/ + end.join("\n") +end + +def run_all_tests + system('clear') + files = Dir.glob("spec/**/*_spec.rb").join(" ") + result = run "rspec #{files}" + growl_results = filter_rspec result + growl growl_results + puts result + puts "GROWL: #{growl_results}" +end + +# Ctrl-\ +Signal.trap 'QUIT' do + puts " --- Running all tests ---\n\n" + run_all_tests +end + +@interrupted = false + +# Ctrl-C +Signal.trap 'INT' do + if @interrupted then + @wants_to_quit = true + abort("\n") + else + puts "Interrupt a second time to quit" + @interrupted = true + Kernel.sleep 1.5 + # raise Interrupt, nil # let the run loop catch it + run_suite + end +end + +def file2spec(file) + result = file.sub('lib/puppet/', 'spec/unit/puppet/').gsub(/\.rb$/, '_spec.rb') + result = file.sub('lib/facter/', 'spec/unit/facter/').gsub(/\.rb$/, '_spec.rb') +end + + +watch( 'spec/.*_spec\.rb' ) do |md| + #run_spec_test(md[0]) + run_all_tests +end +watch( 'lib/.*\.rb' ) do |md| + # run_spec_test(file2spec(md[0])) + run_all_tests +end diff --git a/puppet/stdlib/tests/file_line.pp b/puppet/stdlib/tests/file_line.pp new file mode 100644 index 000000000..eea693e15 --- /dev/null +++ b/puppet/stdlib/tests/file_line.pp @@ -0,0 +1,9 @@ +# This is a simple smoke test +# of the file_line resource type. +file { '/tmp/dansfile': + ensure => present +}-> +file_line { 'dans_line': + line => 'dan is awesome', + path => '/tmp/dansfile', +} diff --git a/puppet/stdlib/tests/init.pp b/puppet/stdlib/tests/init.pp new file mode 100644 index 000000000..9675d8374 --- /dev/null +++ b/puppet/stdlib/tests/init.pp @@ -0,0 +1 @@ +include stdlib