From d64665491075dd86fba37bc1bb02a1f689db821d Mon Sep 17 00:00:00 2001 From: Eugene Kirpichov Date: Fri, 7 Sep 2012 17:38:58 -0700 Subject: [PATCH] Initial commit --- deployment/puppet/mysql/.fixtures.yml | 3 + deployment/puppet/mysql/.gemfile | 5 + deployment/puppet/mysql/.gitignore | 2 + deployment/puppet/mysql/.travis.yml | 17 + deployment/puppet/mysql/CHANGELOG | 112 ++ deployment/puppet/mysql/LICENSE | 201 ++++ deployment/puppet/mysql/Modulefile | 8 + deployment/puppet/mysql/README.md | 124 +++ deployment/puppet/mysql/Rakefile | 1 + deployment/puppet/mysql/TODO | 8 + deployment/puppet/mysql/files/mysqltuner.pl | 966 ++++++++++++++++++ .../puppet/parser/functions/mysql_password.rb | 15 + .../lib/puppet/provider/database/mysql.rb | 42 + .../puppet/provider/database_grant/mysql.rb | 177 ++++ .../puppet/provider/database_user/mysql.rb | 42 + .../puppet/mysql/lib/puppet/type/database.rb | 17 + .../mysql/lib/puppet/type/database_grant.rb | 75 ++ .../mysql/lib/puppet/type/database_user.rb | 25 + deployment/puppet/mysql/manifests/backup.pp | 68 ++ deployment/puppet/mysql/manifests/config.pp | 122 +++ deployment/puppet/mysql/manifests/db.pp | 77 ++ deployment/puppet/mysql/manifests/init.pp | 24 + deployment/puppet/mysql/manifests/java.pp | 24 + deployment/puppet/mysql/manifests/params.pp | 91 ++ deployment/puppet/mysql/manifests/python.pp | 26 + deployment/puppet/mysql/manifests/ruby.pp | 28 + deployment/puppet/mysql/manifests/server.pp | 52 + .../manifests/server/account_security.pp | 13 + .../puppet/mysql/manifests/server/monitor.pp | 19 + .../mysql/manifests/server/mysqltuner.pp | 22 + .../mysql/spec/classes/mysql_backup_spec.rb | 33 + .../mysql/spec/classes/mysql_config_spec.rb | 236 +++++ .../mysql/spec/classes/mysql_init_spec.rb | 56 + .../mysql/spec/classes/mysql_java_spec.rb | 56 + .../mysql/spec/classes/mysql_python_spec.rb | 56 + .../mysql/spec/classes/mysql_ruby_spec.rb | 64 ++ .../mysql_server_account_security_spec.rb | 37 + .../spec/classes/mysql_server_monitor_spec.rb | 18 + .../mysql/spec/classes/mysql_server_spec.rb | 92 ++ .../mysql/spec/defines/mysql_db_spec.rb | 30 + .../mysql/spec/fixtures/manifests/site.pp | 0 deployment/puppet/mysql/spec/spec.opts | 6 + deployment/puppet/mysql/spec/spec_helper.rb | 1 + .../mysql/spec/unit/mysql_password_spec.rb | 30 + .../provider/database_grant/mysql_spec.rb | 81 ++ deployment/puppet/mysql/templates/my.cnf.erb | 42 + .../puppet/mysql/templates/my.cnf.pass.erb | 6 + .../puppet/mysql/templates/mysqlbackup.sh.erb | 23 + deployment/puppet/mysql/tests/backup.pp | 8 + deployment/puppet/mysql/tests/init.pp | 1 + deployment/puppet/mysql/tests/java.pp | 1 + .../puppet/mysql/tests/mysql_database.pp | 12 + deployment/puppet/mysql/tests/mysql_grant.pp | 3 + deployment/puppet/mysql/tests/mysql_user.pp | 23 + deployment/puppet/mysql/tests/python.pp | 1 + deployment/puppet/mysql/tests/ruby.pp | 1 + deployment/puppet/mysql/tests/server.pp | 3 + .../mysql/tests/server/account_security.pp | 4 + 58 files changed, 3330 insertions(+) create mode 100644 deployment/puppet/mysql/.fixtures.yml create mode 100644 deployment/puppet/mysql/.gemfile create mode 100644 deployment/puppet/mysql/.gitignore create mode 100644 deployment/puppet/mysql/.travis.yml create mode 100644 deployment/puppet/mysql/CHANGELOG create mode 100644 deployment/puppet/mysql/LICENSE create mode 100644 deployment/puppet/mysql/Modulefile create mode 100644 deployment/puppet/mysql/README.md create mode 100644 deployment/puppet/mysql/Rakefile create mode 100644 deployment/puppet/mysql/TODO create mode 100644 deployment/puppet/mysql/files/mysqltuner.pl create mode 100644 deployment/puppet/mysql/lib/puppet/parser/functions/mysql_password.rb create mode 100644 deployment/puppet/mysql/lib/puppet/provider/database/mysql.rb create mode 100644 deployment/puppet/mysql/lib/puppet/provider/database_grant/mysql.rb create mode 100644 deployment/puppet/mysql/lib/puppet/provider/database_user/mysql.rb create mode 100644 deployment/puppet/mysql/lib/puppet/type/database.rb create mode 100644 deployment/puppet/mysql/lib/puppet/type/database_grant.rb create mode 100644 deployment/puppet/mysql/lib/puppet/type/database_user.rb create mode 100644 deployment/puppet/mysql/manifests/backup.pp create mode 100644 deployment/puppet/mysql/manifests/config.pp create mode 100644 deployment/puppet/mysql/manifests/db.pp create mode 100644 deployment/puppet/mysql/manifests/init.pp create mode 100644 deployment/puppet/mysql/manifests/java.pp create mode 100644 deployment/puppet/mysql/manifests/params.pp create mode 100644 deployment/puppet/mysql/manifests/python.pp create mode 100644 deployment/puppet/mysql/manifests/ruby.pp create mode 100644 deployment/puppet/mysql/manifests/server.pp create mode 100644 deployment/puppet/mysql/manifests/server/account_security.pp create mode 100644 deployment/puppet/mysql/manifests/server/monitor.pp create mode 100644 deployment/puppet/mysql/manifests/server/mysqltuner.pp create mode 100644 deployment/puppet/mysql/spec/classes/mysql_backup_spec.rb create mode 100644 deployment/puppet/mysql/spec/classes/mysql_config_spec.rb create mode 100644 deployment/puppet/mysql/spec/classes/mysql_init_spec.rb create mode 100644 deployment/puppet/mysql/spec/classes/mysql_java_spec.rb create mode 100644 deployment/puppet/mysql/spec/classes/mysql_python_spec.rb create mode 100644 deployment/puppet/mysql/spec/classes/mysql_ruby_spec.rb create mode 100644 deployment/puppet/mysql/spec/classes/mysql_server_account_security_spec.rb create mode 100644 deployment/puppet/mysql/spec/classes/mysql_server_monitor_spec.rb create mode 100644 deployment/puppet/mysql/spec/classes/mysql_server_spec.rb create mode 100644 deployment/puppet/mysql/spec/defines/mysql_db_spec.rb create mode 100644 deployment/puppet/mysql/spec/fixtures/manifests/site.pp create mode 100644 deployment/puppet/mysql/spec/spec.opts create mode 100644 deployment/puppet/mysql/spec/spec_helper.rb create mode 100644 deployment/puppet/mysql/spec/unit/mysql_password_spec.rb create mode 100644 deployment/puppet/mysql/spec/unit/puppet/provider/database_grant/mysql_spec.rb create mode 100644 deployment/puppet/mysql/templates/my.cnf.erb create mode 100644 deployment/puppet/mysql/templates/my.cnf.pass.erb create mode 100644 deployment/puppet/mysql/templates/mysqlbackup.sh.erb create mode 100644 deployment/puppet/mysql/tests/backup.pp create mode 100644 deployment/puppet/mysql/tests/init.pp create mode 100644 deployment/puppet/mysql/tests/java.pp create mode 100644 deployment/puppet/mysql/tests/mysql_database.pp create mode 100644 deployment/puppet/mysql/tests/mysql_grant.pp create mode 100644 deployment/puppet/mysql/tests/mysql_user.pp create mode 100644 deployment/puppet/mysql/tests/python.pp create mode 100644 deployment/puppet/mysql/tests/ruby.pp create mode 100644 deployment/puppet/mysql/tests/server.pp create mode 100644 deployment/puppet/mysql/tests/server/account_security.pp diff --git a/deployment/puppet/mysql/.fixtures.yml b/deployment/puppet/mysql/.fixtures.yml new file mode 100644 index 0000000000..c395f0c25e --- /dev/null +++ b/deployment/puppet/mysql/.fixtures.yml @@ -0,0 +1,3 @@ +fixtures: + symlinks: + "mysql": "#{source_dir}" diff --git a/deployment/puppet/mysql/.gemfile b/deployment/puppet/mysql/.gemfile new file mode 100644 index 0000000000..9aad840c0a --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/.gitignore b/deployment/puppet/mysql/.gitignore new file mode 100644 index 0000000000..444fe1a3a5 --- /dev/null +++ b/deployment/puppet/mysql/.gitignore @@ -0,0 +1,2 @@ +*.swp +pkg/ diff --git a/deployment/puppet/mysql/.travis.yml b/deployment/puppet/mysql/.travis.yml new file mode 100644 index 0000000000..066317e149 --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/CHANGELOG b/deployment/puppet/mysql/CHANGELOG new file mode 100644 index 0000000000..d16259c07c --- /dev/null +++ b/deployment/puppet/mysql/CHANGELOG @@ -0,0 +1,112 @@ +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/deployment/puppet/mysql/LICENSE b/deployment/puppet/mysql/LICENSE new file mode 100644 index 0000000000..8d968b6cb0 --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/Modulefile b/deployment/puppet/mysql/Modulefile new file mode 100644 index 0000000000..ccb4813990 --- /dev/null +++ b/deployment/puppet/mysql/Modulefile @@ -0,0 +1,8 @@ +name 'puppetlabs-mysql' +version '0.3.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' diff --git a/deployment/puppet/mysql/README.md b/deployment/puppet/mysql/README.md new file mode 100644 index 0000000000..fd73898eb4 --- /dev/null +++ b/deployment/puppet/mysql/README.md @@ -0,0 +1,124 @@ +# 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'] , + } + +A resource default can be specified to handle dependency: + + Database { + require => Class['mysql::server'], + } diff --git a/deployment/puppet/mysql/Rakefile b/deployment/puppet/mysql/Rakefile new file mode 100644 index 0000000000..cd3d379958 --- /dev/null +++ b/deployment/puppet/mysql/Rakefile @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/rake_tasks' diff --git a/deployment/puppet/mysql/TODO b/deployment/puppet/mysql/TODO new file mode 100644 index 0000000000..3913293930 --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/files/mysqltuner.pl b/deployment/puppet/mysql/files/mysqltuner.pl new file mode 100644 index 0000000000..f61881ce81 --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/lib/puppet/parser/functions/mysql_password.rb b/deployment/puppet/mysql/lib/puppet/parser/functions/mysql_password.rb new file mode 100644 index 0000000000..74281b827e --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/lib/puppet/provider/database/mysql.rb b/deployment/puppet/mysql/lib/puppet/provider/database/mysql.rb new file mode 100644 index 0000000000..7964a0c2db --- /dev/null +++ b/deployment/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('-NBe', "show databases").split("\n").collect do |name| + new(:name => name) + end + end + + def create + mysql('-NBe', "create database `#{@resource[:name]}` character set #{resource[:charset]}") + end + + def destroy + mysqladmin('-f', 'drop', @resource[:name]) + end + + def charset + mysql('-NBe', "show create database `#{resource[:name]}`").match(/.*?(\S+)\s\*\//)[1] + end + + def charset=(value) + mysql('-NBe', "alter database `#{resource[:name]}` CHARACTER SET #{value}") + end + + def exists? + begin + mysql('-NBe', "show databases").match(/^#{@resource[:name]}$/) + rescue => e + debug(e.message) + return nil + end + end + +end + diff --git a/deployment/puppet/mysql/lib/puppet/provider/database_grant/mysql.rb b/deployment/puppet/mysql/lib/puppet/provider/database_grant/mysql.rb new file mode 100644 index 0000000000..e5bfb15792 --- /dev/null +++ b/deployment/puppet/mysql/lib/puppet/provider/database_grant/mysql.rb @@ -0,0 +1,177 @@ +# 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("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("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 "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 "mysql", "-e", "INSERT INTO user (host, user) VALUES ('%s', '%s')" % [ + name[:host], name[:user], + ] + when :db + mysql "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 "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( "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 "mysql", "-Be", 'select * from user where user="%s" and host="%s"' % [ name[:user], name[:host] ] + when :db + privs = mysql "mysql", "-Be", 'select * from 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 + + mysql "mysql", "-Be", stmt + mysql_flush + end +end diff --git a/deployment/puppet/mysql/lib/puppet/provider/database_user/mysql.rb b/deployment/puppet/mysql/lib/puppet/provider/database_user/mysql.rb new file mode 100644 index 0000000000..fb647bdb25 --- /dev/null +++ b/deployment/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("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("mysql", "-e", "create user '%s' identified by PASSWORD '%s'" % [ @resource[:name].sub("@", "'@'"), @resource.value(:password_hash) ]) + end + + def destroy + mysql("mysql", "-e", "drop user '%s'" % @resource.value(:name).sub("@", "'@'") ) + end + + def password_hash + mysql("mysql", "-NBe", "select password from user where CONCAT(user, '@', host) = '%s'" % @resource.value(:name)).chomp + end + + def password_hash=(string) + mysql("mysql", "-e", "SET PASSWORD FOR '%s' = '%s'" % [ @resource[:name].sub("@", "'@'"), string ] ) + end + + def exists? + not mysql("mysql", "-NBe", "select '1' from user where CONCAT(user, '@', host) = '%s'" % @resource.value(:name)).empty? + end + + def flush + @property_hash.clear + mysqladmin "flush-privileges" + end + +end diff --git a/deployment/puppet/mysql/lib/puppet/type/database.rb b/deployment/puppet/mysql/lib/puppet/type/database.rb new file mode 100644 index 0000000000..a27fc007ef --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/lib/puppet/type/database_grant.rb b/deployment/puppet/mysql/lib/puppet/type/database_grant.rb new file mode 100644 index 0000000000..965695bff8 --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/lib/puppet/type/database_user.rb b/deployment/puppet/mysql/lib/puppet/type/database_user.rb new file mode 100644 index 0000000000..ef98547019 --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/manifests/backup.pp b/deployment/puppet/mysql/manifests/backup.pp new file mode 100644 index 0000000000..741d497b3d --- /dev/null +++ b/deployment/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' ], + 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/deployment/puppet/mysql/manifests/config.pp b/deployment/puppet/mysql/manifests/config.pp new file mode 100644 index 0000000000..fc98efe66b --- /dev/null +++ b/deployment/puppet/mysql/manifests/config.pp @@ -0,0 +1,122 @@ +# 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 +# +# 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, + $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 +) inherits mysql::params { + + File { + owner => 'root', + group => $root_group, + mode => '0400', + notify => Exec['mysqld-restart'], + } + + 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 => Exec['mysqld-restart'], + 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/deployment/puppet/mysql/manifests/db.pp b/deployment/puppet/mysql/manifests/db.pp new file mode 100644 index 0000000000..0663edccff --- /dev/null +++ b/deployment/puppet/mysql/manifests/db.pp @@ -0,0 +1,77 @@ +# 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. +# +# 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 +) { + + database { $name: + ensure => present, + charset => $charset, + provider => 'mysql', + require => Class['mysql::server'], + } + + database_user { "${user}@${host}": + ensure => present, + password_hash => mysql_password($password), + provider => 'mysql', + require => Database[$name], + } + + 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/deployment/puppet/mysql/manifests/init.pp b/deployment/puppet/mysql/manifests/init.pp new file mode 100644 index 0000000000..dff2ddd879 --- /dev/null +++ b/deployment/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': + name => $package_name, + ensure => $package_ensure, + } + +} diff --git a/deployment/puppet/mysql/manifests/java.pp b/deployment/puppet/mysql/manifests/java.pp new file mode 100644 index 0000000000..2713c058a1 --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/manifests/params.pp b/deployment/puppet/mysql/manifests/params.pp new file mode 100644 index 0000000000..1cec72fc43 --- /dev/null +++ b/deployment/puppet/mysql/manifests/params.pp @@ -0,0 +1,91 @@ +# 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 + + 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' + $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' + $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' + $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: { + fail("Unsupported osfamily: ${::osfamily} operatingsystem: ${::operatingsystem}, module ${module_name} only support osfamily RedHat Debian and FreeBSD") + } + } + +} diff --git a/deployment/puppet/mysql/manifests/python.pp b/deployment/puppet/mysql/manifests/python.pp new file mode 100644 index 0000000000..fb7f6436ff --- /dev/null +++ b/deployment/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': + name => $package_name, + ensure => $package_ensure, + } + +} diff --git a/deployment/puppet/mysql/manifests/ruby.pp b/deployment/puppet/mysql/manifests/ruby.pp new file mode 100644 index 0000000000..9c630f8fe9 --- /dev/null +++ b/deployment/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': + name => $package_name, + ensure => $package_ensure, + provider => $package_provider, + } + +} diff --git a/deployment/puppet/mysql/manifests/server.pp b/deployment/puppet/mysql/manifests/server.pp new file mode 100644 index 0000000000..1f711d434a --- /dev/null +++ b/deployment/puppet/mysql/manifests/server.pp @@ -0,0 +1,52 @@ +# 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 +) inherits mysql::params { + + Class['mysql::server'] -> Class['mysql::config'] + + $config_class = {} + $config_class['mysql::config'] = $config_hash + + create_resources( 'class', $config_class ) + + package { 'mysql-server': + name => $package_name, + ensure => $package_ensure, + } + + if $enabled { + $service_ensure = 'running' + } else { + $service_ensure = 'stopped' + } + + service { 'mysqld': + name => $service_name, + ensure => $service_ensure, + enable => $enabled, + require => Package['mysql-server'], + provider => $service_provider, + } + +} diff --git a/deployment/puppet/mysql/manifests/server/account_security.pp b/deployment/puppet/mysql/manifests/server/account_security.pp new file mode 100644 index 0000000000..8d819582c1 --- /dev/null +++ b/deployment/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', + "@${::fqdn}", "@${::hostname}", '@localhost', '@%' ]: + ensure => 'absent', + require => Class['mysql::config'], + } + database { 'test': + ensure => 'absent', + require => Class['mysql::config'], + } +} diff --git a/deployment/puppet/mysql/manifests/server/monitor.pp b/deployment/puppet/mysql/manifests/server/monitor.pp new file mode 100644 index 0000000000..9692ff11d7 --- /dev/null +++ b/deployment/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}": + password_hash => mysql_password($mysql_monitor_password), + ensure => present, + } + + 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/deployment/puppet/mysql/manifests/server/mysqltuner.pp b/deployment/puppet/mysql/manifests/server/mysqltuner.pp new file mode 100644 index 0000000000..416a3dac5c --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/spec/classes/mysql_backup_spec.rb b/deployment/puppet/mysql/spec/classes/mysql_backup_spec.rb new file mode 100644 index 0000000000..e7f99b8503 --- /dev/null +++ b/deployment/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' ] + )} + + 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/deployment/puppet/mysql/spec/classes/mysql_config_spec.rb b/deployment/puppet/mysql/spec/classes/mysql_config_spec.rb new file mode 100644 index 0000000000..c71d0caca5 --- /dev/null +++ b/deployment/puppet/mysql/spec/classes/mysql_config_spec.rb @@ -0,0 +1,236 @@ +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', + :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', + :root_group => 'wheel', + }, + 'Redhat' => { + :datadir => '/var/lib/mysql', + :service_name => 'mysqld', + :config_file => '/etc/my.cnf', + :socket => '/var/lib/mysql/mysql.sock', + :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 -pfoo 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 -pbar password foo', + 'logoutput' => true, + 'unless' => "mysqladmin -u root -pfoo 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', + :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]}", + "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 -pbar password foo', + 'logoutput' => true, + 'unless' => "mysqladmin -u root -pfoo 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 do + subject + end.should 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 do + subject + end.should raise_error(Puppet::Error, /required when ssl is true/) + end + + end + +end diff --git a/deployment/puppet/mysql/spec/classes/mysql_init_spec.rb b/deployment/puppet/mysql/spec/classes/mysql_init_spec.rb new file mode 100644 index 0000000000..de7cb65959 --- /dev/null +++ b/deployment/puppet/mysql/spec/classes/mysql_init_spec.rb @@ -0,0 +1,56 @@ +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 do + subject + end.should raise_error(/Unsupported osfamily: foo/) + end + end + +end diff --git a/deployment/puppet/mysql/spec/classes/mysql_java_spec.rb b/deployment/puppet/mysql/spec/classes/mysql_java_spec.rb new file mode 100644 index 0000000000..6a4387f723 --- /dev/null +++ b/deployment/puppet/mysql/spec/classes/mysql_java_spec.rb @@ -0,0 +1,56 @@ +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 do + subject + end.should raise_error(/Unsupported osfamily: foo/) + end + end + +end diff --git a/deployment/puppet/mysql/spec/classes/mysql_python_spec.rb b/deployment/puppet/mysql/spec/classes/mysql_python_spec.rb new file mode 100644 index 0000000000..2082b30907 --- /dev/null +++ b/deployment/puppet/mysql/spec/classes/mysql_python_spec.rb @@ -0,0 +1,56 @@ +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 do + subject + end.should raise_error(/Unsupported osfamily: foo/) + end + end + +end diff --git a/deployment/puppet/mysql/spec/classes/mysql_ruby_spec.rb b/deployment/puppet/mysql/spec/classes/mysql_ruby_spec.rb new file mode 100644 index 0000000000..f25129df32 --- /dev/null +++ b/deployment/puppet/mysql/spec/classes/mysql_ruby_spec.rb @@ -0,0 +1,64 @@ +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 do + subject + end.should raise_error(/Unsupported osfamily: foo/) + end + end + +end diff --git a/deployment/puppet/mysql/spec/classes/mysql_server_account_security_spec.rb b/deployment/puppet/mysql/spec/classes/mysql_server_account_security_spec.rb new file mode 100644 index 0000000000..3fdacb7aa3 --- /dev/null +++ b/deployment/puppet/mysql/spec/classes/mysql_server_account_security_spec.rb @@ -0,0 +1,37 @@ +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[@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/deployment/puppet/mysql/spec/classes/mysql_server_monitor_spec.rb b/deployment/puppet/mysql/spec/classes/mysql_server_monitor_spec.rb new file mode 100644 index 0000000000..b7591cc13f --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/spec/classes/mysql_server_spec.rb b/deployment/puppet/mysql/spec/classes/mysql_server_spec.rb new file mode 100644 index 0000000000..e4d6c5d199 --- /dev/null +++ b/deployment/puppet/mysql/spec/classes/mysql_server_spec.rb @@ -0,0 +1,92 @@ +require 'spec_helper' +describe 'mysql::server' do + + let :constant_parameter_defaults do + {:config_hash => {}, + :package_ensure => 'present', + :enabled => 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 + } + ].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 { should contain_service('mysqld').with( + :name => param_values[:service_name], + :ensure => param_values[:enabled] ? 'running' : 'stopped', + :enable => param_values[:enabled], + :require => 'Package[mysql-server]' + )} + + it { should contain_service('mysqld').without_provider } + end + end + end + end + end +end diff --git a/deployment/puppet/mysql/spec/defines/mysql_db_spec.rb b/deployment/puppet/mysql/spec/defines/mysql_db_spec.rb new file mode 100644 index 0000000000..ea2c62111b --- /dev/null +++ b/deployment/puppet/mysql/spec/defines/mysql_db_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe 'mysql::db', :type => :define do + let(:title) { 'test_db' } + + let(:params) { + { 'user' => 'testuser', + 'password' => 'testpass', + } + } + + 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 +end diff --git a/deployment/puppet/mysql/spec/fixtures/manifests/site.pp b/deployment/puppet/mysql/spec/fixtures/manifests/site.pp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/deployment/puppet/mysql/spec/spec.opts b/deployment/puppet/mysql/spec/spec.opts new file mode 100644 index 0000000000..91cd6427ed --- /dev/null +++ b/deployment/puppet/mysql/spec/spec.opts @@ -0,0 +1,6 @@ +--format +s +--colour +--loadby +mtime +--backtrace diff --git a/deployment/puppet/mysql/spec/spec_helper.rb b/deployment/puppet/mysql/spec/spec_helper.rb new file mode 100644 index 0000000000..2c6f56649a --- /dev/null +++ b/deployment/puppet/mysql/spec/spec_helper.rb @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/module_spec_helper' diff --git a/deployment/puppet/mysql/spec/unit/mysql_password_spec.rb b/deployment/puppet/mysql/spec/unit/mysql_password_spec.rb new file mode 100644 index 0000000000..4e67ed99ef --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/spec/unit/puppet/provider/database_grant/mysql_spec.rb b/deployment/puppet/mysql/spec/unit/puppet/provider/database_grant/mysql_spec.rb new file mode 100644 index 0000000000..58a64eab5f --- /dev/null +++ b/deployment/puppet/mysql/spec/unit/puppet/provider/database_grant/mysql_spec.rb @@ -0,0 +1,81 @@ +require 'puppet' +require 'mocha' +RSpec.configure do |config| + config.mock_with :mocha +end +provider_class = Puppet::Type.type(:database_grant).provider(:mysql) +describe provider_class do + before :each do + @resource = Puppet::Type::Database_grant.new( + { :privileges => 'all', :provider => 'mysql', :name => 'user@host'} + ) + @provider = provider_class.new(@resource) + end + it 'should query privilegess from the database' do + provider_class.expects(:mysql) .with('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('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('mysql', '-Be', 'select * from 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('mysql', '-Be', 'select * from 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('mysql', '-Be', 'select * from 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('mysql', '-NBe', 'SELECT "1" FROM user WHERE user = \'user\' AND host = \'host\'').returns "1\n" + provider_class.expects(:mysql).with('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("flush-privileges") + @provider.privileges=(['all']) + end + + it 'should be able to set partial privileges' do + provider_class.expects(:mysql).with('mysql', '-NBe', 'SELECT "1" FROM user WHERE user = \'user\' AND host = \'host\'').returns "1\n" + provider_class.expects(:mysql).with('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("flush-privileges") + @provider.privileges=(['Select_priv', 'Update_priv']) + end + + it 'should be case insensitive' do + provider_class.expects(:mysql).with('mysql', '-NBe', 'SELECT "1" FROM user WHERE user = \'user\' AND host = \'host\'').returns "1\n" + provider_class.expects(:mysql).with('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('flush-privileges') + @provider.privileges=(['SELECT_PRIV', 'insert_priv', 'UpDaTe_pRiV']) + end +end diff --git a/deployment/puppet/mysql/templates/my.cnf.erb b/deployment/puppet/mysql/templates/my.cnf.erb new file mode 100644 index 0000000000..e09c7c94f7 --- /dev/null +++ b/deployment/puppet/mysql/templates/my.cnf.erb @@ -0,0 +1,42 @@ +[client] +port = <%= port %> +socket = <%= socket %> +[mysqld_safe] +socket = <%= socket %> +nice = 0 +[mysqld] +user = mysql +socket = <%= socket %> +port = <%= port %> +basedir = <%= basedir %> +datadir = <%= datadir %> +tmpdir = /tmp +skip-external-locking +bind-address = <%= bind_address %> +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/deployment/puppet/mysql/templates/my.cnf.pass.erb b/deployment/puppet/mysql/templates/my.cnf.pass.erb new file mode 100644 index 0000000000..38a3a4aefc --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/templates/mysqlbackup.sh.erb b/deployment/puppet/mysql/templates/mysqlbackup.sh.erb new file mode 100644 index 0000000000..a2dba331a5 --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/tests/backup.pp b/deployment/puppet/mysql/tests/backup.pp new file mode 100644 index 0000000000..cb669e6db8 --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/tests/init.pp b/deployment/puppet/mysql/tests/init.pp new file mode 100644 index 0000000000..846121b7df --- /dev/null +++ b/deployment/puppet/mysql/tests/init.pp @@ -0,0 +1 @@ +include mysql diff --git a/deployment/puppet/mysql/tests/java.pp b/deployment/puppet/mysql/tests/java.pp new file mode 100644 index 0000000000..0fc009a6da --- /dev/null +++ b/deployment/puppet/mysql/tests/java.pp @@ -0,0 +1 @@ +class { 'mysql::java':} diff --git a/deployment/puppet/mysql/tests/mysql_database.pp b/deployment/puppet/mysql/tests/mysql_database.pp new file mode 100644 index 0000000000..8747f707d5 --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/tests/mysql_grant.pp b/deployment/puppet/mysql/tests/mysql_grant.pp new file mode 100644 index 0000000000..8d9654740a --- /dev/null +++ b/deployment/puppet/mysql/tests/mysql_grant.pp @@ -0,0 +1,3 @@ +database_grant{'test1@localhost/redmine': + privileges => [update], +} diff --git a/deployment/puppet/mysql/tests/mysql_user.pp b/deployment/puppet/mysql/tests/mysql_user.pp new file mode 100644 index 0000000000..f63908431b --- /dev/null +++ b/deployment/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/deployment/puppet/mysql/tests/python.pp b/deployment/puppet/mysql/tests/python.pp new file mode 100644 index 0000000000..04f7ffa1ac --- /dev/null +++ b/deployment/puppet/mysql/tests/python.pp @@ -0,0 +1 @@ +class { 'mysql::python':} diff --git a/deployment/puppet/mysql/tests/ruby.pp b/deployment/puppet/mysql/tests/ruby.pp new file mode 100644 index 0000000000..e84c046a31 --- /dev/null +++ b/deployment/puppet/mysql/tests/ruby.pp @@ -0,0 +1 @@ +include mysql::ruby diff --git a/deployment/puppet/mysql/tests/server.pp b/deployment/puppet/mysql/tests/server.pp new file mode 100644 index 0000000000..47e2fa3015 --- /dev/null +++ b/deployment/puppet/mysql/tests/server.pp @@ -0,0 +1,3 @@ +class { 'mysql::server': + config_hash => { 'root_password' => 'password', }, +} diff --git a/deployment/puppet/mysql/tests/server/account_security.pp b/deployment/puppet/mysql/tests/server/account_security.pp new file mode 100644 index 0000000000..de393cce4d --- /dev/null +++ b/deployment/puppet/mysql/tests/server/account_security.pp @@ -0,0 +1,4 @@ +class { 'mysql::server': + config_hash => { 'root_password' => 'password', }, +} +class { 'mysql::server::account_security': }